diff --git a/.github/workflows/celo-monorepo.yml b/.github/workflows/celo-monorepo.yml index 239b67e7426..a60af96775e 100644 --- a/.github/workflows/celo-monorepo.yml +++ b/.github/workflows/celo-monorepo.yml @@ -9,10 +9,18 @@ on: branches: - master - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' pull_request: branches: - master - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' concurrency: group: celo-monorepo-${{ github.ref }} @@ -24,7 +32,7 @@ defaults: env: # Increment these to force cache rebuilding - NODE_MODULE_CACHE_VERSION: 7 + NODE_MODULE_CACHE_VERSION: 8 NODE_OPTIONS: '--max-old-space-size=4096' TERM: dumb GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.parallel=false -Dorg.gradle.configureondemand=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' diff --git a/.github/workflows/protocol-devchain-anvil.yml b/.github/workflows/protocol-devchain-anvil.yml index 7e9461089e7..483a5dcb6d6 100644 --- a/.github/workflows/protocol-devchain-anvil.yml +++ b/.github/workflows/protocol-devchain-anvil.yml @@ -92,7 +92,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: ${{ env.SUPPORTED_FOUNDRY_VERSION }} + version: 'nightly-fa0e0c2ca3ae75895dd19173a02faf88509c0608' - name: Install forge dependencies run: forge install diff --git a/.github/workflows/protocol_tests.yml b/.github/workflows/protocol_tests.yml index 48e1313930e..c6025ebe8c8 100644 --- a/.github/workflows/protocol_tests.yml +++ b/.github/workflows/protocol_tests.yml @@ -4,15 +4,23 @@ on: branches: - master - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' pull_request: branches: - master - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' env: # Increment these to force cache rebuilding FOUNDRY_CACHE_KEY: 2 - # Supported Foundry version defined at celo-org (GitHub organisation) level, for consistency across workflows. + # Supported Foundry version defined at celo-org (GitHub organisation) level, for consistency across workflows. Please contact DevOps to update value. SUPPORTED_FOUNDRY_VERSION: ${{ vars.SUPPORTED_FOUNDRY_VERSION }} jobs: @@ -54,7 +62,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: ${{ env.SUPPORTED_FOUNDRY_VERSION }} + version: 'nightly-fa0e0c2ca3ae75895dd19173a02faf88509c0608' # TODO: revert back to env var - name: Install forge dependencies run: forge install @@ -132,7 +140,6 @@ jobs: run: ./scripts/foundry/create_and_migrate_anvil_devchain.sh - name: Run migration tests against local anvil devchain - if: success() || failure() run: | source ./scripts/foundry/constants.sh @@ -141,10 +148,9 @@ jobs: --fork-url $ANVIL_RPC_URL - name: Run e2e tests against local anvil devchain - if: success() || failure() run: | source ./scripts/foundry/constants.sh FOUNDRY_PROFILE=devchain forge test -vvv \ --match-path "test-sol/devchain/e2e/*" \ - --fork-url $ANVIL_RPC_URL \ No newline at end of file + --fork-url $ANVIL_RPC_URL diff --git a/.gitmodules b/.gitmodules index 5d63bcd6e59..f3440efa785 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,3 +20,7 @@ path = packages/protocol/lib/celo-foundry url = https://github.com/celo-org/celo-foundry branch = celo-foundry-v0.5.13 +[submodule "packages/protocol/lib/solidity-bytes-utils-8"] + path = packages/protocol/lib/solidity-bytes-utils-8 + url = https://github.com/GNSPS/solidity-bytes-utils + branch = master diff --git a/packages/protocol/contractPackages.ts b/packages/protocol/contractPackages.ts index 45a2e650558..b75d67fa521 100644 --- a/packages/protocol/contractPackages.ts +++ b/packages/protocol/contractPackages.ts @@ -48,12 +48,24 @@ export const SOLIDITY_08_PACKAGE = { proxiesPath: '/', // Proxies are still with 0.5 contracts // Proxies shouldn't have to be added to a list manually // https://github.com/celo-org/celo-monorepo/issues/10555 - contracts: ['GasPriceMinimum', 'FeeCurrencyDirectory', 'CeloDistributionSchedule'], + contracts: [ + 'GasPriceMinimum', + 'FeeCurrencyDirectory', + 'CeloUnreleasedTreasury', + 'Validators', + 'EpochManager', + 'EpochManagerEnabler', + 'ScoreManager', + ], proxyContracts: [ 'GasPriceMinimumProxy', 'FeeCurrencyDirectoryProxy', 'MentoFeeCurrencyAdapterV1', - 'CeloDistributionScheduleProxy', + 'CeloUnreleasedTreasuryProxy', + 'ValidatorsProxy', + 'EpochManagerProxy', + 'EpochManagerEnablerProxy', + 'ScoreManagerProxy', ], truffleConfig: 'truffle-config0.8.js', } satisfies ContractPackage diff --git a/packages/protocol/contracts-0.8/common/CeloDistributionSchedule.sol b/packages/protocol/contracts-0.8/common/CeloDistributionSchedule.sol deleted file mode 100644 index 1674a07c9df..00000000000 --- a/packages/protocol/contracts-0.8/common/CeloDistributionSchedule.sol +++ /dev/null @@ -1,341 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.7 <0.8.20; - -import "@openzeppelin/contracts8/security/ReentrancyGuard.sol"; -import "@openzeppelin/contracts8/utils/math/Math.sol"; - -import "./UsingRegistry.sol"; -import "../common/IsL2Check.sol"; - -import "../../contracts/common/FixidityLib.sol"; -import "../../contracts/common/Initializable.sol"; -import "../../contracts-0.8/common/interfaces/ICeloToken.sol"; - -/** - * @title Contract for distributing CELO token based on a schedule. - */ -contract CeloDistributionSchedule is UsingRegistry, ReentrancyGuard, Initializable, IsL2Check { - using FixidityLib for FixidityLib.Fraction; - - uint256 constant GENESIS_CELO_SUPPLY = 600000000 ether; // 600 million Celo - uint256 constant YEARS_LINEAR = 15; - uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; - - bool public areDependenciesSet; - uint256 constant GENESIS_START_TIME = 1587587214; // Copied over from `EpochRewards().startTime()`. - uint256 public l2StartTime; - uint256 public totalAllocatedAtL2Start; - - uint256 public totalDistributedBySchedule; - address public communityRewardFund; - address public carbonOffsettingPartner; - - FixidityLib.Fraction private communityRewardFraction; - FixidityLib.Fraction private carbonOffsettingFraction; - - event CommunityRewardFractionSet(uint256 fraction); - event CarbonOffsettingFundSet(address indexed partner, uint256 fraction); - - modifier whenActivated() { - require(areDependenciesSet, "Distribution schedule has not been activated."); - _; - } - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /** - * @notice A constructor for initialising a new instance of a CeloDistributionSchedule contract. - * @param registryAddress The address of the registry core smart contract. - */ - function initialize(address registryAddress) external initializer { - _transferOwnership(msg.sender); - setRegistry(registryAddress); - } - - /** - * @notice Sets the distribution schedule dependencies during L2 transition. - * @param _l2StartTime The timestamp of L1 to L2 transition. - * @param _communityRewardFraction The percentage of rewards that go the community funds. - * @param _carbonOffsettingPartner The address of the carbon offsetting partner. - * @param _carbonOffsettingFraction The percentage of rewards going to carbon offsetting partner. - */ - function activate( - uint256 _l2StartTime, - uint256 _communityRewardFraction, - address _carbonOffsettingPartner, - uint256 _carbonOffsettingFraction - ) external onlyOwner onlyL2 { - require(address(this).balance > 0, "Contract does not have CELO balance."); - require(!areDependenciesSet, "Contract has already been activated."); - require(block.timestamp > _l2StartTime, "L2 start time cannot be set to a future date."); - ICeloToken celoToken = ICeloToken(address(getCeloToken())); - require( - registry.getAddressForOrDie(CELO_DISTRIBUTION_SCHEDULE_ID) == address(this), - "CeloDistributionSchedule address is incorrectly set in Registry." - ); - areDependenciesSet = true; - l2StartTime = _l2StartTime; - communityRewardFund = address(getGovernance()); - totalAllocatedAtL2Start = celoToken.allocatedSupply(); - setCommunityRewardFraction(_communityRewardFraction); - setCarbonOffsettingFund(_carbonOffsettingPartner, _carbonOffsettingFraction); - } - - /** - * @notice Distributes CELO to the community and carbon offsetting funds according to the predefined schedule. - */ - function distributeAccordingToSchedule() external nonReentrant onlyL2 returns (bool) { - ( - uint256 targetCeloDistribution, - uint256 communityRewardFundDistributionAmount, - uint256 carbonOffsettingPartnerDistributionAmount - ) = getTargetCeloDistribution(); - - ICeloToken celoToken = ICeloToken(address(getCeloToken())); - - require( - targetCeloDistribution >= celoToken.allocatedSupply(), - "Contract balance is insufficient." - ); - - uint256 distributableAmount = Math.min( - getRemainingBalanceToDistribute(), - targetCeloDistribution - celoToken.allocatedSupply() - ); - - require(distributableAmount > 0, "Distributable amount must be greater than zero."); - - totalDistributedBySchedule += distributableAmount; - - require( - celoToken.transfer(communityRewardFund, communityRewardFundDistributionAmount), - "Failed to transfer to community partner." - ); - - require( - celoToken.transfer(carbonOffsettingPartner, carbonOffsettingPartnerDistributionAmount), - "Failed to transfer to carbon offsetting partner." - ); - return true; - } - - /** - * @notice Returns the community reward fraction. - * @return The percentage of total reward which goes to the community funds. - */ - function getCommunityRewardFraction() external view returns (uint256) { - return communityRewardFraction.unwrap(); - } - - /** - * @notice Returns the carbon offsetting partner reward fraction. - * @return The percentage of total reward which goes to the carbon offsetting partner. - */ - function getCarbonOffsettingFraction() external view returns (uint256) { - return carbonOffsettingFraction.unwrap(); - } - - /** - * @return The total balance distributed by the CeloDistributionSchedule contract. - */ - function getTotalDistributedBySchedule() external view returns (uint256) { - return totalDistributedBySchedule; - } - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); - } - - /** - * @notice Sets the community reward percentage - * @param value The percentage of the total reward to be sent to the community funds as Fixidity fraction. - * @return True upon success. - */ - function setCommunityRewardFraction(uint256 value) public onlyOwner whenActivated returns (bool) { - uint256 timeSinceL2Start = block.timestamp - l2StartTime; - uint256 totalL2LinearSecondsAvailable = SECONDS_LINEAR - (l2StartTime - GENESIS_START_TIME); - FixidityLib.Fraction memory wrappedValue = FixidityLib.wrap(value); - require( - timeSinceL2Start < totalL2LinearSecondsAvailable, - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - require( - !wrappedValue.equals(communityRewardFraction) && wrappedValue.lt(FixidityLib.fixed1()), - "Value must be different from existing community reward fraction and less than 1." - ); - communityRewardFraction = wrappedValue; - require( - FixidityLib.fixed1().gte(communityRewardFraction.add(carbonOffsettingFraction)), - "Sum of partner fractions must be less than or equal to 1." - ); - emit CommunityRewardFractionSet(value); - return true; - } - - /** - * @notice Sets the carbon offsetting fund. - * @param partner The address of the carbon offsetting partner. - * @param value The percentage of the total reward to be sent to the carbon offsetting partner as Fixidity fraction. - * @return True upon success. - */ - function setCarbonOffsettingFund( - address partner, - uint256 value - ) public onlyOwner whenActivated returns (bool) { - require(partner != address(0), "Partner cannot be the zero address."); - uint256 timeSinceL2Start = block.timestamp - l2StartTime; - uint256 totalL2LinearSecondsAvailable = SECONDS_LINEAR - (l2StartTime - GENESIS_START_TIME); - require( - timeSinceL2Start < totalL2LinearSecondsAvailable, - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - FixidityLib.Fraction memory wrappedValue = FixidityLib.wrap(value); - require( - partner != carbonOffsettingPartner || !wrappedValue.equals(carbonOffsettingFraction), - "Partner and value must be different from existing carbon offsetting fund." - ); - require(wrappedValue.lt(FixidityLib.fixed1()), "Value must be less than 1."); - carbonOffsettingPartner = partner; - carbonOffsettingFraction = wrappedValue; - require( - FixidityLib.fixed1().gte(communityRewardFraction.add(carbonOffsettingFraction)), - "Sum of partner fractions must be less than or equal to 1." - ); - emit CarbonOffsettingFundSet(partner, value); - return true; - } - - /** - * @return The remaining CELO balance to distribute. - */ - function getRemainingBalanceToDistribute() public view returns (uint256) { - return address(this).balance; - } - - /** - * @return The currently distributable amount. - */ - function getDistributableAmount() public view returns (uint256) { - (uint256 targetCeloDistribution, , ) = getTargetCeloDistribution(); - ICeloToken celoToken = ICeloToken(address(getCeloToken())); - return targetCeloDistribution - celoToken.allocatedSupply(); - } - - /** - * @notice Returns the target CELO supply according to the target schedule. - * @return targetCeloDistribution The target total CELO supply according to the target schedule. - * @return communityTargetRewards The community reward that can be distributed according to the target schedule. - * @return carbonFundTargetRewards The carbon offsetting reward that can be distributed according to the target schedule. - */ - function getTargetCeloDistribution() - public - view - whenActivated - returns ( - uint256 targetCeloDistribution, - uint256 communityTargetRewards, - uint256 carbonFundTargetRewards - ) - { - require(block.timestamp > GENESIS_START_TIME, "GENESIS_START_TIME has not yet been reached."); - require(block.timestamp > l2StartTime, "l2StartTime has not yet been reached."); - - uint256 timeSinceL2Start = block.timestamp - l2StartTime; - uint256 totalL2LinearSecondsAvailable = SECONDS_LINEAR - (l2StartTime - GENESIS_START_TIME); - uint256 mintedOnL1 = totalAllocatedAtL2Start - GENESIS_CELO_SUPPLY; - - bool isLinearDistribution = timeSinceL2Start < totalL2LinearSecondsAvailable; - if (isLinearDistribution) { - ( - targetCeloDistribution, - communityTargetRewards, - carbonFundTargetRewards - ) = _calculateTargetReward(timeSinceL2Start, totalL2LinearSecondsAvailable, mintedOnL1); - - return (targetCeloDistribution, communityTargetRewards, carbonFundTargetRewards); - } else { - ( - targetCeloDistribution, - communityTargetRewards, - carbonFundTargetRewards - ) = _calculateTargetReward( - totalL2LinearSecondsAvailable - 1, - totalL2LinearSecondsAvailable, - mintedOnL1 - ); - - bool hasNotYetDistributedAllLinearRewards = totalDistributedBySchedule + - GENESIS_CELO_SUPPLY + - mintedOnL1 < - targetCeloDistribution; - - if (hasNotYetDistributedAllLinearRewards) { - return (targetCeloDistribution, communityTargetRewards, carbonFundTargetRewards); - } - revert("Block reward calculation for years 15-30 unimplemented"); - return (0, 0, 0); - } - } - - function _calculateTargetReward( - uint256 elapsedTime, - uint256 _totalL2LinearSecondsAvailable, - uint256 _mintedOnL1 - ) - internal - view - returns ( - uint256 targetCeloDistribution, - uint256 communityTargetRewards, - uint256 carbonFundTargetRewards - ) - { - FixidityLib.Fraction memory elapsedTimeFraction = FixidityLib.wrap(elapsedTime); - FixidityLib.Fraction memory totalL2LinearSecondsAvailableFraction = FixidityLib.wrap( - _totalL2LinearSecondsAvailable - ); - // Pay out half of all block rewards linearly. - ICeloToken celoToken = ICeloToken(address(getCeloToken())); - uint256 totalLinearRewards = (celoToken.totalSupply() - GENESIS_CELO_SUPPLY) / 2; //(200 million) includes validator rewards. - - FixidityLib.Fraction memory l2LinearRewards = FixidityLib.newFixed( - totalLinearRewards - _mintedOnL1 - ); - - FixidityLib.Fraction memory linearRewardsToCommunity = l2LinearRewards.multiply( - communityRewardFraction - ); - - FixidityLib.Fraction memory linearRewardsToCarbon = l2LinearRewards.multiply( - carbonOffsettingFraction - ); - - communityTargetRewards = ( - linearRewardsToCommunity.multiply(elapsedTimeFraction).divide( - totalL2LinearSecondsAvailableFraction - ) - ).fromFixed(); - - carbonFundTargetRewards = linearRewardsToCarbon - .multiply(elapsedTimeFraction) - .divide(totalL2LinearSecondsAvailableFraction) - .fromFixed(); - - targetCeloDistribution = - communityTargetRewards + - carbonFundTargetRewards + - GENESIS_CELO_SUPPLY + - _mintedOnL1; - } -} diff --git a/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasury.sol b/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasury.sol new file mode 100644 index 00000000000..38889d88c32 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasury.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts8/utils/math/Math.sol"; + +import "./UsingRegistry.sol"; +import "../common/IsL2Check.sol"; + +import "../../contracts/common/Initializable.sol"; +import "./interfaces/ICeloUnreleasedTreasuryInitializer.sol"; + +/** + * @title Contract for unreleased Celo tokens. + */ +contract CeloUnreleasedTreasury is UsingRegistry, ReentrancyGuard, Initializable, IsL2Check { + event Released(address indexed to, uint256 amount); + + modifier onlyEpochManager() { + require( + msg.sender == registry.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID), + "Only the EpochManager contract can call this function." + ); + _; + } + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice A constructor for initialising a new instance of a CeloUnreleasedTreasury contract. + * @param registryAddress The address of the registry core smart contract. + + */ + function initialize(address registryAddress) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + } + + /** + * @notice Releases the Celo to the specified address. + * @param to The address to release the amount to. + * @param amount The amount to release. + */ + function release(address to, uint256 amount) external onlyEpochManager { + require(address(this).balance >= amount, "Insufficient balance."); + require(getCeloToken().transfer(to, amount), "CELO transfer failed."); + emit Released(to, amount); + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } +} diff --git a/packages/protocol/contracts-0.8/common/EpochManager.sol b/packages/protocol/contracts-0.8/common/EpochManager.sol new file mode 100644 index 00000000000..516fa66c182 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/EpochManager.sol @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts8/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; + +import "./interfaces/IOracle.sol"; +import "../common/UsingRegistry.sol"; + +import "../../contracts/common/FixidityLib.sol"; +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/IEpochManager.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "./interfaces/IEpochManagerInitializer.sol"; + +contract EpochManager is + Initializable, + UsingRegistry, + IEpochManager, + ReentrancyGuard, + ICeloVersionedContract, + IEpochManagerInitializer +{ + using FixidityLib for FixidityLib.Fraction; + + struct Epoch { + uint256 firstBlock; + uint256 lastBlock; + uint256 startTimestamp; + uint256 rewardsBlock; + } + + enum EpochProcessStatus { + NotStarted, + Started + } + + struct EpochProcessState { + EpochProcessStatus status; + uint256 perValidatorReward; // The per validator epoch reward. + uint256 totalRewardsVoter; // The total rewards to voters. + uint256 totalRewardsCommunity; // The total community reward. + uint256 totalRewardsCarbonFund; // The total carbon offsetting partner reward. + } + + struct ProcessedGroup { + bool processed; + uint256 epochRewards; + } + bool public isSystemInitialized; + + // the length of an epoch in seconds + uint256 public epochDuration; + + uint256 public firstKnownEpoch; + uint256 private currentEpochNumber; + address public oracleAddress; + address[] public elected; + + mapping(address => ProcessedGroup) public processedGroups; + + EpochProcessState public epochProcessing; + mapping(uint256 => Epoch) private epochs; + mapping(address => uint256) public validatorPendingPayments; + + /** + * @notice Event emited when epochProcessing has begun. + * @param epochNumber The epoch number that is being processed. + */ + event EpochProcessingStarted(uint256 indexed epochNumber); + + /** + * @notice Event emited when epochProcessing has ended. + * @param epochNumber The epoch number that is finished being processed. + */ + event EpochProcessingEnded(uint256 indexed epochNumber); + + /** + * @notice Event emited when a new epoch duration is set. + * @param newEpochDuration The new epoch duration. + */ + event EpochDurationSet(uint256 indexed newEpochDuration); + + /** + * @notice Event emited when a new oracle address is set. + * @param newOracleAddress The new oracle address. + */ + event OracleAddressSet(address indexed newOracleAddress); + + /** + * @notice Emitted when an epoch payment is sent. + * @param validator Address of the validator. + * @param validatorPayment Amount of cUSD sent to the validator. + * @param group Address of the validator's group. + * @param groupPayment Amount of cUSD sent to the group. + */ + event ValidatorEpochPaymentDistributed( + address indexed validator, + uint256 validatorPayment, + address indexed group, + uint256 groupPayment, + address indexed beneficiary, + uint256 delegatedPayment + ); + + modifier onlyEpochManagerEnabler() { + require( + msg.sender == registry.getAddressForOrDie(EPOCH_MANAGER_ENABLER_REGISTRY_ID), + "msg.sender is not Enabler" + ); + _; + } + + modifier onlySystemAlreadyInitialized() { + require(systemAlreadyInitialized(), "Epoch system not initialized"); + _; + } + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param registryAddress The address of the registry core smart contract. + * @param newEpochDuration The duration of an epoch in seconds. + */ + function initialize(address registryAddress, uint256 newEpochDuration) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + setEpochDuration(newEpochDuration); + setOracleAddress(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); + } + + // DESIGNDESICION(XXX): we assume that the first epoch on the L2 starts as soon as the system is initialized + // to minimize amount of "limbo blocks" the network should stop relatively close to an epoch number (but with enough time) + // to have time to call the function EpochInitializer.migrateEpochAndValidators() + + /** + * @notice Initializes the EpochManager system, allowing it to start processing epoch + * and distributing the epoch rewards. + * @dev Can only be called by the EpochManagerEnabler contract. + */ + function initializeSystem( + uint256 firstEpochNumber, + uint256 firstEpochBlock, + address[] memory firstElected + ) external onlyEpochManagerEnabler { + require( + getCeloToken().balanceOf(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID)) > + 0, + "CeloUnreleasedTreasury not yet funded." + ); + require(!systemAlreadyInitialized(), "Epoch system already initialized"); + require(firstEpochNumber > 0, "First epoch number must be greater than 0"); + require(firstEpochBlock > 0, "First epoch block must be greater than 0"); + require( + firstEpochBlock <= block.number, + "First epoch block must be less or equal than current block" + ); + require(firstElected.length > 0, "First elected validators must be greater than 0"); + isSystemInitialized = true; + firstKnownEpoch = firstEpochNumber; + currentEpochNumber = firstEpochNumber; + + Epoch storage _currentEpoch = epochs[currentEpochNumber]; + _currentEpoch.firstBlock = firstEpochBlock; + _currentEpoch.startTimestamp = block.timestamp; + + elected = firstElected; + } + + /** + * @notice Starts processing an epoch and allocates funds to the beneficiaries. + * @dev Epoch rewards are frozen at the time of execution. + * @dev Can only be called once the system is initialized. + */ + function startNextEpochProcess() external nonReentrant onlySystemAlreadyInitialized { + require(isTimeForNextEpoch(), "Epoch is not ready to start"); + require(!isOnEpochProcess(), "Epoch process is already started"); + epochProcessing.status = EpochProcessStatus.Started; + + epochs[currentEpochNumber].rewardsBlock = block.number; + + // calculate rewards + getEpochRewards().updateTargetVotingYield(); + + ( + uint256 perValidatorReward, + uint256 totalRewardsVoter, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = getEpochRewards().calculateTargetEpochRewards(); + + epochProcessing.perValidatorReward = perValidatorReward; + epochProcessing.totalRewardsVoter = totalRewardsVoter; + epochProcessing.totalRewardsCommunity = totalRewardsCommunity; + epochProcessing.totalRewardsCarbonFund = totalRewardsCarbonFund; + + allocateValidatorsRewards(); + + emit EpochProcessingStarted(currentEpochNumber); + } + + /** + * @notice Finishes processing an epoch and releasing funds to the beneficiaries. + * @param groups List of validator groups to be processed. + * @param lessers List of validator groups that hold less votes that indexed group. + * @param greaters List of validator groups that hold more votes that indexed group. + */ + function finishNextEpochProcess( + address[] calldata groups, + address[] calldata lessers, + address[] calldata greaters + ) external nonReentrant { + require(isOnEpochProcess(), "Epoch process is not started"); + // finalize epoch + // last block should be the block before and timestamp from previous block + epochs[currentEpochNumber].lastBlock = block.number - 1; + // start new epoch + currentEpochNumber++; + epochs[currentEpochNumber].firstBlock = block.number; + epochs[currentEpochNumber].startTimestamp = block.timestamp; + + EpochProcessState storage _epochProcessing = epochProcessing; + + uint256 toProcessGroups = 0; + IValidators validators = getValidators(); + IElection election = getElection(); + IScoreReader scoreReader = getScoreReader(); + for (uint i = 0; i < elected.length; i++) { + address group = validators.getValidatorsGroup(elected[i]); + if (!processedGroups[group].processed) { + toProcessGroups++; + uint256 groupScore = scoreReader.getGroupScore(group); + // We need to precompute epoch rewards for each group since computation depends on total active votes for all groups. + uint256 epochRewards = election.getGroupEpochRewardsBasedOnScore( + group, + _epochProcessing.totalRewardsVoter, + groupScore + ); + processedGroups[group] = ProcessedGroup(true, epochRewards); + } + } + + require(toProcessGroups == groups.length, "number of groups does not match"); + + for (uint i = 0; i < groups.length; i++) { + ProcessedGroup storage processedGroup = processedGroups[groups[i]]; + // checks that group is actually from elected group + require(processedGroup.processed, "group not from current elected set"); + election.distributeEpochRewards( + groups[i], + processedGroup.epochRewards, + lessers[i], + greaters[i] + ); + + delete processedGroups[groups[i]]; + } + getCeloUnreleasedTreasury().release( + registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID), + epochProcessing.totalRewardsCommunity + ); + getCeloUnreleasedTreasury().release( + getEpochRewards().carbonOffsettingPartner(), + epochProcessing.totalRewardsCarbonFund + ); + // run elections + elected = election.electValidatorAccounts(); + _epochProcessing.status = EpochProcessStatus.NotStarted; + } + + /** + * @notice Sends the allocated epoch payment to a validator, their group, and + * delegation beneficiary. + * @param validator Account of the validator. + * @dev Can only be called once the system is initialized. + */ + function sendValidatorPayment(address validator) external onlySystemAlreadyInitialized { + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorPendingPayments[validator] + ); + validatorPendingPayments[validator] = 0; + + IValidators validators = getValidators(); + address group = validators.getValidatorsGroup(validator); + (, uint256 commissionUnwrapped, , , , , ) = validators.getValidatorGroup(group); + + uint256 groupPayment = totalPayment.multiply(FixidityLib.wrap(commissionUnwrapped)).fromFixed(); + FixidityLib.Fraction memory remainingPayment = FixidityLib.newFixed( + totalPayment.fromFixed() - groupPayment + ); + (address beneficiary, uint256 delegatedFraction) = getAccounts().getPaymentDelegation( + validator + ); + uint256 delegatedPayment = remainingPayment + .multiply(FixidityLib.wrap(delegatedFraction)) + .fromFixed(); + uint256 validatorPayment = remainingPayment.fromFixed() - delegatedPayment; + + IERC20 stableToken = IERC20(getStableToken()); + + if (validatorPayment > 0) { + require(stableToken.transfer(validator, validatorPayment), "transfer failed to validator"); + } + + if (groupPayment > 0) { + require(stableToken.transfer(group, groupPayment), "transfer failed to validator group"); + } + + if (delegatedPayment > 0) { + require(stableToken.transfer(beneficiary, delegatedPayment), "transfer failed to delegatee"); + } + + emit ValidatorEpochPaymentDistributed( + validator, + validatorPayment, + group, + groupPayment, + beneficiary, + delegatedPayment + ); + } + + /** + * @return The current epoch info. + */ + function getCurrentEpoch() + external + view + onlySystemAlreadyInitialized + returns (uint256, uint256, uint256, uint256) + { + Epoch storage _epoch = epochs[currentEpochNumber]; + return (_epoch.firstBlock, _epoch.lastBlock, _epoch.startTimestamp, _epoch.rewardsBlock); + } + + /** + * @return The current epoch number. + * @dev Can only be called once the system is initialized. + */ + function getCurrentEpochNumber() external view onlySystemAlreadyInitialized returns (uint256) { + return currentEpochNumber; + } + + /** + * @return The latest epoch processing state. + */ + function getEpochProcessingState() + external + view + returns (uint256, uint256, uint256, uint256, uint256) + { + EpochProcessState storage _epochProcessing = epochProcessing; + return ( + uint256(_epochProcessing.status), + _epochProcessing.perValidatorReward, + _epochProcessing.totalRewardsVoter, + _epochProcessing.totalRewardsCommunity, + _epochProcessing.totalRewardsCarbonFund + ); + } + + /** + * @notice Used to block select functions in blockable contracts. + * @return Whether or not the blockable functions are blocked. + */ + function isBlocked() external view returns (bool) { + return isOnEpochProcess(); + } + + /** + * @return The list of elected validators. + */ + function getElected() external view returns (address[] memory) { + return elected; + } + + /** + * @param epoch The epoch number of interest. + * @return The First block of the specified epoch. + */ + function getFirstBlockAtEpoch(uint256 epoch) external view returns (uint256) { + require(epoch >= firstKnownEpoch, "Epoch not known"); + require(epoch <= currentEpochNumber, "Epoch not created yet"); + return epochs[epoch].firstBlock; + } + + /** + * @param epoch The epoch number of interest. + * @return The last block of the specified epoch. + */ + function getLastBlockAtEpoch(uint256 epoch) external view returns (uint256) { + require(epoch >= firstKnownEpoch, "Epoch not known"); + require(epoch < currentEpochNumber, "Epoch not finished yet"); + return epochs[epoch].lastBlock; + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + + /** + * @notice Sets the time duration of an epoch. + * @param newEpochDuration The duration of an epoch in seconds. + * @dev Can only be set by owner. + */ + function setEpochDuration(uint256 newEpochDuration) public onlyOwner { + require(newEpochDuration > 0, "New epoch duration must be greater than zero."); + require(!isOnEpochProcess(), "Cannot change epoch duration during processing."); + epochDuration = newEpochDuration; + emit EpochDurationSet(newEpochDuration); + } + + /** + * @notice Sets the address of the Oracle used by this contract. + * @param newOracleAddress The address of the new oracle. + * @dev Can only be set by owner. + */ + function setOracleAddress(address newOracleAddress) public onlyOwner { + require(newOracleAddress != address(0), "Cannot set address zero as the Oracle."); + require(newOracleAddress != oracleAddress, "Oracle address cannot be the same."); + require(!isOnEpochProcess(), "Cannot change oracle address during epoch processing."); + oracleAddress = newOracleAddress; + emit OracleAddressSet(newOracleAddress); + } + + /** + * @return Whether or not the next epoch can be processed. + */ + function isTimeForNextEpoch() public view returns (bool) { + return block.timestamp >= epochs[currentEpochNumber].startTimestamp + epochDuration; + } + + /** + * @return Whether or not the current epoch is being processed. + */ + function isOnEpochProcess() public view returns (bool) { + return epochProcessing.status == EpochProcessStatus.Started; + } + + /** + * @return Whether or not the EpochManager contract has been activated to start processing epochs. + */ + function systemAlreadyInitialized() public view returns (bool) { + return initialized && isSystemInitialized; + } + + /** + * @notice Allocates rewards to elected validator accounts. + */ + function allocateValidatorsRewards() internal { + uint256 totalRewards = 0; + IScoreReader scoreReader = getScoreReader(); + IValidators validators = getValidators(); + + EpochProcessState storage _epochProcessing = epochProcessing; + + for (uint i = 0; i < elected.length; i++) { + uint256 validatorScore = scoreReader.getValidatorScore(elected[i]); + uint256 validatorReward = validators.computeEpochReward( + elected[i], + validatorScore, + _epochProcessing.perValidatorReward + ); + validatorPendingPayments[elected[i]] += validatorReward; + totalRewards += validatorReward; + } + if (totalRewards == 0) { + return; + } + + // Mint all cUSD required for payment and the corresponding CELO + validators.mintStableToEpochManager(totalRewards); + + (uint256 numerator, uint256 denominator) = IOracle(oracleAddress).getExchangeRate( + address(getStableToken()) + ); + + uint256 CELOequivalent = (numerator * totalRewards) / denominator; + getCeloUnreleasedTreasury().release( + registry.getAddressForOrDie(RESERVE_REGISTRY_ID), + CELOequivalent + ); + } +} diff --git a/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol new file mode 100644 index 00000000000..61506ec7874 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; + +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/governance/interfaces/IEpochRewards.sol"; +import "../../contracts/common/interfaces/IEpochManagerEnabler.sol"; +import "./interfaces/IEpochManagerEnablerInitializer.sol"; + +contract EpochManagerEnabler is + Initializable, + UsingPrecompiles, + UsingRegistry, + IEpochManagerEnabler, + IEpochManagerEnablerInitializer +{ + uint256 public lastKnownEpochNumber; + uint256 public lastKnownFirstBlockOfEpoch; + address[] public lastKnownElectedAccounts; + + event LastKnownEpochNumberSet(uint256 lastKnownEpochNumber); + event LastKnownFirstBlockOfEpochSet(uint256 lastKnownFirstBlockOfEpoch); + event LastKnownElectedAccountsSet(); + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param registryAddress The address of the registry core smart contract. + */ + function initialize(address registryAddress) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + } + + /** + * @notice initializes the epochManager contract during L2 transition. + */ + function initEpochManager() external onlyL2 { + require(lastKnownEpochNumber != 0, "lastKnownEpochNumber not set."); + require(lastKnownFirstBlockOfEpoch != 0, "lastKnownFirstBlockOfEpoch not set."); + require(lastKnownElectedAccounts.length > 0, "lastKnownElectedAccounts not set."); + getEpochManager().initializeSystem( + lastKnownEpochNumber, + lastKnownFirstBlockOfEpoch, + lastKnownElectedAccounts + ); + } + + /** + * @notice Stores the last known epochNumber and the related elected validator accounts. + */ + function captureEpochAndValidators() external onlyL1 { + lastKnownEpochNumber = getEpochNumber(); + emit LastKnownEpochNumberSet(lastKnownEpochNumber); + + uint256 numberElectedValidators = numberValidatorsInCurrentSet(); + lastKnownElectedAccounts = new address[](numberElectedValidators); + _setFirstBlockOfEpoch(); + + for (uint256 i = 0; i < numberElectedValidators; i++) { + // TODO: document how much gas this takes for 110 signers + address validatorAccountAddress = getAccounts().validatorSignerToAccount( + validatorSignerAddressFromCurrentSet(i) + ); + lastKnownElectedAccounts[i] = validatorAccountAddress; + } + emit LastKnownElectedAccountsSet(); + } + + /** + * @return a list of know elected validator accounts. + */ + function getlastKnownElectedAccounts() external view returns (address[] memory) { + return lastKnownElectedAccounts; + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + + function _setFirstBlockOfEpoch() internal onlyL1 { + uint256 blocksSinceEpochBlock = block.number % getEpochSize(); + uint256 epochBlock = block.number - blocksSinceEpochBlock; + lastKnownFirstBlockOfEpoch = epochBlock; + emit LastKnownFirstBlockOfEpochSet(lastKnownFirstBlockOfEpoch); + } +} diff --git a/packages/protocol/contracts-0.8/common/ScoreManager.sol b/packages/protocol/contracts-0.8/common/ScoreManager.sol new file mode 100644 index 00000000000..db052df6ec5 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/ScoreManager.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; + +contract ScoreManager is Initializable, Ownable { + struct Score { + uint256 score; + bool exists; + } + + event GroupScoreSet(address indexed group, uint256 score); + event ValidatorScoreSet(address indexed validator, uint256 score); + + uint256 private constant FIXED1_UINT = 1e24; + + mapping(address => Score) public groupScores; + mapping(address => Score) public validatorScores; + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + */ + function initialize() external initializer { + _transferOwnership(msg.sender); + } + + function setGroupScore(address group, uint256 score) external onlyOwner { + require(score <= FIXED1_UINT, "Score must be less than or equal to 1e24."); + Score storage groupScore = groupScores[group]; + if (!groupScore.exists) { + groupScore.exists = true; + } + groupScore.score = score; + + emit GroupScoreSet(group, score); + } + + function setValidatorScore(address validator, uint256 score) external onlyOwner { + require(score <= FIXED1_UINT, "Score must be less than or equal to 1e24."); + Score storage validatorScore = validatorScores[validator]; + if (!validatorScore.exists) { + validatorScore.exists = true; + } + validatorScore.score = score; + + emit ValidatorScoreSet(validator, score); + } + + function getGroupScore(address group) external view returns (uint256) { + Score storage groupScore = groupScores[group]; + if (!groupScore.exists) { + return FIXED1_UINT; + } + return groupScore.score; + } + + function getValidatorScore(address validator) external view returns (uint256) { + Score storage validatorScore = validatorScores[validator]; + if (!validatorScore.exists) { + return FIXED1_UINT; + } + return validatorScore.score; + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } +} diff --git a/packages/protocol/contracts-0.8/common/UsingPrecompiles.sol b/packages/protocol/contracts-0.8/common/UsingPrecompiles.sol new file mode 100644 index 00000000000..261c7f59d53 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/UsingPrecompiles.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.8.20; + +// Note: This is not an exact copy of UsingPrecompiles in the contract's folder, but in solidity 0.8 +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../common/IsL2Check.sol"; + +contract UsingPrecompiles is IsL2Check { + using SafeMath for uint256; + + address constant TRANSFER = address(0xff - 2); + address constant FRACTION_MUL = address(0xff - 3); + address constant PROOF_OF_POSSESSION = address(0xff - 4); + address constant GET_VALIDATOR = address(0xff - 5); + address constant NUMBER_VALIDATORS = address(0xff - 6); + address constant EPOCH_SIZE = address(0xff - 7); + address constant BLOCK_NUMBER_FROM_HEADER = address(0xff - 8); + address constant HASH_HEADER = address(0xff - 9); + address constant GET_PARENT_SEAL_BITMAP = address(0xff - 10); + address constant GET_VERIFIED_SEAL_BITMAP = address(0xff - 11); + uint256 constant DAY = 86400; + + /** + * @notice calculate a * b^x for fractions a, b to `decimals` precision + * @param aNumerator Numerator of first fraction + * @param aDenominator Denominator of first fraction + * @param bNumerator Numerator of exponentiated fraction + * @param bDenominator Denominator of exponentiated fraction + * @param exponent exponent to raise b to + * @param _decimals precision + * @return Numerator of the computed quantity (not reduced). + * @return Denominator of the computed quantity (not reduced). + */ + function fractionMulExp( + uint256 aNumerator, + uint256 aDenominator, + uint256 bNumerator, + uint256 bDenominator, + uint256 exponent, + uint256 _decimals + ) public view returns (uint256, uint256) { + require(aDenominator != 0 && bDenominator != 0, "a denominator is zero"); + uint256 returnNumerator; + uint256 returnDenominator; + bool success; + bytes memory out; + (success, out) = FRACTION_MUL.staticcall( + abi.encodePacked(aNumerator, aDenominator, bNumerator, bDenominator, exponent, _decimals) + ); + require(success, "error calling fractionMulExp precompile"); + returnNumerator = getUint256FromBytes(out, 0); + returnDenominator = getUint256FromBytes(out, 32); + return (returnNumerator, returnDenominator); + } + + /** + * @notice Returns the current epoch size in blocks. + * @return The current epoch size in blocks. + */ + function getEpochSize() public view returns (uint256) { + allowOnlyL1(); + bytes memory out; + bool success; + (success, out) = EPOCH_SIZE.staticcall(abi.encodePacked(true)); + require(success, "error calling getEpochSize precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Returns the epoch number at a block. + * @param blockNumber Block number where epoch number is calculated. + * @return Epoch number. + */ + function getEpochNumberOfBlock(uint256 blockNumber) public view returns (uint256) { + return epochNumberOfBlock(blockNumber, getEpochSize()); + } + + /** + * @notice Returns the epoch number at a block. + * @return Current epoch number. + */ + function getEpochNumber() public view returns (uint256) { + return getEpochNumberOfBlock(block.number); + } + + /** + * @notice Gets a validator address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator at the requested index. + */ + function validatorSignerAddressFromCurrentSet( + uint256 index + ) public view virtual returns (address) { + bytes memory out; + bool success; + (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, uint256(block.number))); + require(success, "error calling validatorSignerAddressFromCurrentSet precompile"); + return address(uint160(getUint256FromBytes(out, 0))); + } + + /** + * @notice Gets a validator address from the validator set at the given block number. + * @param index Index of requested validator in the validator set. + * @param blockNumber Block number to retrieve the validator set from. + * @return Address of validator at the requested index. + */ + function validatorSignerAddressFromSet( + uint256 index, + uint256 blockNumber + ) public view returns (address) { + bytes memory out; + bool success; + (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, blockNumber)); + require(success, "error calling validatorSignerAddressFromSet precompile"); + return address(uint160(getUint256FromBytes(out, 0))); + } + + /** + * @notice Gets the size of the current elected validator set. + * @return Size of the current elected validator set. + */ + function numberValidatorsInCurrentSet() public view virtual returns (uint256) { + bytes memory out; + bool success; + (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(uint256(block.number))); + require(success, "error calling numberValidatorsInCurrentSet precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Gets the size of the validator set that must sign the given block number. + * @param blockNumber Block number to retrieve the validator set from. + * @return Size of the validator set. + */ + function numberValidatorsInSet(uint256 blockNumber) public view virtual returns (uint256) { + bytes memory out; + bool success; + (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(blockNumber)); + require(success, "error calling numberValidatorsInSet precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Checks a BLS proof of possession. + * @param sender The address signed by the BLS key to generate the proof of possession. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. + * @return True upon success. + */ + function checkProofOfPossession( + address sender, + bytes memory blsKey, + bytes memory blsPop + ) public view returns (bool) { + bool success; + (success, ) = PROOF_OF_POSSESSION.staticcall(abi.encodePacked(sender, blsKey, blsPop)); + return success; + } + + /** + * @notice Parses block number out of header. + * @param header RLP encoded header + * @return Block number. + */ + function getBlockNumberFromHeader(bytes memory header) public view returns (uint256) { + bytes memory out; + bool success; + (success, out) = BLOCK_NUMBER_FROM_HEADER.staticcall(abi.encodePacked(header)); + require(success, "error calling getBlockNumberFromHeader precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Computes hash of header. + * @param header RLP encoded header + * @return Header hash. + */ + function hashHeader(bytes memory header) public view returns (bytes32) { + bytes memory out; + bool success; + (success, out) = HASH_HEADER.staticcall(abi.encodePacked(header)); + require(success, "error calling hashHeader precompile"); + return getBytes32FromBytes(out, 0); + } + + /** + * @notice Gets the parent seal bitmap from the header at the given block number. + * @param blockNumber Block number to retrieve. Must be within 4 epochs of the current number. + * @return Bitmap parent seal with set bits at indices corresponding to signing validators. + */ + function getParentSealBitmap(uint256 blockNumber) public view returns (bytes32) { + bytes memory out; + bool success; + (success, out) = GET_PARENT_SEAL_BITMAP.staticcall(abi.encodePacked(blockNumber)); + require(success, "error calling getParentSealBitmap precompile"); + return getBytes32FromBytes(out, 0); + } + + /** + * @notice Verifies the BLS signature on the header and returns the seal bitmap. + * The validator set used for verification is retrieved based on the parent hash field of the + * header. If the parent hash is not in the blockchain, verification fails. + * @param header RLP encoded header + * @return Bitmap parent seal with set bits at indices correspoinding to signing validators. + */ + function getVerifiedSealBitmapFromHeader(bytes memory header) public view returns (bytes32) { + bytes memory out; + bool success; + (success, out) = GET_VERIFIED_SEAL_BITMAP.staticcall(abi.encodePacked(header)); + require(success, "error calling getVerifiedSealBitmapFromHeader precompile"); + return getBytes32FromBytes(out, 0); + } + + /** + * @notice Returns the minimum number of required signers for a given block number. + * @dev Computed in celo-blockchain as int(math.Ceil(float64(2*valSet.Size()) / 3)) + */ + function minQuorumSize(uint256 blockNumber) public view returns (uint256) { + return numberValidatorsInSet(blockNumber).mul(2).add(2).div(3); + } + + /** + * @notice Computes byzantine quorum from current validator set size + * @return Byzantine quorum of validators. + */ + function minQuorumSizeInCurrentSet() public view returns (uint256) { + return minQuorumSize(block.number); + } + + /** + * @notice Returns the epoch number at a block. + * @param blockNumber Block number where epoch number is calculated. + * @param epochSize The epoch size in blocks. + * @return Epoch number. + */ + function epochNumberOfBlock( + uint256 blockNumber, + uint256 epochSize + ) internal pure returns (uint256) { + // Follows GetEpochNumber from celo-blockchain/blob/master/consensus/istanbul/utils.go + uint256 epochNumber = blockNumber / epochSize; + if (blockNumber % epochSize == 0) { + return epochNumber; + } else { + return epochNumber.add(1); + } + } + + /** + * @notice Converts bytes to uint256. + * @param bs byte[] data + * @param start offset into byte data to convert + * @return uint256 data + */ + function getUint256FromBytes(bytes memory bs, uint256 start) internal pure returns (uint256) { + return uint256(getBytes32FromBytes(bs, start)); + } + + /** + * @notice Converts bytes to bytes32. + * @param bs byte[] data + * @param start offset into byte data to convert + * @return bytes32 data + */ + function getBytes32FromBytes(bytes memory bs, uint256 start) internal pure returns (bytes32) { + require(bs.length >= start.add(32), "slicing out of range"); + bytes32 x; + assembly { + x := mload(add(bs, add(start, 32))) + } + return x; + } +} diff --git a/packages/protocol/contracts-0.8/common/UsingRegistry.sol b/packages/protocol/contracts-0.8/common/UsingRegistry.sol index 0767afe11f1..dbe23037a40 100644 --- a/packages/protocol/contracts-0.8/common/UsingRegistry.sol +++ b/packages/protocol/contracts-0.8/common/UsingRegistry.sol @@ -9,17 +9,21 @@ import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; import "../../contracts/common/interfaces/IRegistry.sol"; import "../../contracts/common/interfaces/IAccounts.sol"; +import "../../contracts/common/interfaces/IEpochManager.sol"; import "../../contracts/common/interfaces/IFreezer.sol"; -import "../../contracts/common/interfaces/ICeloDistributionSchedule.sol"; +import "../../contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import "../../contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; +import "../../contracts/common/interfaces/IFeeHandlerSeller.sol"; +import "../../contracts/common/interfaces/IEpochManager.sol"; import "../../contracts/governance/interfaces/IGovernance.sol"; import "../../contracts/governance/interfaces/ILockedGold.sol"; import "../../contracts/governance/interfaces/ILockedCelo.sol"; import "../../contracts/governance/interfaces/IValidators.sol"; -import "../../contracts/stability/interfaces/ISortedOracles.sol"; -import "../../contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; import "../../contracts/governance/interfaces/IElection.sol"; -import "../../contracts/common/interfaces/IFeeHandlerSeller.sol"; import "../../contracts/governance/interfaces/IEpochRewards.sol"; +import "../../contracts/stability/interfaces/ISortedOracles.sol"; + +import "./interfaces/IScoreReader.sol"; contract UsingRegistry is Ownable { // solhint-disable state-visibility @@ -48,8 +52,12 @@ contract UsingRegistry is Ownable { keccak256(abi.encodePacked("MentoFeeHandlerSeller")); bytes32 constant CELO_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("CeloToken")); bytes32 constant LOCKED_CELO_REGISTRY_ID = keccak256(abi.encodePacked("LockedCelo")); - bytes32 constant CELO_DISTRIBUTION_SCHEDULE_ID = - keccak256(abi.encodePacked("CeloDistributionSchedule")); + bytes32 constant CELO_UNRELEASED_TREASURY_REGISTRY_ID = + keccak256(abi.encodePacked("CeloUnreleasedTreasury")); + bytes32 constant EPOCH_MANAGER_ENABLER_REGISTRY_ID = + keccak256(abi.encodePacked("EpochManagerEnabler")); + bytes32 constant EPOCH_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("EpochManager")); + bytes32 constant SCORE_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("ScoreManager")); // solhint-enable state-visibility IRegistry public registry; @@ -133,7 +141,16 @@ contract UsingRegistry is Ownable { return IGovernance(registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID)); } - function getCeloDistributionSchedule() internal view returns (ICeloDistributionSchedule) { - return ICeloDistributionSchedule(registry.getAddressForOrDie(CELO_DISTRIBUTION_SCHEDULE_ID)); + function getCeloUnreleasedTreasury() internal view returns (ICeloUnreleasedTreasury) { + return + ICeloUnreleasedTreasury(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID)); + } + + function getEpochManager() internal view returns (IEpochManager) { + return IEpochManager(registry.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID)); + } + + function getScoreReader() internal view returns (IScoreReader) { + return IScoreReader(registry.getAddressForOrDie(SCORE_MANAGER_REGISTRY_ID)); } } diff --git a/packages/protocol/contracts-0.8/common/interfaces/ICeloToken.sol b/packages/protocol/contracts-0.8/common/interfaces/ICeloToken.sol deleted file mode 100644 index e774da6f884..00000000000 --- a/packages/protocol/contracts-0.8/common/interfaces/ICeloToken.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.13 <0.9.0; - -import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. Does not include - * the optional functions; to access them see {ERC20Detailed}. - */ -interface ICeloToken is IERC20 { - /** - * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param registryAddress Address of the Registry contract. - */ - function initialize(address registryAddress) external; - - /** - * @notice Updates the address pointing to a Registry contract. - * @param registryAddress The address of a registry contract for routing to other contracts. - */ - function setRegistry(address registryAddress) external; - - /** - * @dev Mints a new token. - * @param to The address that will own the minted token. - * @param value The amount of token to be minted. - */ - function mint(address to, uint256 value) external returns (bool); - - /** - * @notice Returns amount of CELO that has been allocated. - */ - function allocatedSupply() external view returns (uint256); -} diff --git a/packages/protocol/contracts-0.8/common/interfaces/ICeloDistributionScheduleInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol similarity index 73% rename from packages/protocol/contracts-0.8/common/interfaces/ICeloDistributionScheduleInitializer.sol rename to packages/protocol/contracts-0.8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol index 7844337b5e3..d87e8514ca1 100644 --- a/packages/protocol/contracts-0.8/common/interfaces/ICeloDistributionScheduleInitializer.sol +++ b/packages/protocol/contracts-0.8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; -interface ICeloDistributionScheduleInitializer { +interface ICeloUnreleasedTreasuryInitializer { function initialize(address registryAddress) external; } diff --git a/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerEnablerInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerEnablerInitializer.sol new file mode 100644 index 00000000000..3643d5ec711 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerEnablerInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerEnablerInitializer { + function initialize(address registryAddress) external; +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerInitializer.sol new file mode 100644 index 00000000000..ee418889182 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerInitializer { + function initialize(address registryAddress, uint256 newEpochDuration) external; +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol b/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol index b13ef6e0ec4..28850334637 100644 --- a/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol +++ b/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol @@ -7,6 +7,15 @@ interface IFeeCurrencyDirectory { uint256 intrinsicGas; } + /** + * @notice Sets the currency configuration for a token. + * @dev This action can only be performed by the contract owner. + * @param token The token address. + * @param oracle The oracle address for price fetching. + * @param intrinsicGas The intrinsic gas value for transactions. + */ + function setCurrencyConfig(address token, address oracle, uint256 intrinsicGas) external; + /** * @notice Returns the list of all currency addresses. * @return An array of addresses. @@ -28,13 +37,4 @@ interface IFeeCurrencyDirectory { function getExchangeRate( address token ) external view returns (uint256 numerator, uint256 denominator); - - /** - * @notice Sets the currency configuration for a token. - * @dev This action can only be performed by the contract owner. - * @param token The token address. - * @param oracle The oracle address for price fetching. - * @param intrinsicGas The intrinsic gas value for transactions. - */ - function setCurrencyConfig(address token, address oracle, uint256 intrinsicGas) external; } diff --git a/packages/protocol/contracts-0.8/common/interfaces/IPrecompiles.sol b/packages/protocol/contracts-0.8/common/interfaces/IPrecompiles.sol new file mode 100644 index 00000000000..cf36e67ee4b --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IPrecompiles.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.13 <0.9.0; + +interface IPrecompiles { + function getEpochSize() external view returns (uint256); + function getEpochNumber() external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IScoreManager.sol b/packages/protocol/contracts-0.8/common/interfaces/IScoreManager.sol new file mode 100644 index 00000000000..0020fd65df0 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IScoreManager.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IScoreManager { + function setGroupScore(address group, uint256 score) external; + function setValidatorScore(address validator, uint256 score) external; + function getValidatorScore(address validator) external view returns (uint256); + function getGroupScore(address validator) external view returns (uint256); + function owner() external view returns (address); +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IScoreManagerInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IScoreManagerInitializer.sol new file mode 100644 index 00000000000..f6229cf72b5 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IScoreManagerInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IScoreManagerInitializer { + function initialize() external; +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IScoreReader.sol b/packages/protocol/contracts-0.8/common/interfaces/IScoreReader.sol new file mode 100644 index 00000000000..f45d57af1eb --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IScoreReader.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +interface IScoreReader { + function getValidatorScore(address validator) external view returns (uint256); + function getGroupScore(address validator) external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IStableToken.sol b/packages/protocol/contracts-0.8/common/interfaces/IStableToken.sol new file mode 100644 index 00000000000..1fa10340fec --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IStableToken.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +/** + * @title This interface describes the functions specific to Celo Stable Tokens, and in the + * absence of interface inheritance is intended as a companion to IERC20.sol and ICeloToken.sol. + */ +interface IStableToken { + function transfer(address, uint256) external returns (bool); + + function mint(address, uint256) external returns (bool); + + function burn(uint256) external returns (bool); + + function setInflationParameters(uint256, uint256) external; + + function valueToUnits(uint256) external view returns (uint256); + + function unitsToValue(uint256) external view returns (uint256); + + function getInflationParameters() external view returns (uint256, uint256, uint256, uint256); + + // NOTE: duplicated with IERC20.sol, remove once interface inheritance is supported. + function balanceOf(address) external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/common/linkedlists/AddressLinkedList.sol b/packages/protocol/contracts-0.8/common/linkedlists/AddressLinkedList.sol new file mode 100644 index 00000000000..10d7988372e --- /dev/null +++ b/packages/protocol/contracts-0.8/common/linkedlists/AddressLinkedList.sol @@ -0,0 +1,106 @@ +pragma solidity >=0.8.0 <0.8.20; + +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; + +import "./LinkedList.sol"; + +/** + * @title Maintains a doubly linked list keyed by address. + * @dev Following the `next` pointers will lead you to the head, rather than the tail. + */ +library AddressLinkedList { + using LinkedList for LinkedList.List; + using SafeMath for uint256; + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param previousKey The key of the element that comes before the element to insert. + * @param nextKey The key of the element that comes after the element to insert. + */ + function insert( + LinkedList.List storage list, + address key, + address previousKey, + address nextKey + ) public { + list.insert(toBytes(key), toBytes(previousKey), toBytes(nextKey)); + } + + /** + * @notice Inserts an element at the end of the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + */ + function push(LinkedList.List storage list, address key) public { + list.insert(toBytes(key), bytes32(0), list.tail); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(LinkedList.List storage list, address key) public { + list.remove(toBytes(key)); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param previousKey The key of the element that comes before the updated element. + * @param nextKey The key of the element that comes after the updated element. + */ + function update( + LinkedList.List storage list, + address key, + address previousKey, + address nextKey + ) public { + list.update(toBytes(key), toBytes(previousKey), toBytes(nextKey)); + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(LinkedList.List storage list, address key) public view returns (bool) { + return list.elements[toBytes(key)].exists; + } + + /** + * @notice Returns the N greatest elements of the list. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to return. + * @return The keys of the greatest elements. + * @dev Reverts if n is greater than the number of elements in the list. + */ + function headN(LinkedList.List storage list, uint256 n) public view returns (address[] memory) { + bytes32[] memory byteKeys = list.headN(n); + address[] memory keys = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + keys[i] = toAddress(byteKeys[i]); + } + return keys; + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return All element keys from head to tail. + */ + function getKeys(LinkedList.List storage list) public view returns (address[] memory) { + return headN(list, list.numElements); + } + + function toBytes(address a) public pure returns (bytes32) { + return bytes32(uint256(uint160(a)) << 96); + } + + function toAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b) >> 96)); + } +} diff --git a/packages/protocol/contracts-0.8/common/mocks/EpochManager_WithMocks.sol b/packages/protocol/contracts-0.8/common/mocks/EpochManager_WithMocks.sol new file mode 100644 index 00000000000..9ddda4aba92 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/mocks/EpochManager_WithMocks.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../EpochManager.sol"; + +contract EpochManager_WithMocks is EpochManager(true) { + function _setPaymentAllocation(address validator, uint256 amount) external { + validatorPendingPayments[validator] = amount; + } +} diff --git a/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol b/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol new file mode 100644 index 00000000000..51ead07c279 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../../../contracts/common/FixidityLib.sol"; + +contract MockAccounts { + using FixidityLib for FixidityLib.Fraction; + + struct PaymentDelegation { + // Address that should receive a fraction of validator payments. + address beneficiary; + // Fraction of payment to delegate to `beneficiary`. + FixidityLib.Fraction fraction; + } + + mapping(address => PaymentDelegation) delegations; + mapping(address => address) accountToSigner; + + function setValidatorSigner(address account, address signer) external { + accountToSigner[account] = signer; + } + + function getValidatorSigner(address account) external returns (address) { + return accountToSigner[account]; + } + + function getPaymentDelegation(address account) external view returns (address, uint256) { + PaymentDelegation storage delegation = delegations[account]; + return (delegation.beneficiary, delegation.fraction.unwrap()); + } + + function setPaymentDelegationFor( + address validator, + address beneficiary, + uint256 fraction + ) public { + delegations[validator] = PaymentDelegation(beneficiary, FixidityLib.wrap(fraction)); + } + + function deletePaymentDelegationFor(address validator) public { + delete delegations[validator]; + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol b/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol new file mode 100644 index 00000000000..b412f134a22 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol @@ -0,0 +1,44 @@ +pragma solidity >=0.8.0 <0.9.0; +// solhint-disable no-unused-vars + +/** + * @title A mock StableToken for testing. This contract can be deprecated once GoldToken gets migrated to 0.8 + */ +contract MockCeloToken08 { + uint256 public totalSupply_; + uint8 public constant decimals = 18; + mapping(address => uint256) balances; + + function setTotalSupply(uint256 value) external { + totalSupply_ = value; + } + + function transfer(address to, uint256 amount) external returns (bool) { + return _transfer(msg.sender, to, amount); + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + return _transfer(from, to, amount); + } + + function setBalanceOf(address a, uint256 value) external { + balances[a] = value; + } + + function balanceOf(address a) public view returns (uint256) { + return balances[a]; + } + + function totalSupply() public view returns (uint256) { + return totalSupply_; + } + + function _transfer(address from, address to, uint256 amount) internal returns (bool) { + if (balances[from] < amount) { + return false; + } + balances[from] -= amount; + balances[to] += amount; + return true; + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasury.sol b/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasury.sol new file mode 100644 index 00000000000..da5b78af65d --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasury.sol @@ -0,0 +1,15 @@ +pragma solidity >=0.8.0 <0.9.0; +// solhint-disable no-unused-vars + +import "../../../contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import "../UsingRegistry.sol"; + +/** + * @title A mock CeloUnreleasedTreasury for testing. + */ +contract MockCeloUnreleasedTreasury is ICeloUnreleasedTreasury, UsingRegistry { + function release(address to, uint256 amount) external { + require(address(this).balance >= amount, "Insufficient balance."); + require(getCeloToken().transfer(to, amount), "CELO transfer failed."); + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockEpochManager.sol b/packages/protocol/contracts-0.8/common/test/MockEpochManager.sol new file mode 100644 index 00000000000..d110f6a2271 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockEpochManager.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; +// solhint-disable no-unused-vars + +import "../../../contracts/common/interfaces/IEpochManager.sol"; + +/** + * @title A mock EpochManager for testing. + */ + +contract MockEpochManager is IEpochManager { + struct Epoch { + uint256 firstBlock; + uint256 lastBlock; + uint256 startTimestamp; + uint256 endTimestamp; + uint256 rewardsBlock; + } + + uint256 public epochDuration; + + uint256 public firstKnownEpoch; + uint256 private currentEpochNumber; + address[] public elected; + address public epochManagerEnabler; + bool systemInitialized; + + bool private _isTimeForNextEpoch; + bool private isProcessingEpoch; + mapping(uint256 => Epoch) private epochs; + + event SendValidatorPaymentCalled(address validator); + + function setCurrentEpochNumber(uint256 _newEpochNumber) external { + currentEpochNumber = _newEpochNumber; + } + + function initializeSystem( + uint256 firstEpochNumber, + uint256 firstEpochBlock, + address[] calldata firstElected + ) external { + firstKnownEpoch = firstEpochNumber; + currentEpochNumber = firstEpochNumber; + + Epoch storage _currentEpoch = epochs[currentEpochNumber]; + _currentEpoch.firstBlock = firstEpochBlock; + _currentEpoch.startTimestamp = block.timestamp; + + elected = firstElected; + + systemInitialized = true; + epochManagerEnabler = address(0); + } + + function startNextEpochProcess() external {} + function finishNextEpochProcess( + address[] calldata groups, + address[] calldata lessers, + address[] calldata greaters + ) external {} + + function setIsTimeForNextEpoch(bool _isTime) external { + _isTimeForNextEpoch = _isTime; + } + function setIsOnEpochProcess(bool _isProcessing) external { + isProcessingEpoch = _isProcessing; + } + + function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256) { + Epoch storage _epoch = epochs[currentEpochNumber]; + + return (_epoch.firstBlock, _epoch.lastBlock, _epoch.startTimestamp, _epoch.rewardsBlock); + } + + function getCurrentEpochNumber() external view returns (uint256) { + return currentEpochNumber; + } + function getElected() external view returns (address[] memory) { + return elected; + } + + function getFirstBlockAtEpoch(uint256 _epoch) external view returns (uint256) { + Epoch storage targetEpoch = epochs[_epoch]; + + return (targetEpoch.firstBlock); + } + + function getLastBlockAtEpoch(uint256 _epoch) external view returns (uint256) { + Epoch storage targetEpoch = epochs[_epoch]; + + return (targetEpoch.lastBlock); + } + + function getEpochProcessingState() + external + view + returns (uint256, uint256, uint256, uint256, uint256) + { + return (0, 0, 0, 0, 0); + } + + function systemAlreadyInitialized() external view returns (bool) { + return systemInitialized; + } + + function isBlocked() external view returns (bool) { + return isProcessingEpoch; + } + function isTimeForNextEpoch() external view returns (bool) { + return _isTimeForNextEpoch; + } + function isOnEpochProcess() external view returns (bool) { + return isProcessingEpoch; + } + + function sendValidatorPayment(address validator) public { + emit SendValidatorPaymentCalled(validator); + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockRegistry.sol b/packages/protocol/contracts-0.8/common/test/MockRegistry.sol new file mode 100644 index 00000000000..eb42d084be4 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockRegistry.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; + +import "../../../contracts/common/interfaces/IRegistry.sol"; +import "../../../contracts/common/interfaces/IRegistryInitializer.sol"; +import "../../../contracts/common/Initializable.sol"; + +/** + * @title Routes identifiers to addresses. + */ +contract MockRegistry is IRegistry, IRegistryInitializer, Ownable, Initializable { + using SafeMath for uint256; + + mapping(bytes32 => address) public registry; + + event RegistryUpdated(string identifier, bytes32 indexed identifierHash, address indexed addr); + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + */ + function initialize() external initializer { + _transferOwnership(msg.sender); + } + + /** + * @notice Associates the given address with the given identifier. + * @param identifier Identifier of contract whose address we want to set. + * @param addr Address of contract. + */ + function setAddressFor(string calldata identifier, address addr) external onlyOwner { + bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); + registry[identifierHash] = addr; + emit RegistryUpdated(identifier, identifierHash, addr); + } + + /** + * @notice Gets address associated with the given identifierHash. + * @param identifierHash Identifier hash of contract whose address we want to look up. + * @dev Throws if address not set. + */ + function getAddressForOrDie(bytes32 identifierHash) external view returns (address) { + require(registry[identifierHash] != address(0), "identifier has no registry entry"); + return registry[identifierHash]; + } + + /** + * @notice Gets address associated with the given identifierHash. + * @param identifierHash Identifier hash of contract whose address we want to look up. + */ + function getAddressFor(bytes32 identifierHash) external view returns (address) { + return registry[identifierHash]; + } + + /** + * @notice Gets address associated with the given identifier. + * @param identifier Identifier of contract whose address we want to look up. + * @dev Throws if address not set. + */ + function getAddressForStringOrDie(string calldata identifier) external view returns (address) { + bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); + require(registry[identifierHash] != address(0), "identifier has no registry entry"); + return registry[identifierHash]; + } + + /** + * @notice Gets address associated with the given identifier. + * @param identifier Identifier of contract whose address we want to look up. + */ + function getAddressForString(string calldata identifier) external view returns (address) { + bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); + return registry[identifierHash]; + } + + /** + * @notice Iterates over provided array of identifiers, getting the address for each. + * Returns true if `sender` matches the address of one of the provided identifiers. + * @param identifierHashes Array of hashes of approved identifiers. + * @param sender Address in question to verify membership. + * @return True if `sender` corresponds to the address of any of `identifiers` + * registry entries. + */ + function isOneOf( + bytes32[] calldata identifierHashes, + address sender + ) external view returns (bool) { + for (uint256 i = 0; i < identifierHashes.length; i = i.add(1)) { + if (registry[identifierHashes[i]] == sender) { + return true; + } + } + return false; + } +} diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts-0.8/governance/Validators.sol similarity index 88% rename from packages/protocol/contracts/governance/Validators.sol rename to packages/protocol/contracts-0.8/governance/Validators.sol index 2f5ce7b2af5..7b9354197f8 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts-0.8/governance/Validators.sol @@ -1,20 +1,24 @@ -pragma solidity ^0.5.13; +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; -import "openzeppelin-solidity/contracts/math/Math.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "solidity-bytes-utils/contracts/BytesLib.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; +import "@openzeppelin/contracts8/utils/math/Math.sol"; +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; +import "solidity-bytes-utils-8/contracts/BytesLib.sol"; -import "./interfaces/IValidators.sol"; +import "../../contracts/governance/interfaces/IValidators.sol"; -import "../common/CalledByVm.sol"; -import "../common/Initializable.sol"; -import "../common/FixidityLib.sol"; +import "../../contracts/common/CalledByVm.sol"; +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; import "../common/UsingRegistry.sol"; import "../common/UsingPrecompiles.sol"; -import "../common/interfaces/ICeloVersionedContract.sol"; -import "../common/libraries/ReentrancyGuard.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/common/libraries/ReentrancyGuard.sol"; +import "../common/interfaces/IStableToken.sol"; + +import "../../contracts/common/interfaces/IAccounts.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. @@ -108,6 +112,12 @@ contract Validators is FixidityLib.Fraction adjustmentSpeed; } + struct InitParams { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } + mapping(address => ValidatorGroup) private groups; mapping(address => Validator) private validators; address[] private registeredGroups; @@ -175,7 +185,6 @@ contract Validators is * @param validatorScoreAdjustmentSpeed The speed at which validator scores are adjusted. * @param _membershipHistoryLength The max number of entries for validator membership history. * @param _maxGroupSize The maximum group size. - * @param _commissionUpdateDelay The number of blocks to delay a ValidatorGroup's commission * update. * @dev Should be called only once. */ @@ -190,8 +199,7 @@ contract Validators is uint256 _membershipHistoryLength, uint256 _slashingMultiplierResetPeriod, uint256 _maxGroupSize, - uint256 _commissionUpdateDelay, - uint256 _downtimeGracePeriod + InitParams calldata initParams ) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); @@ -199,19 +207,18 @@ contract Validators is setValidatorLockedGoldRequirements(validatorRequirementValue, validatorRequirementDuration); setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); setMaxGroupSize(_maxGroupSize); - setCommissionUpdateDelay(_commissionUpdateDelay); + setCommissionUpdateDelay(initParams.commissionUpdateDelay); setMembershipHistoryLength(_membershipHistoryLength); setSlashingMultiplierResetPeriod(_slashingMultiplierResetPeriod); - setDowntimeGracePeriod(_downtimeGracePeriod); + setDowntimeGracePeriod(initParams.downtimeGracePeriod); } /** * @notice Updates a validator's score based on its uptime for the epoch. * @param signer The validator signer of the validator account whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. - * @return True upon success. */ - function updateValidatorScoreFromSigner(address signer, uint256 uptime) external onlyVm { + function updateValidatorScoreFromSigner(address signer, uint256 uptime) external virtual onlyVm { allowOnlyL1(); _updateValidatorScoreFromSigner(signer, uptime); } @@ -221,12 +228,12 @@ contract Validators is * @param signer The validator signer of the account to distribute the epoch payment to. * @param maxPayment The maximum payment to the validator. Actual payment is based on score and * group commission. - * @return The total payment paid to the validator and their group. + * @return distributeEpochPaymentsFromSigner The total payment paid to the validator and their group. */ function distributeEpochPaymentsFromSigner( address signer, uint256 maxPayment - ) external onlyVm returns (uint256) { + ) external virtual onlyVm returns (uint256) { allowOnlyL1(); return _distributeEpochPaymentsFromSigner(signer, maxPayment); } @@ -248,7 +255,7 @@ contract Validators is bytes calldata blsPublicKey, bytes calldata blsPop ) external nonReentrant returns (bool) { - allowOnlyL1(); + allowOnlyL1(); // For L2, use registerValidatorNoBls address account = getAccounts().validatorSignerToAccount(msg.sender); _isRegistrationAllowed(account); require(!isValidator(account) && !isValidatorGroup(account), "Already registered"); @@ -271,6 +278,34 @@ contract Validators is return true; } + /** + * @notice Registers a validator unaffiliated with any validator group. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus, should + * match the validator signer. 64 bytes. + * @return True upon success. + * @dev Fails if the account is already a validator or validator group. + * @dev Fails if the account does not have sufficient Locked Gold. + */ + function registerValidatorNoBls( + bytes calldata ecdsaPublicKey + ) external nonReentrant onlyL2 returns (bool) { + address account = getAccounts().validatorSignerToAccount(msg.sender); + _isRegistrationAllowed(account); + require(!isValidator(account) && !isValidatorGroup(account), "Already registered"); + uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); + require(lockedGoldBalance >= validatorLockedGoldRequirements.value, "Deposit too small"); + Validator storage validator = validators[account]; + address signer = getAccounts().getValidatorSigner(account); + require( + _updateEcdsaPublicKey(validator, account, signer, ecdsaPublicKey), + "Error updating ECDSA public key" + ); + registeredValidators.push(account); + updateMembershipHistory(account, address(0)); + emit ValidatorRegistered(account); + return true; + } + /** * @notice De-registers a validator. * @param index The index of this validator in the list of all registered validators. @@ -294,7 +329,7 @@ contract Validators is uint256 requirementEndTime = validator.membershipHistory.lastRemovedFromGroupTimestamp.add( validatorLockedGoldRequirements.duration ); - require(requirementEndTime < now, "Not yet requirement end time"); + require(requirementEndTime < block.timestamp, "Not yet requirement end time"); // Remove the validator. deleteElement(registeredValidators, account, index); @@ -310,7 +345,6 @@ contract Validators is * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account), "Not a validator"); require(isValidatorGroup(group), "Not a validator group"); @@ -374,7 +408,6 @@ contract Validators is address signer, bytes calldata ecdsaPublicKey ) external onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { - allowOnlyL1(); require(isValidator(account), "Not a validator"); Validator storage validator = validators[account]; require( @@ -400,7 +433,7 @@ contract Validators is uint256[] storage sizeHistory = groups[account].sizeHistory; if (sizeHistory.length > 1) { require( - sizeHistory[1].add(groupLockedGoldRequirements.duration) < now, + sizeHistory[1].add(groupLockedGoldRequirements.duration) < block.timestamp, "Hasn't been empty for long enough" ); } @@ -451,7 +484,6 @@ contract Validators is * @dev Fails if the account does not have sufficient weight. */ function registerValidatorGroup(uint256 commission) external nonReentrant returns (bool) { - allowOnlyL1(); require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); address account = getAccounts().validatorSignerToAccount(msg.sender); _isRegistrationAllowed(account); @@ -476,14 +508,13 @@ contract Validators is * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements > 0, "Validator group empty"); return _addMember(account, validator, address(0), address(0)); } /** - * @notice Adds the first member to a group's list of members and marks it eligible for election. + * @notice Adds the first member to a group's list of members and marks the group eligible for election. * @param validator The validator to add to the group * @param lesser The address of the group that has received fewer votes than this group. * @param greater The address of the group that has received more votes than this group. @@ -496,7 +527,6 @@ contract Validators is address lesser, address greater ) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements == 0, "Validator group not empty"); return _addMember(account, validator, lesser, greater); @@ -546,7 +576,6 @@ contract Validators is * payments made to its members. Must be in the range [0, 1.0]. */ function setNextCommissionUpdate(uint256 commission) external { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; @@ -562,11 +591,12 @@ contract Validators is * @notice Updates a validator group's commission based on the previously queued update */ function updateCommission() external { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; + _sendValidatorGroupPaymentsIfNecessary(group); + require(group.nextCommissionBlock != 0, "No commission update queued"); require(group.nextCommissionBlock <= block.number, "Can't apply commission update yet"); @@ -581,6 +611,7 @@ contract Validators is * @param validatorAccount The validator to deaffiliate from their affiliated validator group. */ function forceDeaffiliateIfValidator(address validatorAccount) external nonReentrant onlySlasher { + allowOnlyL1(); if (isValidator(validatorAccount)) { Validator storage validator = validators[validatorAccount]; if (validator.affiliation != address(0)) { @@ -598,7 +629,7 @@ contract Validators is require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; require( - now >= group.slashInfo.lastSlashed.add(slashingMultiplierResetPeriod), + block.timestamp >= group.slashInfo.lastSlashed.add(slashingMultiplierResetPeriod), "`resetSlashingMultiplier` called before resetPeriod expired" ); group.slashInfo.multiplier = FixidityLib.fixed1(); @@ -613,13 +644,28 @@ contract Validators is require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; group.slashInfo.multiplier = FixidityLib.wrap(group.slashInfo.multiplier.unwrap().div(2)); - group.slashInfo.lastSlashed = now; + group.slashInfo.lastSlashed = block.timestamp; + } + + // TODO: Move this function's logic to `EpochManager` once Mento updates stable token + // to allow `EpochManager` to mint. + /** + * @notice Allows the EpochManager contract to mint stable token for itself. + * @param amount The amount to be minted. + */ + function mintStableToEpochManager( + uint256 amount + ) external onlyL2 nonReentrant onlyRegisteredContract(EPOCH_MANAGER_REGISTRY_ID) { + require( + IStableToken(getStableToken()).mint(msg.sender, amount), + "mint failed to epoch manager" + ); } /** * @notice Returns the validator BLS key. * @param signer The account that registered the validator or its authorized signing address. - * @return The validator BLS key. + * @return blsPublicKey The validator BLS key. */ function getValidatorBlsPublicKeyFromSigner( address signer @@ -629,6 +675,10 @@ contract Validators is return validators[account].publicKeys.bls; } + function getMembershipHistoryLength() external view returns (uint256) { + return membershipHistoryLength; + } + /** * @notice Returns validator group information. * @param account The account that registered the validator group. @@ -664,7 +714,8 @@ contract Validators is * @notice Returns the top n group members for a particular group. * @param account The address of the validator group. * @param n The number of members to return. - * @return The top n group members for a particular group. + * @return The signers of the top n group members for a particular group. + * @dev Returns the account instead of signer on L2. */ function getTopGroupValidators( address account, @@ -672,12 +723,23 @@ contract Validators is ) external view returns (address[] memory) { address[] memory topAccounts = groups[account].members.headN(n); address[] memory topValidators = new address[](n); + + IAccounts accounts = getAccounts(); + for (uint256 i = 0; i < n; i = i.add(1)) { - topValidators[i] = getAccounts().getValidatorSigner(topAccounts[i]); + topValidators[i] = accounts.getValidatorSigner(topAccounts[i]); } return topValidators; } + function getTopGroupValidatorsAccounts( + address account, + uint256 n + ) external view returns (address[] memory) { + address[] memory topAccounts = groups[account].members.headN(n); + return topAccounts; + } + /** * @notice Returns the number of members in the provided validator groups. * @param accounts The addresses of the validator groups. @@ -768,9 +830,8 @@ contract Validators is uint256 epochNumber, uint256 index ) external view returns (address) { - allowOnlyL1(); require(isValidator(account), "Not a validator"); - require(epochNumber <= getEpochNumber(), "Epoch cannot be larger than current"); + require(epochNumber <= _getEpochNumber(), "Epoch cannot be larger than current"); MembershipHistory storage history = validators[account].membershipHistory; require(index < history.tail.add(history.numEntries), "index out of bounds"); require(index >= history.tail && history.numEntries > 0, "index out of bounds"); @@ -840,6 +901,39 @@ contract Validators is return commissionUpdateDelay; } + /** + * @notice Computes epoch payments to the account + * @param account The validator account of the validator to distribute the epoch payment to. + * @param maxPayment The maximum payment to the validator. Actual payment is based on score and + * group commission. + * @return The total payment paid to the validator and their group. + */ + function computeEpochReward( + address account, + uint256 score, + uint256 maxPayment + ) external view virtual returns (uint256) { + require(isValidator(account), "Not a validator"); + FixidityLib.Fraction memory scoreFraction = FixidityLib.wrap(score); + require(scoreFraction.lte(FixidityLib.fixed1()), "Score must be <= 1"); + + // The group that should be paid is the group that the validator was a member of at the + // time it was elected. + address group = getMembershipInLastEpoch(account); + require(group != address(0), "Validator not registered with a group"); + // Both the validator and the group must maintain the minimum locked gold balance in order to + // receive epoch payments. + if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { + FixidityLib.Fraction memory totalPayment = FixidityLib + .newFixed(maxPayment) + .multiply(scoreFraction) + .multiply(groups[group].slashInfo.multiplier); + return totalPayment.fromFixed(); + } else { + return 0; + } + } + /** * @notice Returns the storage, major, minor, and patch version of the contract. * @return Storage version of the contract. @@ -856,7 +950,6 @@ contract Validators is * @param delay Number of blocks to delay the update */ function setCommissionUpdateDelay(uint256 delay) public onlyOwner { - allowOnlyL1(); require(delay != commissionUpdateDelay, "commission update delay not changed"); commissionUpdateDelay = delay; emit CommissionUpdateDelaySet(delay); @@ -868,7 +961,6 @@ contract Validators is * @return True upon success. */ function setMaxGroupSize(uint256 size) public onlyOwner returns (bool) { - allowOnlyL1(); require(0 < size, "Max group size cannot be zero"); require(size != maxGroupSize, "Max group size not changed"); maxGroupSize = size; @@ -882,7 +974,6 @@ contract Validators is * @return True upon success. */ function setMembershipHistoryLength(uint256 length) public onlyOwner returns (bool) { - allowOnlyL1(); require(0 < length, "Membership history length cannot be zero"); require(length != membershipHistoryLength, "Membership history length not changed"); membershipHistoryLength = length; @@ -989,7 +1080,7 @@ contract Validators is uint256[] storage sizeHistory = groups[account].sizeHistory; if (sizeHistory.length > 0) { for (uint256 i = sizeHistory.length.sub(1); i > 0; i = i.sub(1)) { - if (sizeHistory[i].add(groupLockedGoldRequirements.duration) >= now) { + if (sizeHistory[i].add(groupLockedGoldRequirements.duration) >= block.timestamp) { multiplier = Math.max(i, multiplier); break; } @@ -1006,8 +1097,8 @@ contract Validators is * @return The group that `account` was a member of at the end of the last epoch. */ function getMembershipInLastEpoch(address account) public view returns (address) { - allowOnlyL1(); - uint256 epochNumber = getEpochNumber(); + uint256 epochNumber = _getEpochNumber(); + MembershipHistory storage history = validators[account].membershipHistory; uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); // If the most recent entry in the membership history is for the current epoch number, we need @@ -1058,7 +1149,11 @@ contract Validators is /** * @notice Returns validator information. * @param account The account that registered the validator. - * @return The unpacked validator struct. + * @return ecdsaPublicKey The ECDSA public key. + * @return blsPublicKey The BLS public key. + * @return affiliation The address of the validator group the validator is a member of. + * @return score The validator's score. + * @return signer The address of the validator's signer. */ function getValidator( address account @@ -1084,6 +1179,17 @@ contract Validators is ); } + /** + * @notice Returns affiliated group to validator. + * @param account The account that registered the validator. + * @return group The validator group. + */ + function getValidatorsGroup(address account) public view returns (address group) { + require(isValidator(account), "Not a validator"); + Validator storage validator = validators[account]; + return validator.affiliation; + } + /** * @notice Returns the number of members in a validator group. * @param account The address of the validator group. @@ -1109,7 +1215,7 @@ contract Validators is * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { - return validators[account].publicKeys.bls.length > 0; + return validators[account].publicKeys.ecdsa.length > 0; } /** @@ -1143,7 +1249,7 @@ contract Validators is (address beneficiary, uint256 fraction) = getAccounts().getPaymentDelegation(account); uint256 delegatedPayment = remainingPayment.multiply(FixidityLib.wrap(fraction)).fromFixed(); uint256 validatorPayment = remainingPayment.fromFixed().sub(delegatedPayment); - IStableToken stableToken = getStableToken(); + IStableToken stableToken = IStableToken(getStableToken()); require(stableToken.mint(group, groupPayment), "mint failed to validator group"); require(stableToken.mint(account, validatorPayment), "mint failed to validator account"); if (fraction != 0) { @@ -1161,7 +1267,6 @@ contract Validators is * @param signer The validator signer of the validator whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) - * @return True upon success. */ function _updateValidatorScoreFromSigner(address signer, uint256 uptime) internal { address account = getAccounts().signerToAccount(signer); @@ -1288,7 +1393,7 @@ contract Validators is uint256 lastIndex = list.length.sub(1); list[index] = list[lastIndex]; delete list[lastIndex]; - list.length = lastIndex; + list.pop(); } /** @@ -1325,11 +1430,12 @@ contract Validators is */ function updateMembershipHistory(address account, address group) private returns (bool) { MembershipHistory storage history = validators[account].membershipHistory; - uint256 epochNumber = getEpochNumber(); + uint256 epochNumber = _getEpochNumber(); + uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); if (history.numEntries > 0 && group == address(0)) { - history.lastRemovedFromGroupTimestamp = now; + history.lastRemovedFromGroupTimestamp = block.timestamp; } if (history.numEntries > 0 && history.entries[head].epochNumber == epochNumber) { @@ -1368,9 +1474,9 @@ contract Validators is function updateSizeHistory(address group, uint256 size) private { uint256[] storage sizeHistory = groups[group].sizeHistory; if (size == sizeHistory.length) { - sizeHistory.push(now); + sizeHistory.push(block.timestamp); } else if (size < sizeHistory.length) { - sizeHistory[size] = now; + sizeHistory[size] = block.timestamp; } else { require(false, "Unable to update size history"); } @@ -1386,6 +1492,7 @@ contract Validators is Validator storage validator, address validatorAccount ) private returns (bool) { + _sendValidatorPaymentIfNecessary(validatorAccount); address affiliation = validator.affiliation; ValidatorGroup storage group = groups[affiliation]; if (group.members.contains(validatorAccount)) { @@ -1395,4 +1502,29 @@ contract Validators is emit ValidatorDeaffiliated(validatorAccount, affiliation); return true; } + + function _sendValidatorPaymentIfNecessary(address validator) private { + if (isL2()) { + getEpochManager().sendValidatorPayment(validator); + } + } + + function _sendValidatorGroupPaymentsIfNecessary(ValidatorGroup storage group) private { + address[] memory members = group.members.getKeys(); + for (uint256 i = 0; i < members.length; i++) { + _sendValidatorPaymentIfNecessary(members[i]); + } + } + + /** + * @notice Returns the epoch number. + * @return Current epoch number. + */ + function _getEpochNumber() private view returns (uint256) { + if (isL2()) { + return getEpochManager().getCurrentEpochNumber(); + } else { + return getEpochNumber(); + } + } } diff --git a/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol b/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol new file mode 100644 index 00000000000..ce6125dcee1 --- /dev/null +++ b/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../../../contracts/governance/interfaces/IEpochRewards.sol"; + +/** + * @title A wrapper around EpochRewards that exposes internal functions for testing. + */ +contract EpochRewardsMock08 is IEpochRewards { + uint256 private numValidatorsInCurrentSet; + address public carbonOffsettingPartner; + + function setNumberValidatorsInCurrentSet(uint256 value) external { + numValidatorsInCurrentSet = value; + } + + // TODO: (soloseng) implement mock + function updateTargetVotingYield() external {} + + function getRewardsMultiplier( + uint256 targetGoldTotalSupplyIncrease + ) external view returns (uint256) { + // return _getRewardsMultiplier(targetGoldTotalSupplyIncrease).unwrap(); + return 0; + } + + function isReserveLow() external view returns (bool) { + return false; + } + function calculateTargetEpochRewards() + external + view + returns (uint256, uint256, uint256, uint256) + { + return (5, 5, 5, 5); + } + function getTargetVotingYieldParameters() external view returns (uint256, uint256, uint256) { + return (0, 0, 0); + } + function getRewardsMultiplierParameters() external view returns (uint256, uint256, uint256) { + return (0, 0, 0); + } + function getCommunityRewardFraction() external view returns (uint256) { + return 0; + } + function getCarbonOffsettingFraction() external view returns (uint256) { + return 0; + } + function getTargetVotingGoldFraction() external view returns (uint256) { + return 0; + } + function getRewardsMultiplier() external view returns (uint256) { + return 0; + } + + // mocks the precompile + function numberValidatorsInCurrentSet() public view returns (uint256) { + return numValidatorsInCurrentSet; + } +} diff --git a/packages/protocol/contracts-0.8/governance/test/IMockValidators.sol b/packages/protocol/contracts-0.8/governance/test/IMockValidators.sol new file mode 100644 index 00000000000..c20e1b06cfd --- /dev/null +++ b/packages/protocol/contracts-0.8/governance/test/IMockValidators.sol @@ -0,0 +1,61 @@ +pragma solidity >=0.8.7 <0.8.20; + +interface IMockValidators { + function isValidator(address) external returns (bool); + function isValidatorGroup(address) external returns (bool); + + function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool); + + function updatePublicKeys( + address, + address, + bytes calldata, + bytes calldata, + bytes calldata + ) external returns (bool); + + function setValidator(address) external; + + function setValidatorGroup(address group) external; + + function affiliate(address group) external returns (bool); + + function setDoesNotMeetAccountLockedGoldRequirements(address account) external; + + function setNumRegisteredValidators(uint256 value) external; + + function setMembers(address group, address[] calldata _members) external; + + function setCommission(address group, uint256 commission) external; + + function setAccountLockedGoldRequirement(address account, uint256 value) external; + + function halveSlashingMultiplier(address) external; + + function forceDeaffiliateIfValidator(address validator) external; + + function getTopGroupValidators(address group, uint256 n) external view returns (address[] memory); + + function getValidatorGroup( + address + ) + external + view + returns (address[] memory, uint256, uint256, uint256, uint256[] memory, uint256, uint256); + + function getValidatorGroupSlashingMultiplier(address) external view returns (uint256); + + function meetsAccountLockedGoldRequirements(address account) external view returns (bool); + + function getNumRegisteredValidators() external view returns (uint256); + + function getAccountLockedGoldRequirement(address account) external view returns (uint256); + + function calculateGroupEpochScore(uint256[] calldata uptimes) external view returns (uint256); + + function getGroupsNumMembers(address[] calldata groups) external view returns (uint256[] memory); + + function groupMembershipInEpoch(address addr, uint256, uint256) external view returns (address); + + function getGroupNumMembers(address group) external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/stability/test/MockReserve.sol b/packages/protocol/contracts-0.8/stability/test/MockReserve.sol new file mode 100644 index 00000000000..09b64ab4385 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/test/MockReserve.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +// solhint-disable no-unused-vars + +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +/** + * @title A mock Reserve for testing. + */ +contract MockReserve08 { + mapping(address => bool) public tokens; + + IERC20 public goldToken; + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + function setGoldToken(address goldTokenAddress) external { + goldToken = IERC20(goldTokenAddress); + } + + function transferGold(address to, uint256 value) external returns (bool) { + require(goldToken.transfer(to, value), "gold token transfer failed"); + return true; + } + + function transferExchangeGold(address to, uint256 value) external returns (bool) { + require(goldToken.transfer(to, value), "gold token transfer failed"); + return true; + } + + function addToken(address token) external returns (bool) { + tokens[token] = true; + return true; + } + + function getUnfrozenReserveGoldBalance() external view returns (uint256) { + return address(this).balance; + } + + function burnToken(address) external pure returns (bool) { + return true; + } + + function getReserveGoldBalance() public view returns (uint256) { + return address(this).balance; + } +} diff --git a/packages/protocol/contracts-0.8/stability/test/MockStableToken.sol b/packages/protocol/contracts-0.8/stability/test/MockStableToken.sol new file mode 100644 index 00000000000..26c8e940a23 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/test/MockStableToken.sol @@ -0,0 +1,81 @@ +pragma solidity >=0.8.0 <0.9.0; +// solhint-disable no-unused-vars + +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; + +import "../../../contracts/common/FixidityLib.sol"; + +/** + * @title A mock StableToken for testing. + */ +contract MockStableToken08 { + using FixidityLib for FixidityLib.Fraction; + using SafeMath for uint256; + + uint8 public constant decimals = 18; + uint256 public _totalSupply; + FixidityLib.Fraction public inflationFactor; + + // Stored as units. Value can be found using unitsToValue(). + mapping(address => uint256) public balances; + + constructor() public { + setInflationFactor(FixidityLib.fixed1().unwrap()); + } + + function setTotalSupply(uint256 value) external { + _totalSupply = value; + } + + function mint(address to, uint256 value) external returns (bool) { + require(to != address(0), "0 is a reserved address"); + balances[to] = balances[to].add(valueToUnits(value)); + _totalSupply = _totalSupply.add(value); + return true; + } + + function burn(uint256 value) external returns (bool) { + balances[msg.sender] = balances[msg.sender].sub(valueToUnits(value)); + _totalSupply = _totalSupply.sub(value); + return true; + } + + function transfer(address to, uint256 value) external returns (bool) { + return _transfer(msg.sender, to, value); + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + return _transfer(from, to, value); + } + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function setInflationFactor(uint256 newInflationFactor) public { + inflationFactor = FixidityLib.wrap(newInflationFactor); + } + + function balanceOf(address account) public view returns (uint256) { + return unitsToValue(balances[account]); + } + + function unitsToValue(uint256 units) public view returns (uint256) { + return FixidityLib.newFixed(units).divide(inflationFactor).fromFixed(); + } + + function valueToUnits(uint256 value) public view returns (uint256) { + return inflationFactor.multiply(FixidityLib.newFixed(value)).fromFixed(); + } + + function _transfer(address from, address to, uint256 value) internal returns (bool) { + uint256 balanceValue = balanceOf(from); + if (balanceValue < value) { + return false; + } + uint256 units = valueToUnits(value); + balances[from] = balances[from].sub(units); + balances[to] = balances[to].add(units); + return true; + } +} diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index af5a6885dcf..13fed26f9c5 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -567,7 +567,7 @@ contract Accounts is * be greater than 1. * @dev Use `deletePaymentDelegation` to unset the payment delegation. */ - function setPaymentDelegation(address beneficiary, uint256 fraction) public onlyL1 { + function setPaymentDelegation(address beneficiary, uint256 fraction) public { require(isAccount(msg.sender), "Must first register address with Account.createAccount"); require(beneficiary != address(0), "Beneficiary cannot be address 0x0"); FixidityLib.Fraction memory f = FixidityLib.wrap(fraction); diff --git a/packages/protocol/contracts/common/Blockable.sol b/packages/protocol/contracts/common/Blockable.sol new file mode 100644 index 00000000000..34f0903e7a8 --- /dev/null +++ b/packages/protocol/contracts/common/Blockable.sol @@ -0,0 +1,55 @@ +pragma solidity >=0.5.13 <0.9.0; + +import "./interfaces/IBlockable.sol"; +import "./interfaces/IBlocker.sol"; + +/** + * @title Blockable Contract + * @notice This contract allows certain actions to be blocked based on the logic of another contract implementing the IBlocker interface. + * @dev This contract uses an external IBlocker contract to determine if it is blocked. The owner can set the blocking contract. + **/ +contract Blockable is IBlockable { + // using directly memory slot so contracts can inherit from this contract withtout breaking storage layout + bytes32 private constant BLOCKEDBY_POSITION = + bytes32(uint256(keccak256("blocked_by_position")) - 1); + + event BlockedBySet(address indexed _blockedBy); + + /// @notice Modifier to ensure the function is only executed when the contract is not blocked. + /// @dev Reverts with an error if the contract is blocked. + modifier onlyWhenNotBlocked() { + require(!_isBlocked(), "Contract is blocked from performing this action"); + _; + } + + /// @notice Checks if the contract is currently blocked. + /// @return Returns true if the contract is blocked, otherwise false. + /// @dev The function returns false if no blocking contract has been set. + function isBlocked() external view returns (bool) { + return _isBlocked(); + } + + function getBlockedbyContract() public view returns (address blockedBy) { + bytes32 blockedByPosition = BLOCKEDBY_POSITION; + assembly { + blockedBy := sload(blockedByPosition) + } + return blockedBy; + } + + function _setBlockedBy(address _blockedBy) internal { + bytes32 blockedByPosition = BLOCKEDBY_POSITION; + assembly { + sstore(blockedByPosition, _blockedBy) + } + + emit BlockedBySet(_blockedBy); + } + + function _isBlocked() internal view returns (bool) { + if (getBlockedbyContract() == address(0)) { + return false; + } + return IBlocker(getBlockedbyContract()).isBlocked(); + } +} diff --git a/packages/protocol/contracts/common/GoldToken.sol b/packages/protocol/contracts/common/GoldToken.sol index 7f6b36cf105..742c8cd9f4f 100644 --- a/packages/protocol/contracts/common/GoldToken.sol +++ b/packages/protocol/contracts/common/GoldToken.sol @@ -10,7 +10,7 @@ import "./Initializable.sol"; import "./interfaces/ICeloToken.sol"; import "./interfaces/ICeloTokenInitializer.sol"; import "./interfaces/ICeloVersionedContract.sol"; -import "./interfaces/ICeloDistributionSchedule.sol"; +import "./interfaces/ICeloUnreleasedTreasury.sol"; import "../../contracts-0.8/common/IsL2Check.sol"; contract GoldToken is @@ -152,10 +152,6 @@ contract GoldToken is */ function transferFrom(address from, address to, uint256 value) external returns (bool) { require(to != address(0), "transfer attempted to reserved address 0x0"); - require( - to != registry.getAddressForOrDie(CELO_DISTRIBUTION_SCHEDULE_ID), - "transfer attempted to reserved CeloDistributionSchedule address" - ); require(value <= balanceOf(from), "transfer value exceeded balance of sender"); require( value <= allowed[from][msg.sender], @@ -224,18 +220,11 @@ contract GoldToken is return DECIMALS; } - /** - * @return The total amount of allocated CELO. - */ - function allocatedSupply() external view onlyL2 returns (uint256) { - return CELO_SUPPLY_CAP - registry.getAddressForOrDie(CELO_DISTRIBUTION_SCHEDULE_ID).balance; - } - /** * @return The total amount of CELO in existence, not including what the burn address holds. */ function circulatingSupply() external view returns (uint256) { - return totalSupply().sub(getBurnedAmount()).sub(balanceOf(address(0))); + return allocatedSupply().sub(getBurnedAmount()).sub(balanceOf(address(0))); } /** @@ -276,6 +265,18 @@ contract GoldToken is return _owner.balance; } + /** + * @return The total amount of allocated CELO. + */ + function allocatedSupply() public view returns (uint256) { + if (isL2()) { + return + CELO_SUPPLY_CAP - registry.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID).balance; + } else { + return totalSupply(); + } + } + /** * @return The total amount of CELO in existence, including what the burn address holds. */ @@ -294,10 +295,6 @@ contract GoldToken is * @return True if the transaction succeeds. */ function _transfer(address to, uint256 value) internal returns (bool) { - require( - to != registry.getAddressForOrDie(CELO_DISTRIBUTION_SCHEDULE_ID), - "transfer attempted to reserved CeloDistributionSchedule address" - ); require(value <= balanceOf(msg.sender), "transfer value exceeded balance of sender"); bool success; diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index 017ad69bd33..f20d5db08f9 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -57,15 +57,12 @@ contract UsingPrecompiles is IsL2Check { * @return The current epoch size in blocks. */ function getEpochSize() public view returns (uint256) { - if (isL2()) { - return DAY.div(5); - } else { - bytes memory out; - bool success; - (success, out) = EPOCH_SIZE.staticcall(abi.encodePacked(true)); - require(success, "error calling getEpochSize precompile"); - return getUint256FromBytes(out, 0); - } + allowOnlyL1(); + bytes memory out; + bool success; + (success, out) = EPOCH_SIZE.staticcall(abi.encodePacked(true)); + require(success, "error calling getEpochSize precompile"); + return getUint256FromBytes(out, 0); } /** diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index f4cf8d7b26a..d7d71a93e4e 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -5,12 +5,14 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "./interfaces/IAccounts.sol"; +import "./interfaces/IEpochManager.sol"; import "./interfaces/IFeeCurrencyWhitelist.sol"; import "./interfaces/IFreezer.sol"; import "./interfaces/IRegistry.sol"; -import "./interfaces/ICeloDistributionSchedule.sol"; +import "./interfaces/ICeloUnreleasedTreasury.sol"; import "../governance/interfaces/IElection.sol"; +import "../governance/interfaces/IEpochRewards.sol"; import "../governance/interfaces/IGovernance.sol"; import "../governance/interfaces/ILockedGold.sol"; import "../governance/interfaces/ILockedCelo.sol"; @@ -49,8 +51,12 @@ contract UsingRegistry is Ownable { bytes32 constant CELO_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("CeloToken")); bytes32 constant LOCKED_CELO_REGISTRY_ID = keccak256(abi.encodePacked("LockedCelo")); - bytes32 constant CELO_DISTRIBUTION_SCHEDULE_ID = - keccak256(abi.encodePacked("CeloDistributionSchedule")); + bytes32 constant CELO_UNRELEASED_TREASURY_REGISTRY_ID = + keccak256(abi.encodePacked("CeloUnreleasedTreasury")); + bytes32 constant EPOCH_REWARDS_REGISTRY_ID = keccak256(abi.encodePacked("EpochRewards")); + bytes32 constant EPOCH_MANAGER_ENABLER_REGISTRY_ID = + keccak256(abi.encodePacked("EpochManagerEnabler")); + bytes32 constant EPOCH_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("EpochManager")); // solhint-enable state-visibility IRegistry public registry; @@ -139,7 +145,16 @@ contract UsingRegistry is Ownable { return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } - function getCeloDistributionSchedule() internal view returns (ICeloDistributionSchedule) { - return ICeloDistributionSchedule(registry.getAddressForOrDie(CELO_DISTRIBUTION_SCHEDULE_ID)); + function getCeloUnreleasedTreasury() internal view returns (ICeloUnreleasedTreasury) { + return + ICeloUnreleasedTreasury(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID)); + } + + function getEpochRewards() internal view returns (IEpochRewards) { + return IEpochRewards(registry.getAddressForOrDie(EPOCH_REWARDS_REGISTRY_ID)); + } + + function getEpochManager() internal view returns (IEpochManager) { + return IEpochManager(registry.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID)); } } diff --git a/packages/protocol/contracts/common/UsingRegistryV2.sol b/packages/protocol/contracts/common/UsingRegistryV2.sol index f49a3769215..463ea48af1d 100644 --- a/packages/protocol/contracts/common/UsingRegistryV2.sol +++ b/packages/protocol/contracts/common/UsingRegistryV2.sol @@ -4,12 +4,14 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "./interfaces/IAccounts.sol"; +import "./interfaces/IEpochManager.sol"; import "./interfaces/IFeeCurrencyWhitelist.sol"; import "./interfaces/IFreezer.sol"; import "./interfaces/IRegistry.sol"; -import "./interfaces/ICeloDistributionSchedule.sol"; +import "./interfaces/ICeloUnreleasedTreasury.sol"; import "../governance/interfaces/IElection.sol"; +import "../governance/interfaces/IEpochRewards.sol"; import "../governance/interfaces/IGovernance.sol"; import "../governance/interfaces/ILockedGold.sol"; import "../governance/interfaces/ILockedCelo.sol"; @@ -59,11 +61,15 @@ contract UsingRegistryV2 { bytes32 internal constant STABLE_REAL_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableTokenBRL")); bytes32 internal constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); - bytes32 constant CELO_DISTRIBUTION_SCHEDULE_ID = - keccak256(abi.encodePacked("CeloDistributionSchedule")); + bytes32 internal constant CELO_UNRELEASED_TREASURY_REGISTRY_ID = + keccak256(abi.encodePacked("CeloUnreleasedTreasury")); bytes32 internal constant CELO_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("CeloToken")); bytes32 internal constant LOCKED_CELO_REGISTRY_ID = keccak256(abi.encodePacked("LockedCelo")); + bytes32 internal constant EPOCH_REWARDS_REGISTRY_ID = keccak256(abi.encodePacked("EpochRewards")); + bytes32 internal constant EPOCH_MANAGER_ENABLER_REGISTRY_ID = + keccak256(abi.encodePacked("EpochManagerEnabler")); + bytes32 internal constant EPOCH_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("EpochManager")); modifier onlyRegisteredContract(bytes32 identifierHash) { require( @@ -176,8 +182,18 @@ contract UsingRegistryV2 { return IValidators(registryContract.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } - function getCeloDistributionSchedule() internal view returns (ICeloDistributionSchedule) { + function getCeloUnreleasedTreasury() internal view returns (ICeloUnreleasedTreasury) { return - ICeloDistributionSchedule(registryContract.getAddressForOrDie(CELO_DISTRIBUTION_SCHEDULE_ID)); + ICeloUnreleasedTreasury( + registryContract.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID) + ); + } + + function getEpochRewards() internal view returns (IEpochRewards) { + return IEpochRewards(registryContract.getAddressForOrDie(EPOCH_REWARDS_REGISTRY_ID)); + } + + function getEpochManager() internal view returns (IEpochManager) { + return IEpochManager(registryContract.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID)); } } diff --git a/packages/protocol/contracts/common/interfaces/IBlockable.sol b/packages/protocol/contracts/common/interfaces/IBlockable.sol new file mode 100644 index 00000000000..c6663fbbaf1 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IBlockable.sol @@ -0,0 +1,7 @@ +pragma solidity >=0.5.13 <0.9.0; + +interface IBlockable { + function setBlockedByContract(address _blockedBy) external; + function isBlocked() external view returns (bool); + function getBlockedbyContract() external view returns (address); +} diff --git a/packages/protocol/contracts/common/interfaces/IBlocker.sol b/packages/protocol/contracts/common/interfaces/IBlocker.sol new file mode 100644 index 00000000000..e711c71597a --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IBlocker.sol @@ -0,0 +1,5 @@ +pragma solidity >=0.5.13 <0.9.0; + +interface IBlocker { + function isBlocked() external view returns (bool); +} diff --git a/packages/protocol/contracts/common/interfaces/ICeloDistributionSchedule.sol b/packages/protocol/contracts/common/interfaces/ICeloDistributionSchedule.sol deleted file mode 100644 index acec8464ae3..00000000000 --- a/packages/protocol/contracts/common/interfaces/ICeloDistributionSchedule.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity >=0.5.13 <0.9.0; - -interface ICeloDistributionSchedule { - /** - * @notice Sets the distribution schedule dependencies during L2 transition. - */ - function activate(uint256, uint256, address, uint256) external; - - /** - * @notice Mints CELO to the beneficiaries according to the predefined schedule. - */ - function mintAccordingToSchedule() external returns (bool); - - /** - * @return The currently mintable amount. - */ - function getMintableAmount() external returns (uint256); - - /** - * @notice Returns the target CELO supply according to the target schedule. - * @return The target CELO supply according to the target schedule. - */ - function getTargetCeloTotalSupply() external returns (uint256, uint256, uint256); -} diff --git a/packages/protocol/contracts/common/interfaces/ICeloToken.sol b/packages/protocol/contracts/common/interfaces/ICeloToken.sol index 8e2986adc53..5f7107395db 100644 --- a/packages/protocol/contracts/common/interfaces/ICeloToken.sol +++ b/packages/protocol/contracts/common/interfaces/ICeloToken.sol @@ -9,7 +9,9 @@ interface ICeloToken { function initialize(address) external; function transferWithComment(address, uint256, string calldata) external returns (bool); function burn(uint256 value) external returns (bool); + function mint(address to, uint256 value) external returns (bool); function name() external view returns (string memory); function symbol() external view returns (string memory); function decimals() external view returns (uint8); + function allocatedSupply() external view returns (uint256); } diff --git a/packages/protocol/contracts/common/interfaces/ICeloUnreleasedTreasury.sol b/packages/protocol/contracts/common/interfaces/ICeloUnreleasedTreasury.sol new file mode 100644 index 00000000000..b561ba11cb4 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/ICeloUnreleasedTreasury.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface ICeloUnreleasedTreasury { + /** + * @notice Releases the Celo to the specified address. + * @param to The address to release the amount to. + * @param amount The amount to release. + */ + function release(address to, uint256 amount) external; +} diff --git a/packages/protocol/contracts/common/interfaces/IEpochManager.sol b/packages/protocol/contracts/common/interfaces/IEpochManager.sol new file mode 100644 index 00000000000..343039256cc --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IEpochManager.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManager { + function initializeSystem( + uint256 firstEpochNumber, + uint256 firstEpochBlock, + address[] calldata firstElected + ) external; + function startNextEpochProcess() external; + function finishNextEpochProcess( + address[] calldata groups, + address[] calldata lessers, + address[] calldata greaters + ) external; + function sendValidatorPayment(address) external; + function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256); + function getCurrentEpochNumber() external view returns (uint256); + function getElected() external view returns (address[] memory); + function epochDuration() external view returns (uint256); + function firstKnownEpoch() external view returns (uint256); + function getEpochProcessingState() + external + view + returns (uint256, uint256, uint256, uint256, uint256); + + function systemAlreadyInitialized() external view returns (bool); + function isBlocked() external view returns (bool); + function isTimeForNextEpoch() external view returns (bool); + function isOnEpochProcess() external view returns (bool); + function getFirstBlockAtEpoch(uint256) external view returns (uint256); + function getLastBlockAtEpoch(uint256) external view returns (uint256); +} diff --git a/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol b/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol new file mode 100644 index 00000000000..8cf8ce591bd --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerEnabler { + function initEpochManager() external; + function captureEpochAndValidators() external; +} diff --git a/packages/protocol/contracts/common/interfaces/IScoreManager.sol b/packages/protocol/contracts/common/interfaces/IScoreManager.sol new file mode 100644 index 00000000000..0020fd65df0 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IScoreManager.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IScoreManager { + function setGroupScore(address group, uint256 score) external; + function setValidatorScore(address validator, uint256 score) external; + function getValidatorScore(address validator) external view returns (uint256); + function getGroupScore(address validator) external view returns (uint256); + function owner() external view returns (address); +} diff --git a/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol b/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol index d3394816bbf..8c48a49c652 100644 --- a/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol +++ b/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.5.13; +pragma solidity >=0.5.13 <0.8.20; /** * @title Helps contracts guard against reentrancy attacks. @@ -24,7 +24,7 @@ contract ReentrancyGuard { require(localCounter == _guardCounter, "reentrant call"); } - constructor() internal { + constructor() public { // The counter starts at one to prevent changing it from zero to a non-zero // value, which is a more expensive operation. _guardCounter = 1; diff --git a/packages/protocol/contracts/common/proxies/CeloDistributionScheduleProxy.sol b/packages/protocol/contracts/common/proxies/CeloUnreleasedTreasuryProxy.sol similarity index 65% rename from packages/protocol/contracts/common/proxies/CeloDistributionScheduleProxy.sol rename to packages/protocol/contracts/common/proxies/CeloUnreleasedTreasuryProxy.sol index 5fd995d64bf..553ff00a4b7 100644 --- a/packages/protocol/contracts/common/proxies/CeloDistributionScheduleProxy.sol +++ b/packages/protocol/contracts/common/proxies/CeloUnreleasedTreasuryProxy.sol @@ -3,4 +3,4 @@ pragma solidity ^0.5.13; import "../Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract CeloDistributionScheduleProxy is Proxy {} +contract CeloUnreleasedTreasuryProxy is Proxy {} diff --git a/packages/protocol/contracts/common/proxies/EpochManagerEnablerProxy.sol b/packages/protocol/contracts/common/proxies/EpochManagerEnablerProxy.sol new file mode 100644 index 00000000000..ddd10f3cd43 --- /dev/null +++ b/packages/protocol/contracts/common/proxies/EpochManagerEnablerProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract EpochManagerEnablerProxy is Proxy {} diff --git a/packages/protocol/contracts/common/proxies/EpochManagerProxy.sol b/packages/protocol/contracts/common/proxies/EpochManagerProxy.sol new file mode 100644 index 00000000000..7f5fc943e37 --- /dev/null +++ b/packages/protocol/contracts/common/proxies/EpochManagerProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract EpochManagerProxy is Proxy {} diff --git a/packages/protocol/contracts/common/proxies/ScoreManagerProxy.sol b/packages/protocol/contracts/common/proxies/ScoreManagerProxy.sol new file mode 100644 index 00000000000..d46446ee16b --- /dev/null +++ b/packages/protocol/contracts/common/proxies/ScoreManagerProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract ScoreManagerProxy is Proxy {} diff --git a/packages/protocol/contracts/governance/DoubleSigningSlasher.sol b/packages/protocol/contracts/governance/DoubleSigningSlasher.sol index b9aef3f737c..b36359cd07d 100644 --- a/packages/protocol/contracts/governance/DoubleSigningSlasher.sol +++ b/packages/protocol/contracts/governance/DoubleSigningSlasher.sol @@ -116,7 +116,7 @@ contract DoubleSigningSlasher is ICeloVersionedContract, SlasherUtil { uint256 index, bytes memory headerA, bytes memory headerB - ) public view returns (uint256) { + ) public view onlyL1 returns (uint256) { require(hashHeader(headerA) != hashHeader(headerB), "Block hashes have to be different"); uint256 blockNumber = getBlockNumberFromHeader(headerA); require( diff --git a/packages/protocol/contracts/governance/DowntimeSlasher.sol b/packages/protocol/contracts/governance/DowntimeSlasher.sol index d9f4f68a0ab..51a099cc542 100644 --- a/packages/protocol/contracts/governance/DowntimeSlasher.sol +++ b/packages/protocol/contracts/governance/DowntimeSlasher.sol @@ -170,7 +170,7 @@ contract DowntimeSlasher is ICeloVersionedContract, SlasherUtil { function getBitmapForInterval( uint256 startBlock, uint256 endBlock - ) public view returns (bytes32) { + ) public view onlyL1 returns (bytes32) { require(endBlock >= startBlock, "endBlock must be greater or equal than startBlock"); // The signature bitmap for block N is stored in block N+1. // The latest block is `block.number - 1`, which stores the signature bitmap for @@ -215,7 +215,7 @@ contract DowntimeSlasher is ICeloVersionedContract, SlasherUtil { uint256 startBlock, uint256 endBlock, uint256 signerIndex - ) public view returns (bool) { + ) public view onlyL1 returns (bool) { require(signerIndex < numberValidatorsInSet(startBlock), "bad validator index at start block"); require( isBitmapSetForInterval(startBlock, endBlock), @@ -250,7 +250,7 @@ contract DowntimeSlasher is ICeloVersionedContract, SlasherUtil { uint256[] memory startBlocks, uint256[] memory endBlocks, uint256[] memory signerIndices - ) public view returns (bool) { + ) public view onlyL1 returns (bool) { require(startBlocks.length > 0, "requires at least one interval"); require( startBlocks.length == endBlocks.length, diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 8a7633599cd..5e1c15c4684 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -15,6 +15,7 @@ import "../common/UsingRegistry.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/libraries/Heap.sol"; import "../common/libraries/ReentrancyGuard.sol"; +import "../common/Blockable.sol"; contract Election is IElection, @@ -24,7 +25,8 @@ contract Election is Initializable, UsingRegistry, UsingPrecompiles, - CalledByVm + CalledByVm, + Blockable { using AddressSortedLinkedList for SortedLinkedList.List; using FixidityLib for FixidityLib.Fraction; @@ -151,6 +153,14 @@ contract Election is ); event EpochRewardsDistributedToVoters(address indexed group, uint256 value); + modifier onlyVmOrPermitted(address permittedAddress) { + if (isL2()) require(msg.sender == permittedAddress, "Only permitted address can call"); + else { + require(msg.sender == address(0), "Only VM can call"); + } + _; + } + /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. * @param registryAddress The address of the registry core smart contract. @@ -196,7 +206,7 @@ contract Election is uint256 value, address lesser, address greater - ) external nonReentrant returns (bool) { + ) external nonReentrant onlyWhenNotBlocked returns (bool) { require(votes.total.eligible.contains(group), "Group not eligible"); require(0 < value, "Vote value cannot be zero"); require(canReceiveVotes(group, value), "Group cannot receive votes"); @@ -338,7 +348,7 @@ contract Election is uint256 value, address lesser, address greater - ) external onlyVm onlyL1 { + ) external onlyVmOrPermitted(registry.getAddressFor(EPOCH_MANAGER_REGISTRY_ID)) { _distributeEpochRewards(group, value, lesser, greater); } @@ -418,6 +428,13 @@ contract Election is return value; } + /// @notice Sets the address of the blocking contract. + /// @param _blockedBy The address of the contract that will determine if this contract is blocked. + /// @dev Can only be called by the owner of the contract. + function setBlockedByContract(address _blockedBy) external onlyOwner { + _setBlockedBy(_blockedBy); + } + /** * @notice Returns the groups that `account` has voted for. * @param account The address of the account casting votes. @@ -469,14 +486,23 @@ contract Election is } /** - * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * @notice Returns a list of elected validator signers with seats allocated to groups via the D'Hondt * method. - * @return The list of elected validators. + * @return The list of elected validator signers. */ function electValidatorSigners() external view returns (address[] memory) { return electNValidatorSigners(electableValidators.min, electableValidators.max); } + /** + * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * method. + * @return The list of elected validators. + */ + function electValidatorAccounts() external view returns (address[] memory) { + return electNValidatorAccounts(electableValidators.min, electableValidators.max); + } + /** * @notice Returns the total number of votes cast by an account. * @param account The address of the account. @@ -565,6 +591,43 @@ contract Election is .fromFixed(); } + /** + * @notice Returns the amount of rewards that voters for `group` are due at the end of an epoch. + * @param group The group to calculate epoch rewards for. + * @param totalEpochRewards The total amount of rewards going to all voters. + * @param groupScore The score of the group. + * @return The amount of rewards that voters for `group` are due at the end of an epoch. + * @dev Eligible groups that have received their maximum number of votes cannot receive more. + */ + function getGroupEpochRewardsBasedOnScore( + address group, + uint256 totalEpochRewards, + uint256 groupScore + ) external view onlyL2 returns (uint256) { + IValidators validators = getValidators(); + // The group must meet the balance requirements for their voters to receive epoch rewards. + if (!validators.meetsAccountLockedGoldRequirements(group) || votes.active.total <= 0) { + return 0; + } + + FixidityLib.Fraction memory votePortion = FixidityLib.newFixedFraction( + votes.active.forGroup[group].total, + votes.active.total + ); + FixidityLib.Fraction memory slashingMultiplier = FixidityLib.wrap( + validators.getValidatorGroupSlashingMultiplier(group) + ); + + FixidityLib.Fraction memory score = FixidityLib.wrap(groupScore); + return + FixidityLib + .newFixed(totalEpochRewards) + .multiply(votePortion) + .multiply(score) + .multiply(slashingMultiplier) + .fromFixed(); + } + /** * @notice Returns whether or not an account's votes for the specified group can be activated. * @param account The account with pending votes. @@ -574,7 +637,7 @@ contract Election is */ function hasActivatablePendingVotes(address account, address group) external view returns (bool) { PendingVote storage pendingVote = votes.pending.forGroup[group].byAccount[account]; - return pendingVote.epoch < getEpochNumber() && pendingVote.value > 0; + return pendingVote.epoch < _getEpochNumber() && pendingVote.value > 0; } /** @@ -732,16 +795,35 @@ contract Election is return votes.active.total; } + function electNValidatorSigners( + uint256 minElectableValidators, + uint256 maxElectableValidators + ) public view returns (address[] memory) { + bool accounts = false; + return + _electNValidatorSignerOrAccount(minElectableValidators, maxElectableValidators, accounts); + } + + function electNValidatorAccounts( + uint256 minElectableValidators, + uint256 maxElectableValidators + ) public view returns (address[] memory) { + bool accounts = true; + return + _electNValidatorSignerOrAccount(minElectableValidators, maxElectableValidators, accounts); + } + /** - * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * @notice Returns a list of elected validator with seats allocated to groups via the D'Hondt * method. - * @return The list of elected validators. + * @return The list of elected validator signers or accounts depending on input. * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. */ - function electNValidatorSigners( + function _electNValidatorSignerOrAccount( uint256 minElectableValidators, - uint256 maxElectableValidators - ) public view returns (address[] memory) { + uint256 maxElectableValidators, + bool accounts // accounts or signers + ) internal view returns (address[] memory) { // Groups must have at least `electabilityThreshold` proportion of the total votes to be // considered for the election. uint256 requiredVotes = electabilityThreshold @@ -753,6 +835,7 @@ contract Election is requiredVotes, maxElectableValidators ); + address[] memory electionGroups = votes.total.eligible.headN(numElectionGroups); uint256[] memory numMembers = getValidators().getGroupsNumMembers(electionGroups); // Holds the number of members elected for each of the eligible validator groups. @@ -794,12 +877,23 @@ contract Election is // Grab the top validators from each group that won seats. address[] memory electedValidators = new address[](totalNumMembersElected); totalNumMembersElected = 0; + + IValidators validators = getValidators(); + for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { // We use the validating delegate if one is set. - address[] memory electedGroupValidators = getValidators().getTopGroupValidators( - electionGroups[i], - numMembersElected[i] - ); + address[] memory electedGroupValidators; + if (accounts) { + electedGroupValidators = validators.getTopGroupValidatorsAccounts( + electionGroups[i], + numMembersElected[i] + ); + } else { + electedGroupValidators = validators.getTopGroupValidators( + electionGroups[i], + numMembersElected[i] + ); + } for (uint256 j = 0; j < electedGroupValidators.length; j = j.add(1)) { electedValidators[totalNumMembersElected] = electedGroupValidators[j]; totalNumMembersElected = totalNumMembersElected.add(1); @@ -900,7 +994,7 @@ contract Election is uint256 value, address lesser, address greater - ) internal onlyL1 { + ) internal { if (votes.total.eligible.contains(group)) { uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); @@ -911,9 +1005,11 @@ contract Election is emit EpochRewardsDistributedToVoters(group, value); } - function _activate(address group, address account) internal returns (bool) { + function _activate(address group, address account) internal onlyWhenNotBlocked returns (bool) { PendingVote storage pendingVote = votes.pending.forGroup[group].byAccount[account]; - require(pendingVote.epoch < getEpochNumber(), "Pending vote epoch not passed"); + + require(pendingVote.epoch < _getEpochNumber(), "Pending vote epoch not passed"); + uint256 value = pendingVote.value; require(value > 0, "Vote value cannot be zero"); decrementPendingVotes(group, account, value); @@ -928,7 +1024,7 @@ contract Election is address lesser, address greater, uint256 index - ) internal returns (bool) { + ) internal onlyWhenNotBlocked returns (bool) { // TODO(asa): Dedup with revokePending. require(group != address(0), "Group address zero"); address account = getAccounts().voteSignerToAccount(msg.sender); @@ -969,7 +1065,7 @@ contract Election is address lesser, address greater, uint256 index - ) internal returns (uint256) { + ) internal onlyWhenNotBlocked returns (uint256) { uint256 remainingValue = maxValue; uint256 pendingVotes = getPendingVotesForGroupByAccount(group, account); if (pendingVotes > 0) { @@ -1061,7 +1157,7 @@ contract Election is PendingVote storage pendingVote = groupPending.byAccount[account]; pendingVote.value = pendingVote.value.add(value); - pendingVote.epoch = getEpochNumber(); + pendingVote.epoch = _getEpochNumber(); } /** @@ -1182,4 +1278,17 @@ contract Election is value.mul(votes.active.forGroup[group].total).div(votes.active.forGroup[group].totalUnits); } } + + /** + * @notice Returns the epoch number. + * @return Current epoch number. + */ + function _getEpochNumber() private view returns (uint256) { + // TODO remove this after L2 is fully implemented + if (isL2()) { + return getEpochManager().getCurrentEpochNumber(); + } else { + return getEpochNumber(); + } + } } diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index 1d399b4cc46..c55d3d74335 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -3,12 +3,13 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "../common/CalledByVm.sol"; +import "./interfaces/IEpochRewards.sol"; import "../common/FixidityLib.sol"; import "../common/Freezable.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; import "../common/UsingPrecompiles.sol"; +import "../common/interfaces/ICeloToken.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; /** @@ -16,12 +17,12 @@ import "../common/interfaces/ICeloVersionedContract.sol"; */ contract EpochRewards is ICeloVersionedContract, + IEpochRewards, Ownable, Initializable, UsingPrecompiles, UsingRegistry, - Freezable, - CalledByVm + Freezable { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; @@ -84,6 +85,14 @@ contract EpochRewards is event TargetVotingYieldUpdated(uint256 fraction); + modifier onlyVmOrPermitted(address permittedAddress) { + if (isL2()) require(msg.sender == permittedAddress, "Only permitted address can call"); + else { + require(msg.sender == address(0), "Only VM can call"); + } + _; + } + /** * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization @@ -143,7 +152,11 @@ contract EpochRewards is * voting Gold fraction. * @dev Only called directly by the protocol. */ - function updateTargetVotingYield() external onlyVm onlyWhenNotFrozen onlyL1 { + function updateTargetVotingYield() + external + onlyVmOrPermitted(registry.getAddressFor(EPOCH_MANAGER_REGISTRY_ID)) + onlyWhenNotFrozen + { _updateTargetVotingYield(); } @@ -273,7 +286,7 @@ contract EpochRewards is * @param value The percentage of the total reward to be sent to the community funds. * @return True upon success. */ - function setCommunityRewardFraction(uint256 value) public onlyOwner onlyL1 returns (bool) { + function setCommunityRewardFraction(uint256 value) public onlyOwner returns (bool) { require( value != communityRewardFraction.unwrap() && value < FixidityLib.fixed1().unwrap(), "Value must be different from existing community reward fraction and less than 1" @@ -289,10 +302,7 @@ contract EpochRewards is * @param value The percentage of the total reward to be sent to the carbon offsetting partner. * @return True upon success. */ - function setCarbonOffsettingFund( - address partner, - uint256 value - ) public onlyOwner onlyL1 returns (bool) { + function setCarbonOffsettingFund(address partner, uint256 value) public onlyOwner returns (bool) { require( partner != carbonOffsettingPartner || value != carbonOffsettingFraction.unwrap(), "Partner and value must be different from existing carbon offsetting fund" @@ -309,7 +319,7 @@ contract EpochRewards is * @param value The percentage of floating Gold voting to target. * @return True upon success. */ - function setTargetVotingGoldFraction(uint256 value) public onlyOwner onlyL1 returns (bool) { + function setTargetVotingGoldFraction(uint256 value) public onlyOwner returns (bool) { require(value != targetVotingGoldFraction.unwrap(), "Target voting gold fraction unchanged"); require( value < FixidityLib.fixed1().unwrap(), @@ -325,7 +335,7 @@ contract EpochRewards is * @param value The value in Celo Dollars. * @return True upon success. */ - function setTargetValidatorEpochPayment(uint256 value) public onlyOwner onlyL1 returns (bool) { + function setTargetValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { require(value != targetValidatorEpochPayment, "Target validator epoch payment unchanged"); targetValidatorEpochPayment = value; emit TargetValidatorEpochPaymentSet(value); @@ -345,7 +355,7 @@ contract EpochRewards is uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor - ) public onlyOwner onlyL1 returns (bool) { + ) public onlyOwner returns (bool) { require( max != rewardsMultiplierParams.max.unwrap() || overspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.overspend.unwrap() || @@ -372,7 +382,7 @@ contract EpochRewards is function setTargetVotingYieldParameters( uint256 max, uint256 adjustmentFactor - ) public onlyOwner onlyL1 returns (bool) { + ) public onlyOwner returns (bool) { require( max != targetVotingYieldParams.max.unwrap() || adjustmentFactor != targetVotingYieldParams.adjustmentFactor.unwrap(), @@ -394,7 +404,7 @@ contract EpochRewards is * @param targetVotingYield The relative target block reward for voters. * @return True upon success. */ - function setTargetVotingYield(uint256 targetVotingYield) public onlyOwner onlyL1 returns (bool) { + function setTargetVotingYield(uint256 targetVotingYield) public onlyOwner returns (bool) { FixidityLib.Fraction memory target = FixidityLib.wrap(targetVotingYield); require( target.lte(targetVotingYieldParams.max), @@ -441,6 +451,12 @@ contract EpochRewards is function getTargetTotalEpochPaymentsInGold() public view returns (uint256) { address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); + if (isL2()) { + return + getEpochManager().getElected().length.mul(targetValidatorEpochPayment).mul(denominator).div( + numerator + ); + } return numberValidatorsInCurrentSet().mul(targetValidatorEpochPayment).mul(denominator).div( numerator @@ -452,7 +468,9 @@ contract EpochRewards is * @return The fraction of floating Gold being used for voting in validator elections. */ function getVotingGoldFraction() public view returns (uint256) { - uint256 liquidGold = getCeloToken().totalSupply().sub(getReserve().getReserveGoldBalance()); + uint256 liquidGold = ICeloToken(address(getCeloToken())).allocatedSupply().sub( + getReserve().getReserveGoldBalance() + ); uint256 votingGold = getElection().getTotalVotes(); return FixidityLib.newFixed(votingGold).divide(FixidityLib.newFixed(liquidGold)).unwrap(); } @@ -501,8 +519,8 @@ contract EpochRewards is uint256 targetGoldSupplyIncrease ) internal view returns (FixidityLib.Fraction memory) { uint256 targetSupply = getTargetGoldTotalSupply(); - uint256 totalSupply = getCeloToken().totalSupply(); - uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(totalSupply.add(targetGoldSupplyIncrease)); + uint256 allocatedSupply = ICeloToken(address(getCeloToken())).allocatedSupply(); + uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(allocatedSupply.add(targetGoldSupplyIncrease)); uint256 targetRemainingSupply = GOLD_SUPPLY_CAP.sub(targetSupply); FixidityLib.Fraction memory remainingToTargetRatio = FixidityLib .newFixed(remainingSupply) diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 4d2db3449b2..3344421ab42 100755 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -7,6 +7,7 @@ import "openzeppelin-solidity/contracts/utils/Address.sol"; import "openzeppelin-solidity/contracts/utils/EnumerableSet.sol"; import "./interfaces/ILockedGold.sol"; +import "./interfaces/ILockedGoldInitializer.sol"; import "../common/FixidityLib.sol"; import "../common/Initializable.sol"; @@ -14,15 +15,17 @@ import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/libraries/ReentrancyGuard.sol"; +import "../common/Blockable.sol"; contract LockedGold is ILockedGold, + ILockedGoldInitializer, ICeloVersionedContract, ReentrancyGuard, Initializable, - UsingRegistry + UsingRegistry, + Blockable { - // TODO add initializer using SafeMath for uint256; using Address for address payable; // prettier-ignore using FixidityLib for FixidityLib.Fraction; @@ -469,7 +472,7 @@ contract LockedGold is address[] calldata lessers, address[] calldata greaters, uint256[] calldata indices - ) external onlySlasher { + ) external onlySlasher onlyWhenNotBlocked { uint256 maxSlash = Math.min(penalty, getAccountTotalLockedGold(account)); require(maxSlash >= reward, "reward cannot exceed penalty."); // `reporter` receives the reward in locked CELO, so it must be given to an account @@ -502,6 +505,13 @@ contract LockedGold is emit AccountSlashed(account, maxSlash, reporter, reward); } + /// @notice Sets the address of the blocking contract. + /// @param _blockedBy The address of the contract that will determine if this contract is blocked. + /// @dev Can only be called by the owner of the contract. + function setBlockedByContract(address _blockedBy) external onlyOwner { + _setBlockedBy(_blockedBy); + } + /** * @notice Returns the total amount of locked gold in the system. Note that this does not include * gold that has been unlocked but not yet withdrawn. diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index 5ba6aee6502..42fd7a7225f 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -29,7 +29,9 @@ interface IElection { // view functions function electValidatorSigners() external view returns (address[] memory); + function electValidatorAccounts() external view returns (address[] memory); function electNValidatorSigners(uint256, uint256) external view returns (address[] memory); + function electNValidatorAccounts(uint256, uint256) external view returns (address[] memory); function getElectableValidators() external view returns (uint256, uint256); function getElectabilityThreshold() external view returns (uint256); function getNumVotesReceivable(address) external view returns (uint256); @@ -49,6 +51,11 @@ interface IElection { uint256, uint256[] calldata ) external view returns (uint256); + function getGroupEpochRewardsBasedOnScore( + address group, + uint256 totalEpochRewards, + uint256 groupScore + ) external view returns (uint256); function getGroupsVotedForByAccount(address) external view returns (address[] memory); function getEligibleValidatorGroups() external view returns (address[] memory); function getTotalVotesForEligibleValidatorGroups() @@ -60,4 +67,5 @@ interface IElection { function hasActivatablePendingVotes(address, address) external view returns (bool); function validatorSignerAddressFromCurrentSet(uint256 index) external view returns (address); function numberValidatorsInCurrentSet() external view returns (uint256); + function owner() external view returns (address); } diff --git a/packages/protocol/contracts/governance/interfaces/IEpochRewards.sol b/packages/protocol/contracts/governance/interfaces/IEpochRewards.sol index 29001315df5..1c32d669933 100644 --- a/packages/protocol/contracts/governance/interfaces/IEpochRewards.sol +++ b/packages/protocol/contracts/governance/interfaces/IEpochRewards.sol @@ -1,6 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; interface IEpochRewards { + function updateTargetVotingYield() external; + function isReserveLow() external view returns (bool); + function calculateTargetEpochRewards() external view returns (uint256, uint256, uint256, uint256); + function getTargetVotingYieldParameters() external view returns (uint256, uint256, uint256); + function getRewardsMultiplierParameters() external view returns (uint256, uint256, uint256); function getCommunityRewardFraction() external view returns (uint256); function getCarbonOffsettingFraction() external view returns (uint256); + function getTargetVotingGoldFraction() external view returns (uint256); + function getRewardsMultiplier() external view returns (uint256); + function carbonOffsettingPartner() external view returns (address); } diff --git a/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol b/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol index 7db68b0ba0c..cfa19772a8d 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol @@ -20,6 +20,7 @@ interface ILockedCelo { ) external; function addSlasher(string calldata slasherIdentifier) external; + function getAccountNonvotingLockedGold(address account) external view returns (uint256); function getAccountTotalLockedCelo(address) external view returns (uint256); function getTotalLockedCelo() external view returns (uint256); function getPendingWithdrawals( diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 6d8a0f92e28..8d771efec6c 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -7,6 +7,7 @@ interface IValidators { bytes calldata, bytes calldata ) external returns (bool); + function registerValidatorNoBls(bytes calldata ecdsaPublicKey) external returns (bool); function deregisterValidator(uint256) external returns (bool); function affiliate(address) external returns (bool); function deaffiliate() external returns (bool); @@ -29,6 +30,7 @@ interface IValidators { function setGroupLockedGoldRequirements(uint256, uint256) external returns (bool); function setValidatorLockedGoldRequirements(uint256, uint256) external returns (bool); function setSlashingMultiplierResetPeriod(uint256) external; + function setDowntimeGracePeriod(uint256 value) external; // only registered contract function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool); @@ -39,6 +41,7 @@ interface IValidators { bytes calldata, bytes calldata ) external returns (bool); + function mintStableToEpochManager(uint256 amount) external; // only VM function updateValidatorScoreFromSigner(address, uint256) external; @@ -49,6 +52,8 @@ interface IValidators { function halveSlashingMultiplier(address) external; // view functions + function maxGroupSize() external view returns (uint256); + function downtimeGracePeriod() external view returns (uint256); function getCommissionUpdateDelay() external view returns (uint256); function getValidatorScoreParameters() external view returns (uint256, uint256); function getMembershipHistory( @@ -62,6 +67,7 @@ interface IValidators { function getValidator( address account ) external view returns (bytes memory, bytes memory, address, uint256, address); + function getValidatorsGroup(address account) external view returns (address affiliation); function getValidatorGroup( address ) @@ -70,6 +76,7 @@ interface IValidators { returns (address[] memory, uint256, uint256, uint256, uint256[] memory, uint256, uint256); function getGroupNumMembers(address) external view returns (uint256); function getTopGroupValidators(address, uint256) external view returns (address[] memory); + function getTopGroupValidatorsAccounts(address, uint256) external view returns (address[] memory); function getGroupsNumMembers( address[] calldata accounts ) external view returns (uint256[] memory); @@ -85,4 +92,10 @@ interface IValidators { function getValidatorGroupSlashingMultiplier(address) external view returns (uint256); function getMembershipInLastEpoch(address) external view returns (address); function getMembershipInLastEpochFromSigner(address) external view returns (address); + function computeEpochReward( + address account, + uint256 score, + uint256 maxPayment + ) external view returns (uint256); + function getMembershipHistoryLength() external view returns (uint256); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol b/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol index c8c8964e8ed..21c68bb975b 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol @@ -1,4 +1,5 @@ pragma solidity >=0.5.13 <0.9.0; +pragma experimental ABIEncoderV2; interface IValidatorsInitializer { function initialize( @@ -12,7 +13,14 @@ interface IValidatorsInitializer { uint256 _membershipHistoryLength, uint256 _slashingMultiplierResetPeriod, uint256 _maxGroupSize, - uint256 _commissionUpdateDelay, - uint256 _downtimeGracePeriod + InitParamsLib.InitParams calldata initParams ) external; } + +library InitParamsLib { + struct InitParams { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } +} diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index 395dec5b252..b2b6034383a 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -17,7 +17,7 @@ contract MockElection is IsL2Check { isIneligible[account] = true; } - function markGroupEligible(address account, address, address) external onlyL1 { + function markGroupEligible(address account, address, address) external { isEligible[account] = true; } @@ -33,11 +33,11 @@ contract MockElection is IsL2Check { electedValidators = _electedValidators; } - function vote(address, uint256, address, address) external onlyL1 returns (bool) { + function vote(address, uint256, address, address) external returns (bool) { return true; } - function activate(address) external onlyL1 returns (bool) { + function activate(address) external returns (bool) { return true; } @@ -76,11 +76,14 @@ contract MockElection is IsL2Check { return 0; } - function electValidatorSigners() external view returns (address[] memory) { + function electValidatorSigners() external view onlyL1 returns (address[] memory) { + return electedValidators; + } + function electValidators() external view onlyL2 returns (address[] memory) { return electedValidators; } - function setAllowedToVoteOverMaxNumberOfGroups(address account, bool flag) public onlyL1 { + function setAllowedToVoteOverMaxNumberOfGroups(address account, bool flag) public { allowedToVoteOverMaxNumberOfGroups[account] = flag; } } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 97cf017a9a3..d47f44b447c 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -2,12 +2,16 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../interfaces/IValidators.sol"; import "../../../contracts-0.8/common/IsL2Check.sol"; +// Mocks Validators, compatible with 0.5 +// For forge tests, can be avoided with calls to deployCodeTo + /** * @title Holds a list of addresses of validators */ -contract MockValidators is IsL2Check { +contract MockValidators is IValidators, IsL2Check { using SafeMath for uint256; uint256 private constant FIXED1_UINT = 1000000000000000000000000; @@ -19,10 +23,10 @@ contract MockValidators is IsL2Check { mapping(address => bool) private doesNotMeetAccountLockedGoldRequirements; mapping(address => address[]) private members; mapping(address => address) private affiliations; + mapping(address => uint256) private commissions; uint256 private numRegisteredValidators; function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool) { - allowOnlyL1(); return true; } @@ -46,7 +50,6 @@ contract MockValidators is IsL2Check { } function affiliate(address group) external returns (bool) { - allowOnlyL1(); affiliations[msg.sender] = group; return true; } @@ -61,6 +64,13 @@ contract MockValidators is IsL2Check { function setMembers(address group, address[] calldata _members) external { members[group] = _members; + for (uint256 i; i < _members.length; i++) { + affiliations[_members[i]] = group; + } + } + + function setCommission(address group, uint256 commission) external { + commissions[group] = commission; } function setAccountLockedGoldRequirement(address account, uint256 value) external { @@ -75,16 +85,26 @@ contract MockValidators is IsL2Check { allowOnlyL1(); } - function getTopGroupValidators( + function getValidatorsGroup(address validator) external view returns (address) { + return affiliations[validator]; + } + + function getTopGroupValidatorsAccounts( address group, uint256 n ) external view returns (address[] memory) { - require(n <= members[group].length); - address[] memory validators = new address[](n); - for (uint256 i = 0; i < n; i = i.add(1)) { - validators[i] = members[group][i]; - } - return validators; + return getTopGroupValidators(group, n); + } + + function getValidatorGroup( + address group + ) + external + view + returns (address[] memory, uint256, uint256, uint256, uint256[] memory, uint256, uint256) + { + uint256[] memory sizeHistory; + return (members[group], commissions[group], 0, 0, sizeHistory, 0, 0); } function getValidatorGroupSlashingMultiplier(address) external view returns (uint256) { @@ -117,11 +137,193 @@ contract MockValidators is IsL2Check { } function groupMembershipInEpoch(address addr, uint256, uint256) external view returns (address) { - allowOnlyL1(); return affiliations[addr]; } function getGroupNumMembers(address group) public view returns (uint256) { return members[group].length; } + + function getTopGroupValidators(address group, uint256 n) public view returns (address[] memory) { + require(n <= members[group].length); + address[] memory validators = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + validators[i] = members[group][i]; + } + return validators; + } + + // Not implemented in mock, added here to support the interface + // without the interface, missing function erros get hard to debug + + function addFirstMember(address, address, address) external returns (bool) { + revert("Method not implemented in mock"); + } + + function registerValidatorGroup(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function registerValidatorNoBls(bytes calldata ecdsaPublicKey) external returns (bool) { + revert("Method not implemented in mock"); + } + function removeMember(address) external returns (bool) { + revert("Method not implemented in mock"); + } + function setGroupLockedGoldRequirements(uint256, uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + function setMembershipHistoryLength(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + function setNextCommissionUpdate(uint256) external { + revert("Method not implemented in mock"); + } + function setSlashingMultiplierResetPeriod(uint256) external { + revert("Method not implemented in mock"); + } + + function updateCommission() external { + revert("Method not implemented in mock"); + } + + function updateBlsPublicKey(bytes calldata, bytes calldata) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setValidatorScoreParameters(uint256, uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setValidatorLockedGoldRequirements(uint256, uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setMaxGroupSize(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setDowntimeGracePeriod(uint256 value) external { + revert("Method not implemented in mock"); + } + + function setCommissionUpdateDelay(uint256) external { + revert("Method not implemented in mock"); + } + + function resetSlashingMultiplier() external { + revert("Method not implemented in mock"); + } + + function reorderMember(address, address, address) external returns (bool) { + revert("Method not implemented in mock"); + } + + function updateValidatorScoreFromSigner(address, uint256) external { + revert("Method not implemented in mock"); + } + + function mintStableToEpochManager(uint256 amount) external { + revert("Method not implemented in mock"); + } + + function maxGroupSize() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function getValidatorScoreParameters() external view returns (uint256, uint256) { + revert("Method not implemented in mock"); + } + + function getValidatorLockedGoldRequirements() external view returns (uint256, uint256) { + revert("Method not implemented in mock"); + } + + function getValidatorBlsPublicKeyFromSigner(address) external view returns (bytes memory) { + revert("Method not implemented in mock"); + } + + function getRegisteredValidators() external view returns (address[] memory) { + revert("Method not implemented in mock"); + } + + function getRegisteredValidatorGroups() external view returns (address[] memory) { + revert("Method not implemented in mock"); + } + + function getMembershipInLastEpochFromSigner(address) external view returns (address) { + revert("Method not implemented in mock"); + } + + function getMembershipInLastEpoch(address) external view returns (address) { + revert("Method not implemented in mock"); + } + + function getMembershipHistoryLength() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function addMember(address) external returns (bool) { + revert("Method not implemented in mock"); + } + + function calculateEpochScore(uint256) external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function deaffiliate() external returns (bool) { + revert("Method not implemented in mock"); + } + + function deregisterValidator(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function deregisterValidatorGroup(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function distributeEpochPaymentsFromSigner(address, uint256) external onlyL1 returns (uint256) { + revert("Method not implemented in mock"); + } + + function downtimeGracePeriod() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function getCommissionUpdateDelay() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function getGroupLockedGoldRequirements() external view returns (uint256, uint256) { + revert("Method not implemented in mock"); + } + + function computeEpochReward( + address account, + uint256 score, + uint256 maxPayment + ) external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function registerValidator( + bytes calldata, + bytes calldata, + bytes calldata + ) external returns (bool) { + revert("Method not implemented in mock"); + } + + function getMembershipHistory( + address + ) external view returns (uint256[] memory, address[] memory, uint256, uint256) { + revert("Method not implemented in mock"); + } + + function getValidator( + address account + ) external view returns (bytes memory, bytes memory, address, uint256, address) { + revert("Method not implemented in mock"); + } } diff --git a/packages/protocol/contracts/governance/test/ValidatorsMock.sol b/packages/protocol/contracts/governance/test/ValidatorsMock.sol deleted file mode 100644 index ab9557badec..00000000000 --- a/packages/protocol/contracts/governance/test/ValidatorsMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -pragma solidity ^0.5.13; - -import "../Validators.sol"; -import "../../common/FixidityLib.sol"; - -/** - * @title A wrapper around Validators that exposes onlyVm functions for testing. - */ -contract ValidatorsMock is Validators(true) { - function updateValidatorScoreFromSigner(address signer, uint256 uptime) external { - return _updateValidatorScoreFromSigner(signer, uptime); - } - - function distributeEpochPaymentsFromSigner( - address signer, - uint256 maxPayment - ) external returns (uint256) { - return _distributeEpochPaymentsFromSigner(signer, maxPayment); - } -} diff --git a/packages/protocol/contracts/stability/test/MockSortedOracles.sol b/packages/protocol/contracts/stability/test/MockSortedOracles.sol index d19ce120b89..2a18a3c85d9 100644 --- a/packages/protocol/contracts/stability/test/MockSortedOracles.sol +++ b/packages/protocol/contracts/stability/test/MockSortedOracles.sol @@ -32,17 +32,23 @@ contract MockSortedOracles { return _numRates[token]; } - function medianRate(address token) external view returns (uint256, uint256) { - if (numerators[token] > 0) { - return (numerators[token], DENOMINATOR); - } - return (0, 0); + function getExchangeRate( + address token + ) external view returns (uint256 numerator, uint256 denominator) { + (numerator, denominator) = medianRate(token); } function setOldestReportExpired(address token) public { expired[token] = true; } + function medianRate(address token) public view returns (uint256, uint256) { + if (numerators[token] > 0) { + return (numerators[token], DENOMINATOR); + } + return (0, 0); + } + function isOldestReportExpired(address token) public view returns (bool, address) { return (expired[token], token); } diff --git a/packages/protocol/foundry.toml b/packages/protocol/foundry.toml index 1557a1e57f8..c934ff7bbbe 100644 --- a/packages/protocol/foundry.toml +++ b/packages/protocol/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -src = 'contracts' +src = 'contracts-0.8' out = 'out' test = 'test-sol' libs = ['lib', 'node_modules'] @@ -17,6 +17,7 @@ remappings = [ 'forge-std-8/=lib/celo-foundry-8/lib/forge-std/src/', '@summa-tx/memview.sol/=lib/memview.sol', 'solidity-bytes-utils/=lib/solidity-bytes-utils/', + 'solidity-bytes-utils-8/=lib/solidity-bytes-utils-8/', 'ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/', ] @@ -36,6 +37,7 @@ fs_permissions = [ ] [profile.devchain] # Special profile for the tests that require an anvil devchain +test = 'test-sol/devchain' match_path = "**/test-sol/devchain/**" no_match_path = "{**/test/BLS12Passthrough.sol,**/test/RandomTest.sol}" diff --git a/packages/protocol/governanceConstitution.js b/packages/protocol/governanceConstitution.js index 601bda35862..fa0e0ea0be7 100644 --- a/packages/protocol/governanceConstitution.js +++ b/packages/protocol/governanceConstitution.js @@ -135,6 +135,7 @@ const DefaultConstitution = { setValidatorLockedGoldRequirements: 0.8, setSlashingMultiplierResetPeriod: 0.7, setValidatorScoreParameters: 0.7, + __contractPackage: contractPackages.SOLIDITY_08_PACKAGE, }, } diff --git a/packages/protocol/lib/artifactsSingleton.ts b/packages/protocol/lib/artifactsSingleton.ts index 3d475431836..03de766b5ff 100644 --- a/packages/protocol/lib/artifactsSingleton.ts +++ b/packages/protocol/lib/artifactsSingleton.ts @@ -6,33 +6,37 @@ export interface ArtifactSet { getProxy(key: string): any; } -function getProxyName(contractName:string){ +function getProxyName(contractName: string) { return contractName + "Proxy"; } // This class is meant to be used to wrap truffle artifacts // and extend its interface. // ArtifactsSingleton.wrap returns an instance of DefaultArtifact -export class DefaultArtifact implements ArtifactSet{ +export class DefaultArtifact implements ArtifactSet { public artifacts: any - + public constructor(artifacts) { this.artifacts = artifacts } - + public require(key: string) { return this.artifacts.require(key) } - + public getProxy(key: string) { return this.require(getProxyName(key)) } - + + public contains(key: string) { + return this.artifacts.require(key) !== undefined + } + } // This objects replicates a Truffle `artifacts.require` singleton // but constructed manually -export class ArtifactsSingleton implements ArtifactSet{ +export class ArtifactsSingleton implements ArtifactSet { public static setNetwork(network: any) { this.network = network } @@ -56,8 +60,8 @@ export class ArtifactsSingleton implements ArtifactSet{ return ArtifactsSingleton.instances[namespace] } - public static wrap(artifacts:any){ - if (artifacts instanceof ArtifactsSingleton || artifacts instanceof DefaultArtifact){ + public static wrap(artifacts: any) { + if (artifacts instanceof ArtifactsSingleton || artifacts instanceof DefaultArtifact) { return artifacts } @@ -70,22 +74,29 @@ export class ArtifactsSingleton implements ArtifactSet{ public artifacts: { [key: string]: any } = {} - private constructor() {} + private constructor() { } public addArtifact(key: string, value: any) { this.artifacts[key] = value } - public require(key: string) { - return this.artifacts[key] + public require(key: string, defaultArtifacts?: any) { + if (key in this.artifacts) { + return this.artifacts[key] + } + return defaultArtifacts?.require(key) + } + + public contains(key: string) { + return key in this.artifacts } - public getProxy(key: string, defaultArtifacts?:any) { + public getProxy(key: string, defaultArtifacts?: any) { const proxyArtifactName = getProxyName(key) const toReturn = this.require(proxyArtifactName) - if (toReturn === undefined){ + if (toReturn === undefined) { // in case the package of this artifact has proxiesPath set // this needs to be changed to support it, now only "/" path is supported return defaultArtifacts?.require(proxyArtifactName) diff --git a/packages/protocol/lib/compatibility/verify-bytecode.ts b/packages/protocol/lib/compatibility/verify-bytecode.ts index b5dfdb59f26..56d6d9d0ee8 100644 --- a/packages/protocol/lib/compatibility/verify-bytecode.ts +++ b/packages/protocol/lib/compatibility/verify-bytecode.ts @@ -21,7 +21,12 @@ let ignoredContracts = [ // These contracts are not in the Registry (before release 1) 'ReserveSpenderMultiSig', - 'GovernanceApproverMultiSig' + 'GovernanceApproverMultiSig', + + // These contracts live in monorepo but are not part of the core protocol + 'CeloFeeCurrencyAdapterOwnable', + 'FeeCurrencyAdapter', + 'FeeCurrencyAdapterOwnable', ] interface VerificationContext { diff --git a/packages/protocol/lib/proxy-utils.ts b/packages/protocol/lib/proxy-utils.ts index a2bd042271e..a24bd1fe688 100644 --- a/packages/protocol/lib/proxy-utils.ts +++ b/packages/protocol/lib/proxy-utils.ts @@ -42,27 +42,33 @@ export async function setAndInitializeImplementation( }, ...args: any[] ) { - const callData = web3.eth.abi.encodeFunctionCall(initializerAbi, args) - if (txOptions.from != null) { - // The proxied contract needs to be funded prior to initialization - if (txOptions.value != null) { - // Proxy's fallback fn expects the contract's implementation to be set already - // So we set the implementation first, send the funding, and then set and initialize again. - await retryTx(proxy._setImplementation, [implementationAddress, { from: txOptions.from }]) - await retryTx(web3.eth.sendTransaction, [ - { - from: txOptions.from, - to: proxy.address, - value: txOptions.value, - }, + try { + + + const callData = web3.eth.abi.encodeFunctionCall(initializerAbi, args) + if (txOptions.from != null) { + // The proxied contract needs to be funded prior to initialization + if (txOptions.value != null) { + // Proxy's fallback fn expects the contract's implementation to be set already + // So we set the implementation first, send the funding, and then set and initialize again. + await retryTx(proxy._setImplementation, [implementationAddress, { from: txOptions.from }]) + await retryTx(web3.eth.sendTransaction, [ + { + from: txOptions.from, + to: proxy.address, + value: txOptions.value, + }, + ]) + } + return retryTx(proxy._setAndInitializeImplementation, [ + implementationAddress, + callData as any, + { from: txOptions.from }, ]) + } else { + return retryTx(proxy._setAndInitializeImplementation, [implementationAddress, callData as any]) } - return retryTx(proxy._setAndInitializeImplementation, [ - implementationAddress, - callData as any, - { from: txOptions.from }, - ]) - } else { - return retryTx(proxy._setAndInitializeImplementation, [implementationAddress, callData as any]) + } catch (error) { + console.log("errror", error); } } diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index 23ca9572507..36777a976ef 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -19,6 +19,9 @@ export enum CeloContractName { DowntimeSlasher = 'DowntimeSlasher', Election = 'Election', EpochRewards = 'EpochRewards', + EpochManagerEnabler = 'EpochManagerEnabler', + EpochManager = 'EpochManager', + ScoreManager = 'ScoreManager', Escrow = 'Escrow', Exchange = 'Exchange', ExchangeEUR = 'ExchangeEUR', @@ -37,7 +40,7 @@ export enum CeloContractName { GrandaMento = 'GrandaMento', LockedGold = 'LockedGold', LockedCelo = 'LockedCelo', - CeloDistributionSchedule = 'CeloDistributionSchedule', + CeloUnreleasedTreasury = 'CeloUnreleasedTreasury', OdisPayments = 'OdisPayments', Random = 'Random', Reserve = 'Reserve', diff --git a/packages/protocol/lib/solidity-bytes-utils-8 b/packages/protocol/lib/solidity-bytes-utils-8 new file mode 160000 index 00000000000..df88556cbbc --- /dev/null +++ b/packages/protocol/lib/solidity-bytes-utils-8 @@ -0,0 +1 @@ +Subproject commit df88556cbbc267b33a787a3a6eaa32fd7247b589 diff --git a/packages/protocol/lib/web3-utils.ts b/packages/protocol/lib/web3-utils.ts index 127c09ff6d5..ebaae5d256a 100644 --- a/packages/protocol/lib/web3-utils.ts +++ b/packages/protocol/lib/web3-utils.ts @@ -207,13 +207,17 @@ export async function _setInitialProxyImplementation< return receipt.tx } +export const getProxiedContract = async (contractName: string, contractPackage: ContractPackage) => { + const artifactsObject = ArtifactsSingleton.getInstance(contractPackage, artifacts) + /* eslint-disable-next-line */ + return await getDeployedProxiedContract(contractName, artifactsObject) +} + export async function getDeployedProxiedContract( contractName: string, customArtifacts: any ): Promise { - const Contract: Truffle.Contract = customArtifacts.require(contractName) - let Proxy: ProxyContract // this wrap avoids a lot of rewrite const overloadedArtifact = ArtifactsSingleton.wrap(customArtifacts) @@ -271,11 +275,11 @@ export const makeTruffleContractForMigrationWithoutSingleton = (contractName: st const artifact = require(`${path.join(__dirname, "..")}/build/contracts-${contractPath}/${contractName}.json`) const Contract = truffleContract({ + contractName: artifact.contractName, abi: artifact.abi, unlinked_binary: artifact.bytecode, }) - Contract.setProvider(web3.currentProvider) Contract.setNetwork(network.network_id) @@ -292,9 +296,14 @@ export const makeTruffleContractForMigrationWithoutSingleton = (contractName: st export const makeTruffleContractForMigration = (contractName: string, contractPath: ContractPackage, web3: Web3) => { + const singleton = ArtifactsSingleton.getInstance(contractPath) + if (singleton.contains(contractName)) { + return singleton.require(contractName) + } + const network = ArtifactsSingleton.getNetwork() const Contract = makeTruffleContractForMigrationWithoutSingleton(contractName, network, contractPath.name, web3) - ArtifactsSingleton.getInstance(contractPath).addArtifact(contractName, Contract) + singleton.addArtifact(contractName, Contract) return Contract } @@ -382,7 +391,7 @@ export async function transferOwnershipOfProxy( export async function transferOwnershipOfProxyAndImplementation< ContractInstance extends OwnableInstance >(contractName: string, owner: string, artifacts: any) { - console.info(` Transferring ownership of ${contractName} and its Proxy to ${owner}`) + console.info(`Transferring ownership of ${contractName} and its Proxy to ${owner}`) const contract: ContractInstance = await getDeployedProxiedContract( contractName, artifacts diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 1522bd1be88..79045a72c5d 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -77,6 +77,9 @@ const DefaultConfig = { carbonOffsettingFraction: 1 / 1000, frozen: false, }, + epochManager: { + newEpochDuration: 100, + }, exchange: { spread: 5 / 1000, reserveFraction: 1 / 100, @@ -165,6 +168,9 @@ const DefaultConfig = { numRequiredConfirmations: 1, numInternalRequiredConfirmations: 1, }, + scoreManager: { + newEpochDuration: 100, + }, stableToken: { decimals: 18, goldPrice: 1, @@ -581,7 +587,7 @@ NetworkConfigs.mainnet = NetworkConfigs.rc1 const linkedLibraries = { Proposals: ['Governance'], - AddressLinkedList: ['Validators', 'ValidatorsMock'], + AddressLinkedList: ['Validators'], AddressSortedLinkedList: ['Election', 'ElectionTest'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListMock'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianMock'], diff --git a/packages/protocol/migrations_sol/CONTRIBUTING.md b/packages/protocol/migrations_sol/CONTRIBUTING.md index 5524a3a8858..6fcbdbedfae 100644 --- a/packages/protocol/migrations_sol/CONTRIBUTING.md +++ b/packages/protocol/migrations_sol/CONTRIBUTING.md @@ -32,7 +32,7 @@ Starts a new anvil devchain serving at localhost (default port 8546). You can now run commands against the local devchain. For example: ```sh -# Call `isL2()` on `CeloDistributionSchedule.sol` +# Call `isL2()` on `CeloUnreleasedTreasury.sol` cast call \ 0xA16cF67AFa80BB9Ce7a325597F80057c6B290fD4 \ "isL2()(bool)" \ diff --git a/packages/protocol/migrations_sol/Migration.s.sol b/packages/protocol/migrations_sol/Migration.s.sol index 5a328003685..aaa9303e3fa 100644 --- a/packages/protocol/migrations_sol/Migration.s.sol +++ b/packages/protocol/migrations_sol/Migration.s.sol @@ -27,6 +27,7 @@ import "@celo-contracts/common/interfaces/IFeeHandler.sol"; import "@celo-contracts/common/interfaces/IFeeHandlerInitializer.sol"; import "@celo-contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; import "@celo-contracts/common/interfaces/IAccounts.sol"; +import "@celo-contracts/common/interfaces/IEpochManagerEnabler.sol"; import "@celo-contracts/governance/interfaces/ILockedGoldInitializer.sol"; import "@celo-contracts/governance/interfaces/IValidatorsInitializer.sol"; import "@celo-contracts/governance/interfaces/IElectionInitializer.sol"; @@ -49,10 +50,15 @@ import "@celo-contracts/stability/interfaces/ISortedOracles.sol"; // Core contract imports on Solidity 0.8 import "@celo-contracts-8/common/interfaces/IFeeCurrencyDirectoryInitializer.sol"; import "@celo-contracts-8/common/interfaces/IGasPriceMinimumInitializer.sol"; -import "@celo-contracts-8/common/interfaces/ICeloDistributionScheduleInitializer.sol"; +import "@celo-contracts-8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IEpochManagerEnablerInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IEpochManagerInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IScoreManagerInitializer.sol"; import "@celo-contracts-8/common/interfaces/IFeeCurrencyDirectory.sol"; import "@celo-contracts-8/common/UsingRegistry.sol"; +import "@test-sol/utils/SECP256K1.sol"; + contract ForceTx { // event to trigger so a tx can be processed event VanillaEvent(string); @@ -67,6 +73,12 @@ contract ForceTx { contract Migration is Script, UsingRegistry, MigrationsConstants { using stdJson for string; + struct InitParamsTunnel { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } + IProxyFactory proxyFactory; uint256 proxyNonce = 0; @@ -227,7 +239,10 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { migrateUniswapFeeHandlerSeller(); migrateFeeHandler(json); migrateOdisPayments(); - migrateCeloDistributionSchedule(); + migrateCeloUnreleasedTreasury(); + migrateEpochManagerEnabler(); + migrateEpochManager(json); + migrateScoreManager(); migrateGovernance(json); vm.stopBroadcast(); @@ -235,6 +250,12 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { // Functions with broadcast with different addresses // Validators needs to lock, which can be only used by the msg.sender electValidators(json); + + vm.startBroadcast(DEPLOYER_ACCOUNT); + + captureEpochManagerEnablerValidators(); + + vm.stopBroadcast(); } /** @@ -606,6 +627,11 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { (uint256) ); + InitParamsTunnel memory initParamsTunnel = InitParamsTunnel({ + commissionUpdateDelay: commissionUpdateDelay, + downtimeGracePeriod: downtimeGracePeriod + }); + deployProxiedContract( "Validators", abi.encodeWithSelector( @@ -620,8 +646,7 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { membershipHistoryLength, slashingMultiplierResetPeriod, maxGroupSize, - commissionUpdateDelay, - downtimeGracePeriod + initParamsTunnel ) ); } @@ -910,16 +935,46 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { ); } - function migrateCeloDistributionSchedule() public { + function migrateCeloUnreleasedTreasury() public { deployProxiedContract( - "CeloDistributionSchedule", + "CeloUnreleasedTreasury", abi.encodeWithSelector( - ICeloDistributionScheduleInitializer.initialize.selector, + ICeloUnreleasedTreasuryInitializer.initialize.selector, REGISTRY_ADDRESS ) ); } + function migrateEpochManagerEnabler() public { + deployProxiedContract( + "EpochManagerEnabler", + abi.encodeWithSelector(IEpochManagerEnablerInitializer.initialize.selector, REGISTRY_ADDRESS) + ); + } + + function migrateScoreManager() public { + deployProxiedContract( + "ScoreManager", + abi.encodeWithSelector(IScoreManagerInitializer.initialize.selector) + ); + } + + function migrateEpochManager(string memory json) public { + address newEpochDuration = abi.decode( + json.parseRaw(".epochManager.newEpochDuration"), + (address) + ); + + deployProxiedContract( + "EpochManager", + abi.encodeWithSelector( + IEpochManagerInitializer.initialize.selector, + REGISTRY_ADDRESS, + newEpochDuration + ) + ); + } + function migrateGovernance(string memory json) public { bool useApprover = abi.decode(json.parseRaw(".governanceApproverMultiSig.required"), (bool)); @@ -1050,14 +1105,12 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { function registerValidator( uint256 validatorIndex, - bytes memory ecdsaPubKey, uint256 validatorKey, uint256 amountToLock, address groupToAffiliate ) public returns (address) { vm.startBroadcast(validatorKey); lockGold(amountToLock); - bytes memory _ecdsaPubKey = ecdsaPubKey; address accountAddress = (new ForceTx()).identity(); // these blobs are not checked in the contract @@ -1072,29 +1125,23 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { bytes16(0x05050505050505050505050505050506), bytes16(0x06060606060606060606060606060607) ); + + (bytes memory ecdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner(accountAddress, validatorKey); getValidators().registerValidator(ecdsaPubKey, newBlsPublicKey, newBlsPop); getValidators().affiliate(groupToAffiliate); - console.log("Done registering validatora"); + console.log("Done registering validators"); vm.stopBroadcast(); return accountAddress; } function getValidatorKeyIndex( + uint256 groupCount, uint256 groupIndex, uint256 validatorIndex, uint256 membersInAGroup ) public returns (uint256) { - return groupIndex * membersInAGroup + validatorIndex + 1; - } - - function getValidatorKeyFromGroupGroup( - uint256[] memory keys, - uint256 groupIndex, - uint256 validatorIndex, - uint256 membersInAGroup - ) public returns (uint256) { - return keys[getValidatorKeyIndex(groupIndex, validatorIndex, membersInAGroup)]; + return groupCount + groupIndex * membersInAGroup + validatorIndex; } function registerValidatorGroup( @@ -1113,6 +1160,57 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { vm.stopBroadcast(); } + function _generateEcdsaPubKeyWithSigner( + address _validator, + uint256 _signerPk + ) internal returns (bytes memory ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) { + (v, r, s) = getParsedSignatureOfAddress(_validator, _signerPk); + + bytes32 addressHash = keccak256(abi.encodePacked(_validator)); + + ecdsaPubKey = addressToPublicKey(addressHash, v, r, s); + } + + function addressToPublicKey( + bytes32 message, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public returns (bytes memory) { + address SECP256K1Address = actor("SECP256K1Address"); + deployCodeTo("SECP256K1.sol:SECP256K1", SECP256K1Address); + ISECP256K1 sECP256K1 = ISECP256K1(SECP256K1Address); + + string memory header = "\x19Ethereum Signed Message:\n32"; + bytes32 _message = keccak256(abi.encodePacked(header, message)); + (uint256 x, uint256 y) = sECP256K1.recover( + uint256(_message), + _v - 27, + uint256(_r), + uint256(_s) + ); + return abi.encodePacked(x, y); + } + + function actor(string memory name) internal returns (address) { + return vm.addr(uint256(keccak256(abi.encodePacked(name)))); + } + + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } + + function getParsedSignatureOfAddress( + address _address, + uint256 privateKey + ) public pure returns (uint8, bytes32, bytes32) { + bytes32 addressHash = keccak256(abi.encodePacked(_address)); + bytes32 prefixedHash = toEthSignedMessageHash(addressHash); + return vm.sign(privateKey, prefixedHash); + } + function electValidators(string memory json) public { console.log("Electing validators: "); @@ -1127,7 +1225,6 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { json.parseRaw(".validators.validatorLockedGoldRequirements.value"), (uint256) ); - bytes[] memory ecdsaPubKeys = abi.decode(json.parseRaw(".validators.ecdsaPubKeys"), (bytes[])); // attestationKeys not migrated if (valKeys.length == 0) { @@ -1140,52 +1237,76 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { ); } - uint256 validatorGroup0Key = valKeys[0]; + uint256 groupCount = 3; + console.log("groupCount", groupCount); - address groupAddress = registerValidatorGroup( - validatorGroup0Key, - maxGroupSize * validatorLockedGoldRequirements, - commission, - json - ); + address[] memory groups = new address[](groupCount); - console.log(" * Registering ${group.valKeys.length} validators ..."); + // register 3 validator groups + for (uint256 groupIndex = 0; groupIndex < groupCount; groupIndex++) { + address groupAddress = registerValidatorGroup( + valKeys[groupIndex], + maxGroupSize * validatorLockedGoldRequirements, + commission, + json + ); + groups[groupIndex] = groupAddress; + console.log("registered group: ", groupAddress); + } + + console.log(" * Registering validators ... Count: ", valKeys.length - groupCount); // Split the validator keys into groups that will fit within the max group size. - uint256 amountOfGroups = Math.ceilDiv(valKeys.length, maxGroupSize); // TODO change name of variable amount of groups for amount in group - for (uint256 validatorIndex = 0; validatorIndex < amountOfGroups; validatorIndex++) { - console.log("Validator key index", getValidatorKeyIndex(0, validatorIndex, maxGroupSize)); - console.log("Registering validator #: ", validatorIndex); - bytes memory ecdsaPubKey = ecdsaPubKeys[ - getValidatorKeyIndex(0, validatorIndex, maxGroupSize) - ]; - address validator = registerValidator( - validatorIndex, - ecdsaPubKey, - getValidatorKeyFromGroupGroup(valKeys, 0, validatorIndex, maxGroupSize), - validatorLockedGoldRequirements, - groupAddress - ); - // TODO start broadcast - console.log("Adding to group..."); - - vm.startBroadcast(validatorGroup0Key); - if (validatorIndex == 0) { - getValidators().addFirstMember(validator, address(0), address(0)); - console.log("Making group vote for itself"); - getElection().vote( - groupAddress, - getLockedGold().getAccountNonvotingLockedGold(groupAddress), - address(0), - address(0) + for (uint256 groupIndex = 0; groupIndex < groupCount; groupIndex++) { + address groupAddress = groups[groupIndex]; + console.log("Registering members for group: ", groupAddress); + for (uint256 validatorIndex = 0; validatorIndex < maxGroupSize; validatorIndex++) { + uint256 validatorKeyIndex = getValidatorKeyIndex( + groupCount, + groupIndex, + validatorIndex, + maxGroupSize ); - } else { - // unimplemented - console.log("WARNING: case not implemented"); - } + console.log("Registering validator #: ", validatorIndex); + address validator = registerValidator( + validatorIndex, + valKeys[validatorKeyIndex], + validatorLockedGoldRequirements, + groupAddress + ); + // TODO start broadcast + console.log("Adding to group..."); + + vm.startBroadcast(groups[groupIndex]); + address greater = groupIndex == 0 ? address(0) : groups[groupIndex - 1]; + + if (validatorIndex == 0) { + getValidators().addFirstMember(validator, address(0), greater); + console.log("Making group vote for itself"); + } else { + getValidators().addMember(validator); + } + getElection().vote(groupAddress, validatorLockedGoldRequirements, address(0), greater); - vm.stopBroadcast(); + vm.stopBroadcast(); + } } } + + function captureEpochManagerEnablerValidators() public { + address numberValidatorsInCurrentSetPrecompileAddress = 0x00000000000000000000000000000000000000f9; + numberValidatorsInCurrentSetPrecompileAddress.call( + abi.encodeWithSignature("setNumberOfValidators()") + ); + + address validatorSignerAddressFromCurrentSetPrecompileAddress = 0x00000000000000000000000000000000000000fa; + validatorSignerAddressFromCurrentSetPrecompileAddress.call( + abi.encodeWithSignature("setValidators()") + ); + + address epochManagerEnabler = registry.getAddressForString("EpochManagerEnabler"); + IEpochManagerEnabler epochManagerEnablerContract = IEpochManagerEnabler(epochManagerEnabler); + epochManagerEnablerContract.captureEpochAndValidators(); + } } diff --git a/packages/protocol/migrations_sol/MigrationL2.s.sol b/packages/protocol/migrations_sol/MigrationL2.s.sol index a85d4ba4522..decd5ebc374 100644 --- a/packages/protocol/migrations_sol/MigrationL2.s.sol +++ b/packages/protocol/migrations_sol/MigrationL2.s.sol @@ -3,8 +3,12 @@ pragma solidity >=0.8.7 <0.8.20; import { Script } from "forge-std-8/Script.sol"; import { MigrationsConstants } from "@migrations-sol/constants.sol"; +// Foundry imports +import "forge-std/console.sol"; + import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts-8/common/UsingRegistry.sol"; +import "../../contracts/common/interfaces/IEpochManagerEnabler.sol"; contract MigrationL2 is Script, MigrationsConstants, UsingRegistry { using FixidityLib for FixidityLib.Fraction; @@ -16,7 +20,9 @@ contract MigrationL2 is Script, MigrationsConstants, UsingRegistry { vm.startBroadcast(DEPLOYER_ACCOUNT); setupUsingRegistry(); - activateCeloDistributionSchedule(); + dealToCeloUnreleasedTreasury(); + + initializeEpochManagerSystem(); vm.stopBroadcast(); } @@ -26,17 +32,19 @@ contract MigrationL2 is Script, MigrationsConstants, UsingRegistry { setRegistry(REGISTRY_ADDRESS); } - function activateCeloDistributionSchedule() public { - uint256 l2StartTime = 1721909903 - 5; // Arbitrarily 5 seconds before last black - uint256 communityRewardFraction = getEpochRewards().getCommunityRewardFraction(); - address carbonOffsettingPartner = 0x22579CA45eE22E2E16dDF72D955D6cf4c767B0eF; - uint256 carbonOffsettingFraction = getEpochRewards().getCarbonOffsettingFraction(); - - getCeloDistributionSchedule().activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction + function dealToCeloUnreleasedTreasury() public { + vm.deal(address(getCeloUnreleasedTreasury()), L2_INITIAL_STASH_BALANCE); + } + + function initializeEpochManagerSystem() public { + console.log("Initializing EpochManager system"); + address[] memory firstElected = getValidators().getRegisteredValidators(); + IEpochManager epochManager = getEpochManager(); + address epochManagerEnablerAddress = registry.getAddressForOrDie( + EPOCH_MANAGER_ENABLER_REGISTRY_ID ); + + IEpochManagerEnabler epochManagerEnabler = IEpochManagerEnabler(epochManagerEnablerAddress); + epochManagerEnabler.initEpochManager(); } } diff --git a/packages/protocol/migrations_sol/constants.sol b/packages/protocol/migrations_sol/constants.sol index e08b2a5e734..c47449a5e1c 100644 --- a/packages/protocol/migrations_sol/constants.sol +++ b/packages/protocol/migrations_sol/constants.sol @@ -7,15 +7,17 @@ contract MigrationsConstants is TestConstants { address constant DEPLOYER_ACCOUNT = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; // List of contracts that are expected to be in Registry.sol - string[24] contractsInRegistry = [ + string[27] contractsInRegistry = [ "Accounts", "BlockchainParameters", - "CeloDistributionSchedule", + "CeloUnreleasedTreasury", "CeloToken", "DoubleSigningSlasher", "DowntimeSlasher", "Election", "EpochRewards", + "EpochManagerEnabler", + "EpochManager", "Escrow", "FederatedAttestations", "FeeCurrencyWhitelist", @@ -31,6 +33,7 @@ contract MigrationsConstants is TestConstants { "SortedOracles", "UniswapFeeHandlerSeller", "MentoFeeHandlerSeller", - "Validators" + "Validators", + "ScoreManager" ]; } diff --git a/packages/protocol/migrations_sol/migrationsConfig.json b/packages/protocol/migrations_sol/migrationsConfig.json index 54c5e208267..7edaee535bf 100644 --- a/packages/protocol/migrations_sol/migrationsConfig.json +++ b/packages/protocol/migrations_sol/migrationsConfig.json @@ -1,5 +1,5 @@ { - "goldToken": {"frozen":false}, + "goldToken": { "frozen": false }, "sortedOracles": { "reportExpirySeconds": 300 }, @@ -21,9 +21,23 @@ "0x246f4599eFD3fA67AC44335Ed5e749E518Ffd8bB", "0x298FbD6dad2Fc2cB56d7E37d8aCad8Bf07324f67" ], - "assetAllocationSymbols_": ["cGLD", "BTC", "ETH", "DAI", "They are don't by converting string to hex, and then `cast to-bytes32`"], - "assetAllocationSymbols": ["0x63474c4400000000000000000000000000000000000000000000000000000000", "0x4254430000000000000000000000000000000000000000000000000000000000", "0x4554480000000000000000000000000000000000000000000000000000000000", "0x4441490000000000000000000000000000000000000000000000000000000000"], - "assetAllocationWeights": [500000000000000000000000, 300000000000000000000000, 150000000000000000000000, 50000000000000000000000], + "assetAllocationSymbols_": [ + "cGLD", + "BTC", + "ETH", + "DAI", + "They are don't by converting string to hex, and then `cast to-bytes32`" + ], + "assetAllocationSymbols": [ + "0x63474c4400000000000000000000000000000000000000000000000000000000", + "0x4254430000000000000000000000000000000000000000000000000000000000", + "0x4554480000000000000000000000000000000000000000000000000000000000", + "0x4441490000000000000000000000000000000000000000000000000000000000" + ], + "assetAllocationWeights": [ + 500000000000000000000000, 300000000000000000000000, 150000000000000000000000, + 50000000000000000000000 + ], "initialBalance": 5000000000000000000000000 }, "stableTokens": { @@ -39,7 +53,7 @@ }, "exchange": { "spread": 5000000000000000000000, - "reserveFraction": 10000000000000000000000, + "reserveFraction": 10000000000000000000000, "updateFrequency": 300, "minimumReports": 1, "frozen": false @@ -64,10 +78,10 @@ "adjustmentSpeed": 100000000000000000000000 }, "membershipHistoryLength": 60, - "commissionUpdateDelay": 51840, - "commissionUpdateDelay_help": "(3 * DAY) / 5", + "commissionUpdateDelay": 51840, + "commissionUpdateDelay_help": "(3 * DAY) / 5", - "maxGroupSize": 5, + "maxGroupSize": 2, "slashingMultiplierResetPeriod": 2592000, "slashingPenaltyResetPeriod_help": "30 * DAY", "downtimeGracePeriod": 0, @@ -76,10 +90,17 @@ "groupName": "cLabs", "commission": 100000000000000000000000, "votesRatioOfLastVsFirstGroup": 2000000000000000000000000, - "valKeys": ["0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d","0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"], - "ecdsaPubKeys": [ - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "0x9d9031e97dd78ff8c15aa86939de9b1e791066a0224e331bc962a2099a7b1f0464b8bbafe1535f2301c72c2cb3535b172da30b02686ab0393d348614f157fbdb"] + "valKeys": [ + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", + "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", + "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba", + "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e", + "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356", + "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97", + "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + ] }, "election": { "minElectableValidators": 1, @@ -89,14 +110,14 @@ }, "epochRewards": { "targetVotingYieldParameters": { - "initial": 0, - "max": 500000000000000000000, + "initial": 160000000000000000000, + "max": 500000000000000000000, "max_helper": "(x + 1) ^ 365 = 1.20", - "adjustmentFactor": 0, + "adjustmentFactor": 0, "adjustmentFactor_helper": "Change to 1 / 3650 once Mainnet activated." }, "rewardsMultiplierParameters": { - "max": 2, + "max": 2000000000000000000000000, "adjustmentFactors": { "underspend": 500000000000000000000000, "overspend": 5000000000000000000000000 @@ -106,10 +127,13 @@ "maxValidatorEpochPayment": 205479452054794520547, "maxValidatorEpochPayment_helper": "(75,000 / 365) * 10 ^ 18", "communityRewardFraction": 250000000000000000000000, - "carbonOffsettingPartner": "0x0000000000000000000000000000000000000000", + "carbonOffsettingPartner": "0xD533Ca259b330c7A88f74E000a3FaEa2d63B7972", "carbonOffsettingFraction": 1000000000000000000000, "frozen": false }, + "epochManager": { + "newEpochDuration": 86400 + }, "random": { "randomnessBlockRetentionWindow": "720", "randomnessBlockRetentionWindow_helper": "HOUR / 5" @@ -147,7 +171,7 @@ "dequeueFrequency": 14400, "queueExpiry": 2419200, "queueExpiry_helper": "4 * WEEK", - "dequeueFrequency_helper":"4 * HOUR", + "dequeueFrequency_helper": "4 * HOUR", "approvalStageDuration": 14400, "approvalStageDuration_helper": "4 * HOUR", "referendumStageDuration": 86400, @@ -162,4 +186,4 @@ "skipSetConstitution": false, "skipTransferOwnership": false } -} \ No newline at end of file +} diff --git a/packages/protocol/migrations_ts/01_libraries.ts b/packages/protocol/migrations_ts/01_libraries.ts index 3cb33f15561..6dd5b4047e7 100644 --- a/packages/protocol/migrations_ts/01_libraries.ts +++ b/packages/protocol/migrations_ts/01_libraries.ts @@ -1,10 +1,21 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { ArtifactsSingleton } from '@celo/protocol/lib/artifactsSingleton' +import { makeTruffleContractForMigration } from '@celo/protocol/lib/web3-utils' import { linkedLibraries } from '@celo/protocol/migrationsConfig' module.exports = (deployer: any) => { Object.keys(linkedLibraries).forEach((lib: string) => { - const Library = artifacts.require(lib) + const artifacts08 = ArtifactsSingleton.getInstance(SOLIDITY_08_PACKAGE, artifacts) + + for (const contractName of SOLIDITY_08_PACKAGE.contracts) { + makeTruffleContractForMigration(contractName, SOLIDITY_08_PACKAGE, web3) + } + + const Library = artifacts08.require(lib, artifacts) deployer.deploy(Library) - const Contracts = linkedLibraries[lib].map((contract: string) => artifacts.require(contract)) + const Contracts = linkedLibraries[lib].map((contract: string) => + artifacts08.require(contract, artifacts) + ) deployer.link(Library, Contracts) }) } diff --git a/packages/protocol/migrations_ts/13_validators.ts b/packages/protocol/migrations_ts/13_validators.ts index 2cc51d2ae48..fc29dfdd635 100644 --- a/packages/protocol/migrations_ts/13_validators.ts +++ b/packages/protocol/migrations_ts/13_validators.ts @@ -2,7 +2,8 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' import { toFixed } from '@celo/utils/lib/fixidity' -import { ValidatorsInstance } from 'types' +import { ValidatorsInstance } from 'types/08' +import { SOLIDITY_08_PACKAGE } from '../contractPackages' const initializeArgs = async (): Promise => { return [ @@ -16,8 +17,10 @@ const initializeArgs = async (): Promise => { config.validators.membershipHistoryLength, config.validators.slashingPenaltyResetPeriod, config.validators.maxGroupSize, - config.validators.commissionUpdateDelay, - config.validators.downtimeGracePeriod, + { + commissionUpdateDelay: config.validators.commissionUpdateDelay, + downtimeGracePeriod: config.validators.downtimeGracePeriod, + }, ] } @@ -25,5 +28,7 @@ module.exports = deploymentForCoreContract( web3, artifacts, CeloContractName.Validators, - initializeArgs + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE ) diff --git a/packages/protocol/migrations_ts/26_101_score_manager.ts b/packages/protocol/migrations_ts/26_101_score_manager.ts new file mode 100644 index 00000000000..93a74670975 --- /dev/null +++ b/packages/protocol/migrations_ts/26_101_score_manager.ts @@ -0,0 +1,17 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { ScoreManagerInstance } from 'types/08' + +const initializeArgs = async (): Promise => { + return [] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.ScoreManager, + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE +) diff --git a/packages/protocol/migrations_ts/26_102_epoch_manager_enabler.ts b/packages/protocol/migrations_ts/26_102_epoch_manager_enabler.ts new file mode 100644 index 00000000000..6a9a3d9220f --- /dev/null +++ b/packages/protocol/migrations_ts/26_102_epoch_manager_enabler.ts @@ -0,0 +1,18 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { EpochManagerEnablerInstance } from 'types/08' + +const initializeArgs = async (): Promise => { + return [config.registry.predeployedProxyAddress] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.EpochManagerEnabler, + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE +) diff --git a/packages/protocol/migrations_ts/26_103_epoch_manager.ts b/packages/protocol/migrations_ts/26_103_epoch_manager.ts new file mode 100644 index 00000000000..4956ebcc139 --- /dev/null +++ b/packages/protocol/migrations_ts/26_103_epoch_manager.ts @@ -0,0 +1,18 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { EpochManagerInstance } from 'types/08' + +const initializeArgs = async (): Promise => { + return [config.registry.predeployedProxyAddress, config.epochManager.newEpochDuration] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.EpochManager, + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE +) diff --git a/packages/protocol/migrations_ts/28_celoDistributionSchedule.ts b/packages/protocol/migrations_ts/28_celo_unreleased_treasury.ts similarity index 75% rename from packages/protocol/migrations_ts/28_celoDistributionSchedule.ts rename to packages/protocol/migrations_ts/28_celo_unreleased_treasury.ts index 7496fff67fa..81f0c6d1f7d 100644 --- a/packages/protocol/migrations_ts/28_celoDistributionSchedule.ts +++ b/packages/protocol/migrations_ts/28_celo_unreleased_treasury.ts @@ -4,7 +4,7 @@ import { getDeployedProxiedContract, } from '@celo/protocol/lib/web3-utils' import { RegistryInstance } from '@celo/protocol/types' -import { CeloDistributionScheduleInstance } from 'types/08' +import { CeloUnreleasedTreasuryInstance } from '@celo/protocol/types/08' import { SOLIDITY_08_PACKAGE } from '../contractPackages' const initializeArgs = async (): Promise<[string]> => { @@ -15,10 +15,10 @@ const initializeArgs = async (): Promise<[string]> => { return [registry.address] } -module.exports = deploymentForCoreContract( +module.exports = deploymentForCoreContract( web3, artifacts, - CeloContractName.CeloDistributionSchedule, + CeloContractName.CeloUnreleasedTreasury, initializeArgs, undefined, SOLIDITY_08_PACKAGE diff --git a/packages/protocol/migrations_ts/29_governance.ts b/packages/protocol/migrations_ts/29_governance.ts index 56845d78a8d..634f711a5ac 100644 --- a/packages/protocol/migrations_ts/29_governance.ts +++ b/packages/protocol/migrations_ts/29_governance.ts @@ -113,7 +113,6 @@ module.exports = deploymentForCoreContract( 'Random', 'Registry', 'SortedOracles', - 'Validators', ], }, { @@ -130,7 +129,13 @@ module.exports = deploymentForCoreContract( __contractPackage: MENTO_PACKAGE, }, { - contracts: ['GasPriceMinimum'], + contracts: [ + 'GasPriceMinimum', + 'Validators', + 'EpochManager', + 'ScoreManager', + 'EpochManagerEnabler', + ], __contractPackage: SOLIDITY_08_PACKAGE, }, ] diff --git a/packages/protocol/migrations_ts/30_elect_validators.ts b/packages/protocol/migrations_ts/30_elect_validators.ts index aaf0fd1424a..17d35a0b6c7 100644 --- a/packages/protocol/migrations_ts/30_elect_validators.ts +++ b/packages/protocol/migrations_ts/30_elect_validators.ts @@ -1,6 +1,8 @@ import { NULL_ADDRESS } from '@celo/base/lib/address' import { CeloTxObject } from '@celo/connect' import { getBlsPoP, getBlsPublicKey } from '@celo/cryptographic-utils/lib/bls' +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { ArtifactsSingleton } from '@celo/protocol/lib/artifactsSingleton' import { getDeployedProxiedContract, sendTransactionWithPrivateKey, @@ -10,7 +12,8 @@ import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/addr import { toFixed } from '@celo/utils/lib/fixidity' import { signMessage } from '@celo/utils/lib/signatureUtils' import { BigNumber } from 'bignumber.js' -import { AccountsInstance, ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' +import { AccountsInstance, ElectionInstance, LockedGoldInstance } from 'types' +import { ValidatorsInstance } from 'types/08' import Web3 from 'web3' const truffle = require('@celo/protocol/truffle-config.js') @@ -221,6 +224,8 @@ async function registerValidator( } module.exports = async (_deployer: any, networkName: string) => { + const artifacts08 = ArtifactsSingleton.getInstance(SOLIDITY_08_PACKAGE, artifacts) + const accounts: AccountsInstance = await getDeployedProxiedContract( 'Accounts', artifacts @@ -228,7 +233,7 @@ module.exports = async (_deployer: any, networkName: string) => { const validators: ValidatorsInstance = await getDeployedProxiedContract( 'Validators', - artifacts + artifacts08 ) const lockedGold: LockedGoldInstance = await getDeployedProxiedContract( diff --git a/packages/protocol/package.json b/packages/protocol/package.json index fde24a0e13b..e9210652a2c 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -10,7 +10,7 @@ "lint:sol": "solhint --version && solhint './contracts/**/*.sol' && solhint './contracts-0.8/**/*.sol'", "lint": "yarn run lint:ts && yarn run lint:sol", "clean": "rm -rf ./types/typechain && rm -rf build/* && rm -rf .0x-artifacts/* && rm -rf migrations/*.js* && rm -rf migrations_ts/*.js* && rm -rf test/**/*.js* && rm -f lib/*.js* && rm -f lib/**/*.js* && rm -f scripts/*.js* && yarn clean:foundry", - "clean:foundry": "rm -rf cache out", + "clean:foundry": "forge clean && rm -rf cache out", "test": "rm test/**/*.js ; node runTests.js", "test:scripts": "yarn ts-node scripts/run-scripts-tests.ts --testPathPattern=scripts/", "quicktest": "./scripts/bash/quicktest.sh", @@ -109,6 +109,7 @@ "solhint": "^4.5.4", "semver": "^7.5.4", "solidity-bytes-utils": "0.0.7", + "solidity-bytes-utils-8": "npm:solidity-bytes-utils@^0.8.2", "truffle": "5.9.0", "truffle-security": "^1.7.3", "weak-map": "^1.0.5", @@ -151,4 +152,4 @@ "typechain-target-ethers-v5": "^5.0.1", "yargs": "^14.0.0" } -} \ No newline at end of file +} diff --git a/packages/protocol/releaseData/initializationData/release12.json b/packages/protocol/releaseData/initializationData/release12.json index b9814f49cf1..1b67eec0e45 100644 --- a/packages/protocol/releaseData/initializationData/release12.json +++ b/packages/protocol/releaseData/initializationData/release12.json @@ -1,4 +1,7 @@ { - "FeeCurrencyDirectory": [], - "CeloDistributionSchedule": ["0x000000000000000000000000000000000000ce10"] + "CeloUnreleasedTreasury": ["0x000000000000000000000000000000000000ce10"], + "EpochManager": ["0x000000000000000000000000000000000000ce10", 86400], + "EpochManagerEnabler": ["0x000000000000000000000000000000000000ce10"], + "ScoreManager": [], + "FeeCurrencyDirectory": [] } diff --git a/packages/protocol/remappings.txt b/packages/protocol/remappings.txt index 8722ca0c8b0..58f99cbe97a 100644 --- a/packages/protocol/remappings.txt +++ b/packages/protocol/remappings.txt @@ -1,4 +1,4 @@ @celo-contracts=contracts/ @celo-contracts-8=contracts-0.8/ @test-sol=test-sol -@lib=lib \ No newline at end of file +@lib=lib diff --git a/packages/protocol/scripts/bash/contract-exclusion-regex.sh b/packages/protocol/scripts/bash/contract-exclusion-regex.sh index a92f61da77f..c233d4aa287 100644 --- a/packages/protocol/scripts/bash/contract-exclusion-regex.sh +++ b/packages/protocol/scripts/bash/contract-exclusion-regex.sh @@ -3,7 +3,7 @@ set -euo pipefail # Exclude test contracts, mock contracts, contract interfaces, Proxy contracts, inlined libraries, # MultiSig contracts, and the ReleaseGold contract. -CONTRACT_EXCLUSION_REGEX=".*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles" +CONTRACT_EXCLUSION_REGEX=".*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles|CeloFeeCurrencyAdapterOwnable|FeeCurrencyAdapter|FeeCurrencyAdapterOwnable" # Before CR7, UsingRegistry and UsingRegistryV2 had been deployed, they need to keep getting deployed to keep the release reports without changes. VERSION_NUMBER=$(echo "$BRANCH" | tr -dc '0-9') diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index fafb06ab9cc..a4455fe650d 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -39,7 +39,7 @@ function compile({ coreContractsOnly, solidity: outdir }: BuildTargets) { } exec( - `yarn run truffle compile --silent --contracts_directory=${contractPath} --contracts_build_directory=${outdir}/contracts-${contractPackage.name} --config ${contractPackage.truffleConfig}` // todo change to outdir + `yarn run truffle compile --silent --contracts_directory=${contractPath} --contracts_build_directory=${outdir}/contracts-${contractPackage.name} --config ${contractPackage.truffleConfig} --verbose-rpc` // todo change to outdir ) } diff --git a/packages/protocol/scripts/consts.ts b/packages/protocol/scripts/consts.ts index d599e87950e..af001078776 100644 --- a/packages/protocol/scripts/consts.ts +++ b/packages/protocol/scripts/consts.ts @@ -36,7 +36,7 @@ export const ProxyContracts = [ 'RegistryProxy', 'SortedOraclesProxy', 'UniswapFeeHandlerSellerProxy', - 'CeloDistributionScheduleProxy', + 'CeloUnreleasedTreasuryProxy', ] export const CoreContracts = [ @@ -52,11 +52,13 @@ export const CoreContracts = [ 'MultiSig', 'Registry', 'Freezer', - 'CeloDistributionSchedule', + 'CeloUnreleasedTreasury', // governance 'Election', 'EpochRewards', + 'EpochManager', + 'EpochManagerEnabler', 'Governance', 'GovernanceApproverMultiSig', 'BlockchainParameters', @@ -65,6 +67,7 @@ export const CoreContracts = [ 'LockedGold', 'Validators', 'ReleaseGold', + 'ScoreManager', // identity 'Attestations', diff --git a/packages/protocol/scripts/foundry/constants.sh b/packages/protocol/scripts/foundry/constants.sh index 17db16c51ef..4ecf41ad136 100755 --- a/packages/protocol/scripts/foundry/constants.sh +++ b/packages/protocol/scripts/foundry/constants.sh @@ -40,12 +40,12 @@ export CARBON_OFFSETTING_FRACTION="10000000000000000000" # 0.001 in fixidity for export REGISTRY_STORAGE_LOCATION="0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103" # Position is bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); GOLD_TOKEN_CELO_SUPPLY_CAP=1000000000 # `GoldToken.CELO_SUPPLY_CAP()` GOLD_TOKEN_TOTAL_SUPPLY=700000000 # Arbitrary amount chosen to be approximately equal to `GoldToken.totalSupply()` on the L1 Mainnet (695,313,643 CELO as of this commit). -export CELO_DISTRIBUTION_SCHEDULE_INITIAL_BALANCE="$(($GOLD_TOKEN_CELO_SUPPLY_CAP - $GOLD_TOKEN_TOTAL_SUPPLY))" # During the real L2 genesis, the VM will calculate and set an appropriate balance. +export CELO_UNRELEASED_TREASURY_INITIAL_BALANCE="$(($GOLD_TOKEN_CELO_SUPPLY_CAP - $GOLD_TOKEN_TOTAL_SUPPLY))" # During the real L2 genesis, the VM will calculate and set an appropriate balance. # Contract libraries export LIBRARIES_PATH=("contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian" "contracts/common/Signatures.sol:Signatures" - "contracts/common/linkedlists/AddressLinkedList.sol:AddressLinkedList" + "contracts-0.8/common/linkedlists/AddressLinkedList.sol:AddressLinkedList" "contracts/common/linkedlists/AddressSortedLinkedList.sol:AddressSortedLinkedList" "contracts/common/linkedlists/IntegerSortedLinkedList.sol:IntegerSortedLinkedList" "contracts/governance/Proposals.sol:Proposals" @@ -53,11 +53,14 @@ export LIBRARIES_PATH=("contracts/common/linkedlists/AddressSortedLinkedListWith export LIBRARY_DEPENDENCIES_PATH=( "contracts/common/FixidityLib.sol" "contracts/common/linkedlists/LinkedList.sol" + "contracts-0.8/common/linkedlists/LinkedList.sol" "contracts/common/linkedlists/SortedLinkedList.sol" "contracts/common/linkedlists/SortedLinkedListWithMedian.sol" "lib/openzeppelin-contracts/contracts/math/SafeMath.sol" + "lib/openzeppelin-contracts8/contracts/utils/math/SafeMath.sol" "lib/openzeppelin-contracts/contracts/math/Math.sol" - "lib/openzeppelin-contracts/contracts/cryptography/ECDSA.sol" + "lib/openzeppelin-contracts/contracts/cryptography/ECDSA.sol" "lib/openzeppelin-contracts/contracts/utils/Address.sol" "lib/solidity-bytes-utils/contracts/BytesLib.sol" + "lib/celo-foundry/lib/forge-std/src/console.sol" ) diff --git a/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh b/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh index e529e384808..a77c48f81ff 100755 --- a/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh +++ b/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh @@ -29,8 +29,8 @@ echo "Library flags are: $LIBRARY_FLAGS" # Build all contracts with deployed libraries # Including contracts that depend on libraries. This step replaces the library placeholder # in the bytecode with the address of the actually deployed library. -echo "Compiling with libraries... " -time forge build $LIBRARY_FLAGS +echo "Compiling with libraries..." +time FOUNDRY_PROFILE=devchain forge build $LIBRARY_FLAGS # Deploy precompile contracts source $PWD/scripts/foundry/deploy_precompiles.sh @@ -58,11 +58,29 @@ forge script \ $SKIP_SIMULATION \ $NON_INTERACTIVE \ $LIBRARY_FLAGS \ - --rpc-url $ANVIL_RPC_URL || echo "Migration script failed" + --rpc-url $ANVIL_RPC_URL || { echo "Migration script failed"; exit 1; } + +CELO_EPOCH_REWARDS_ADDRESS=$( + cast call \ + $REGISTRY_ADDRESS \ + "getAddressForStringOrDie(string calldata identifier)(address)" \ + "EpochRewards" \ + --rpc-url $ANVIL_RPC_URL +) + +echo "Setting storage of EpochRewards start time to same value as on mainnet" +# Storage slot of start time is 2 and the value is 1587587214 which is identical to mainnet +cast rpc \ +anvil_setStorageAt \ +$CELO_EPOCH_REWARDS_ADDRESS 2 "0x000000000000000000000000000000000000000000000000000000005ea0a88e" \ +--rpc-url $ANVIL_RPC_URL # Keeping track of the finish time to measure how long it takes to run the script entirely ELAPSED_TIME=$(($SECONDS - $START_TIME)) -echo "Total elapsed time: $ELAPSED_TIME seconds" +echo "Migration script total elapsed time: $ELAPSED_TIME seconds" + +# this helps to make sure that devchain state is actually being saved +sleep 1 if [[ "${KEEP_DEVCHAIN_FOLDER:-}" == "true" ]]; then cp $ANVIL_FOLDER/state.json $TMP_FOLDER/$L1_DEVCHAIN_FILE_NAME diff --git a/packages/protocol/scripts/foundry/create_and_migrate_anvil_l2_devchain.sh b/packages/protocol/scripts/foundry/create_and_migrate_anvil_l2_devchain.sh index d423ec11970..4f00d37dd7f 100755 --- a/packages/protocol/scripts/foundry/create_and_migrate_anvil_l2_devchain.sh +++ b/packages/protocol/scripts/foundry/create_and_migrate_anvil_l2_devchain.sh @@ -18,22 +18,23 @@ cast rpc anvil_setCode \ --rpc-url $ANVIL_RPC_URL # Fetch address of Celo distribution -CELO_DISTRIBUTION_SCHEDULE_ADDRESS=$( +CELO_UNRELEASED_TREASURY_ADDRESS=$( cast call \ $REGISTRY_ADDRESS \ "getAddressForStringOrDie(string calldata identifier)(address)" \ - "CeloDistributionSchedule" \ + "CeloUnreleasedTreasury" \ --rpc-url $ANVIL_RPC_URL ) -# Set the balance of the CeloDistributionSchedule (like the Celo client would do during L2 genesis) -# Note: This can't be done from the migration script, because CeloDistributionSchedule.sol does not +# Set the balance of the CeloUnreleasedTreasury (like the Celo client would do during L2 genesis) +# Note: This can't be done from the migration script, because CeloUnreleasedTreasury.sol does not # implement the receive function nor does it allow ERC20 transfers. This is the only way I -# managed to give the CeloDistributionSchedule a balance. -echo "Setting CeloDistributionSchedule balance..." +# managed to give the CeloUnreleasedTreasury a balance. +echo "Setting CeloUnreleasedTreasury balance..." +HEX_CELO_UNRELEASED_TREASURY_INITIAL_BALANCE=$(cast to-hex $CELO_UNRELEASED_TREASURY_INITIAL_BALANCE"000000000000000000") cast rpc \ anvil_setBalance \ - $CELO_DISTRIBUTION_SCHEDULE_ADDRESS $CELO_DISTRIBUTION_SCHEDULE_INITIAL_BALANCE \ + $CELO_UNRELEASED_TREASURY_ADDRESS $HEX_CELO_UNRELEASED_TREASURY_INITIAL_BALANCE \ --rpc-url $ANVIL_RPC_URL # Run L2 migrations @@ -47,7 +48,7 @@ forge script \ $BROADCAST \ $SKIP_SIMULATION \ $NON_INTERACTIVE \ - --rpc-url $ANVIL_RPC_URL || echo "L2 Migration script failed" + --rpc-url $ANVIL_RPC_URL || { echo "Migration script failed"; exit 1; } # # Save L2 state so it can published to NPM mv $ANVIL_FOLDER/state.json $TMP_FOLDER/$L2_DEVCHAIN_FILE_NAME diff --git a/packages/protocol/scripts/foundry/deploy_precompiles.sh b/packages/protocol/scripts/foundry/deploy_precompiles.sh index f87b77761f1..1ebbae44a1d 100755 --- a/packages/protocol/scripts/foundry/deploy_precompiles.sh +++ b/packages/protocol/scripts/foundry/deploy_precompiles.sh @@ -13,4 +13,12 @@ cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $EpochSizeAddress $EpochSizeByte ProofOfPossesionAddress=0x00000000000000000000000000000000000000fb ProofOfPossesionBytecode=`cat ./out/ProofOfPossesionPrecompile.sol/ProofOfPossesionPrecompile.json | jq -r '.deployedBytecode.object'` -cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $ProofOfPossesionAddress $ProofOfPossesionBytecode \ No newline at end of file +cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $ProofOfPossesionAddress $ProofOfPossesionBytecode + +NumberValidatorsInCurrentSetPrecompileAddress=0x00000000000000000000000000000000000000f9 +NumberValidatorsInCurrentSetPrecompileBytecode=`cat ./out/NumberValidatorsInCurrentSetPrecompile.sol/NumberValidatorsInCurrentSetPrecompile.json | jq -r '.deployedBytecode.object'` +cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $NumberValidatorsInCurrentSetPrecompileAddress $NumberValidatorsInCurrentSetPrecompileBytecode + +ValidatorSignerAddressFromCurrentSetAddress=0x00000000000000000000000000000000000000fa +ValidatorSignerAddressFromCurrentSetBytecode=`cat ./out/ValidatorSignerAddressFromCurrentSetPrecompile.sol/ValidatorSignerAddressFromCurrentSetPrecompile.json | jq -r '.deployedBytecode.object'` +cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $ValidatorSignerAddressFromCurrentSetAddress $ValidatorSignerAddressFromCurrentSetBytecode diff --git a/packages/protocol/scripts/foundry/run_e2e_tests_in_anvil.sh b/packages/protocol/scripts/foundry/run_e2e_tests_in_anvil.sh index d515b84e5bc..3ecf3824427 100755 --- a/packages/protocol/scripts/foundry/run_e2e_tests_in_anvil.sh +++ b/packages/protocol/scripts/foundry/run_e2e_tests_in_anvil.sh @@ -8,13 +8,14 @@ source $PWD/scripts/foundry/constants.sh echo "Generating and running devchain before running e2e tests..." source $PWD/scripts/foundry/create_and_migrate_anvil_devchain.sh + # Run e2e tests echo "Running e2e tests..." -forge test \ +time FOUNDRY_PROFILE=devchain forge test \ -vvv \ ---match-path "*test-sol/e2e/*" \ +--match-path "*test-sol/devchain/e2e/*" \ --fork-url $ANVIL_RPC_URL # Stop devchain echo "Stopping devchain..." -source $PWD/scripts/foundry/stop_anvil.sh \ No newline at end of file +source $PWD/scripts/foundry/stop_anvil.sh diff --git a/packages/protocol/scripts/foundry/run_integration_tests_in_anvil.sh b/packages/protocol/scripts/foundry/run_integration_tests_in_anvil.sh index 94e23de7489..bdcd06f4f09 100755 --- a/packages/protocol/scripts/foundry/run_integration_tests_in_anvil.sh +++ b/packages/protocol/scripts/foundry/run_integration_tests_in_anvil.sh @@ -10,9 +10,9 @@ source $PWD/scripts/foundry/create_and_migrate_anvil_devchain.sh # Run integration tests echo "Running integration tests..." -forge test \ +time FOUNDRY_PROFILE=devchain forge test \ -vvv \ ---match-contract RegistryIntegrationTest \ +--match-path "test-sol/devchain/migration/*" \ --fork-url $ANVIL_RPC_URL # Stop devchain diff --git a/packages/protocol/scripts/truffle/make-release.ts b/packages/protocol/scripts/truffle/make-release.ts index af7911955b2..440fd8a4566 100644 --- a/packages/protocol/scripts/truffle/make-release.ts +++ b/packages/protocol/scripts/truffle/make-release.ts @@ -118,9 +118,6 @@ const deployImplementation = async ( // without this delay it sometimes fails with ProviderError await delay(getRandomNumber(1, 1000)) - console.log('gas update in2') - console.log('dryRun', dryRun) - const bytecodeSize = (Contract.bytecode.length - 2) / 2 console.log('Bytecode size in bytes:', bytecodeSize) diff --git a/packages/protocol/test-sol/constants.sol b/packages/protocol/test-sol/constants.sol index 60b2254eb3f..b3570fa3f2b 100644 --- a/packages/protocol/test-sol/constants.sol +++ b/packages/protocol/test-sol/constants.sol @@ -23,8 +23,27 @@ contract TestConstants { string constant LockedCeloContract = "LockedCelo"; string constant ValidatorsContract = "Validators"; string constant GovernanceContract = "Governance"; + string constant EpochRewardsContract = "EpochRewards"; + string constant EpochManagerContract = "EpochManager"; + string constant EpochManagerEnablerContract = "EpochManagerEnabler"; + string constant ScoreManagerContract = "ScoreManager"; + string constant ReserveContract = "Reserve"; + string constant CeloUnreleasedTreasuryContract = "CeloUnreleasedTreasury"; // Constant addresses address constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; address constant PROXY_ADMIN_ADDRESS = 0x4200000000000000000000000000000000000018; + + uint256 constant L1_MINTED_CELO_SUPPLY = 692702432463315819704447326; // as of May 15 2024 + + uint256 constant CELO_SUPPLY_CAP = 1000000000 ether; // 1 billion Celo + uint256 constant GENESIS_CELO_SUPPLY = 600000000 ether; // 600 million Celo + + uint256 constant FIFTEEN_YEAR_LINEAR_REWARD = (CELO_SUPPLY_CAP - GENESIS_CELO_SUPPLY) / 2; // 200 million Celo + + uint256 constant FIFTEEN_YEAR_CELO_SUPPLY = GENESIS_CELO_SUPPLY + FIFTEEN_YEAR_LINEAR_REWARD; // 800 million Celo (includes GENESIS_CELO_SUPPLY) + + uint256 constant MAX_L2_DISTRIBUTION = FIFTEEN_YEAR_CELO_SUPPLY - L1_MINTED_CELO_SUPPLY; // 107.2 million Celo + + uint256 constant L2_INITIAL_STASH_BALANCE = FIFTEEN_YEAR_LINEAR_REWARD + MAX_L2_DISTRIBUTION; // leftover from L1 target supply plus the 2nd 15 year term. } diff --git a/packages/protocol/test-sol/devchain/Import05Dependencies.sol b/packages/protocol/test-sol/devchain/Import05Dependencies.sol new file mode 100644 index 00000000000..30c1a4fa5f8 --- /dev/null +++ b/packages/protocol/test-sol/devchain/Import05Dependencies.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.5.13; + +// this file only exists so that foundry compiles this contracts +import { Proxy } from "@celo-contracts/common/Proxy.sol"; +import { ProxyFactory } from "@celo-contracts/common/ProxyFactory.sol"; +import { GoldToken } from "@celo-contracts/common/GoldToken.sol"; +import { Accounts } from "@celo-contracts/common/Accounts.sol"; +import { Election } from "@celo-contracts/governance/Election.sol"; +import { Governance } from "@celo-contracts/governance/Governance.sol"; +import { LockedGold } from "@celo-contracts/governance/LockedGold.sol"; +import { GovernanceApproverMultiSig } from "@celo-contracts/governance/GovernanceApproverMultiSig.sol"; +import { Escrow } from "@celo-contracts/identity/Escrow.sol"; +import { FederatedAttestations } from "@celo-contracts/identity/FederatedAttestations.sol"; +import { SortedOracles } from "@celo-contracts/stability/SortedOracles.sol"; +import { ReserveSpenderMultiSig } from "@mento-core/contracts/ReserveSpenderMultiSig.sol"; +import { Reserve } from "@mento-core/contracts/Reserve.sol"; +import { StableToken } from "@mento-core/contracts/StableToken.sol"; +import { StableTokenEUR } from "@mento-core/contracts/StableTokenEUR.sol"; +import { StableTokenBRL } from "@mento-core/contracts/StableTokenBRL.sol"; +import { Exchange } from "@mento-core/contracts/Exchange.sol"; + +import { IEpochManager } from "@celo-contracts/common/interfaces/IEpochManager.sol"; +import { IValidators } from "@celo-contracts/governance/interfaces/IValidators.sol"; +import "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; + +contract Import05 {} diff --git a/packages/protocol/test-sol/devchain/ImportPrecompiles.t.sol b/packages/protocol/test-sol/devchain/ImportPrecompiles.t.sol new file mode 100644 index 00000000000..a976098c20a --- /dev/null +++ b/packages/protocol/test-sol/devchain/ImportPrecompiles.t.sol @@ -0,0 +1,9 @@ +pragma solidity >=0.8.7 <0.8.20; + +// this file only exists so that foundry compiles this contracts +import "@test-sol/precompiles/ProofOfPossesionPrecompile.sol"; +import "@test-sol/precompiles/EpochSizePrecompile.sol"; +import "@test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol"; +import "@test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol"; + +contract ImportPrecompiles {} diff --git a/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol b/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol new file mode 100644 index 00000000000..7a30f76f3d6 --- /dev/null +++ b/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol @@ -0,0 +1,544 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import { Devchain } from "@test-sol/devchain/e2e/utils.sol"; +import { Utils08 } from "@test-sol/utils08.sol"; + +import { IEpochManager } from "@celo-contracts/common/interfaces/IEpochManager.sol"; + +import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; +import "@test-sol/utils/ECDSAHelper08.sol"; +import "@openzeppelin/contracts8/utils/structs/EnumerableSet.sol"; + +contract E2E_EpochManager is Test, Devchain, Utils08, ECDSAHelper08 { + struct VoterWithPK { + address voter; + uint256 privateKey; + } + + struct GroupWithVotes { + address group; + uint256 votes; + } + + address epochManagerOwner; + address epochManagerEnabler; + address[] firstElected; + + uint256 epochDuration; + + address[] groups; + address[] validatorsArray; + + uint256[] groupScore = [5e23, 7e23, 1e24, 4e23]; + uint256[] validatorScore = [1e23, 1e23, 1e23, 1e23, 1e23, 1e23, 1e23]; + + mapping(address => uint256) addressToPrivateKeys; + mapping(address => VoterWithPK) validatorToVoter; + + function setUp() public virtual { + uint256 totalVotes = election.getTotalVotes(); + + epochManagerOwner = Ownable(address(epochManager)).owner(); + epochManagerEnabler = registry.getAddressForOrDie(EPOCH_MANAGER_ENABLER_REGISTRY_ID); + firstElected = getValidators().getRegisteredValidators(); + + epochDuration = epochManager.epochDuration(); + + vm.deal(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); // 80% of the total supply to the treasury - whis will be yet distributed + } + + function activateValidators() public { + uint256[] memory valKeys = new uint256[](9); + valKeys[0] = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; + valKeys[1] = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; + valKeys[2] = 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6; + valKeys[3] = 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a; + valKeys[4] = 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba; + valKeys[5] = 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e; + valKeys[6] = 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356; + valKeys[7] = 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97; + valKeys[8] = 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6; + + for (uint256 i = 0; i < valKeys.length; i++) { + address account = vm.addr(valKeys[i]); + addressToPrivateKeys[account] = valKeys[i]; + } + + address[] memory registeredValidators = getValidators().getRegisteredValidators(); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + for (uint256 i = 0; i < registeredValidators.length; i++) { + (, , address validatorGroup, , ) = getValidators().getValidator(registeredValidators[i]); + if (getElection().getPendingVotesForGroup(validatorGroup) == 0) { + continue; + } + vm.startPrank(validatorGroup); + election.activate(validatorGroup); + vm.stopPrank(); + } + } + + function authorizeVoteSigner(uint256 signerPk, address account) internal { + bytes32 messageHash = keccak256(abi.encodePacked(account)); + bytes32 prefixedHash = ECDSAHelper08.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); + vm.prank(account); + accounts.authorizeVoteSigner(vm.addr(signerPk), v, r, s); + } +} + +contract E2E_EpochManager_InitializeSystem is E2E_EpochManager { + function setUp() public override { + super.setUp(); + whenL2(vm); + } + + function test_shouldRevert_WhenCalledByNonEnabler() public { + vm.expectRevert("msg.sender is not Enabler"); + epochManager.initializeSystem(1, 1, firstElected); + } + + function test_ShouldInitializeSystem() public { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(42, 43, firstElected); + + assertEq(epochManager.firstKnownEpoch(), 42); + assertEq(epochManager.getCurrentEpochNumber(), 42); + + assertTrue(epochManager.systemAlreadyInitialized()); + } +} +contract E2E_EpochManager_GetCurrentEpoch is E2E_EpochManager { + function setUp() public override { + super.setUp(); + whenL2(vm); + } + + function test_Revert_WhenSystemNotInitialized() public { + vm.expectRevert("Epoch system not initialized"); + ( + uint256 firstBlock, + uint256 lastBlock, + uint256 startTimestamp, + uint256 rewardsBlock + ) = epochManager.getCurrentEpoch(); + } + + function test_ReturnExpectedValues() public { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(42, 43, firstElected); + + assertEq(epochManager.firstKnownEpoch(), 42); + assertEq(epochManager.getCurrentEpochNumber(), 42); + + ( + uint256 firstBlock, + uint256 lastBlock, + uint256 startTimestamp, + uint256 rewardsBlock + ) = epochManager.getCurrentEpoch(); + assertEq(firstBlock, 43); + assertEq(lastBlock, 0); + assertEq(startTimestamp, block.timestamp); + assertEq(rewardsBlock, 0); + } +} + +contract E2E_EpochManager_StartNextEpochProcess is E2E_EpochManager { + function setUp() public override { + super.setUp(); + activateValidators(); + whenL2(vm); + + validatorsArray = getValidators().getRegisteredValidators(); + groups = getValidators().getRegisteredValidatorGroups(); + + address scoreManagerOwner = scoreManager.owner(); + + vm.startPrank(scoreManagerOwner); + scoreManager.setGroupScore(groups[0], groupScore[0]); + scoreManager.setGroupScore(groups[1], groupScore[1]); + scoreManager.setGroupScore(groups[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsArray[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsArray[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsArray[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsArray[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsArray[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsArray[5], validatorScore[5]); + + vm.stopPrank(); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(1, 1, firstElected); + } + + function test_shouldHaveInitialValues() public { + assertEq(epochManager.firstKnownEpoch(), 1); + assertEq(epochManager.getCurrentEpochNumber(), 1); + + // get getEpochProcessingState + ( + uint256 status, + uint256 perValidatorReward, + uint256 totalRewardsVote, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + assertEq(status, 0); // Not started + assertEq(perValidatorReward, 0); + assertEq(totalRewardsVote, 0); + assertEq(totalRewardsCommunity, 0); + assertEq(totalRewardsCarbonFund, 0); + } + + function test_shouldStartNextEpochProcessing() public { + timeTravel(vm, epochDuration + 1); + + epochManager.startNextEpochProcess(); + + ( + uint256 status, + uint256 perValidatorReward, + uint256 totalRewardsVote, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + assertEq(status, 1); // Started + assertGt(perValidatorReward, 0, "perValidatorReward"); + assertGt(totalRewardsVote, 0, "totalRewardsVote"); + assertGt(totalRewardsCommunity, 0, "totalRewardsCommunity"); + assertGt(totalRewardsCarbonFund, 0, "totalRewardsCarbonFund"); + } +} + +contract E2E_EpochManager_FinishNextEpochProcess is E2E_EpochManager { + using EnumerableSet for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet internal originalyElected; + + function setUp() public override { + super.setUp(); + activateValidators(); + whenL2(vm); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(1, 1, firstElected); + + validatorsArray = getValidators().getRegisteredValidators(); + groups = getValidators().getRegisteredValidatorGroups(); + + address scoreManagerOwner = scoreManager.owner(); + + vm.startPrank(scoreManagerOwner); + scoreManager.setGroupScore(groups[0], groupScore[0]); + scoreManager.setGroupScore(groups[1], groupScore[1]); + scoreManager.setGroupScore(groups[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsArray[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsArray[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsArray[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsArray[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsArray[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsArray[5], validatorScore[5]); + + vm.stopPrank(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + } + + function assertGroupWithVotes(GroupWithVotes[] memory groupWithVotes) internal { + for (uint256 i = 0; i < groupWithVotes.length; i++) { + uint256 expected = election.getTotalVotesForGroup(groupWithVotes[i].group); + assertEq(election.getTotalVotesForGroup(groupWithVotes[i].group), groupWithVotes[i].votes); + } + } + + function test_shouldFinishNextEpochProcessing() public { + address[] memory lessers; + address[] memory greaters; + address[] memory groupsEligible; + GroupWithVotes[] memory groupWithVotes; + uint256[] memory groupActiveBalances; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + + uint256 currentEpoch = epochManager.getCurrentEpochNumber(); + address[] memory currentlyElected = epochManager.getElected(); + for (uint256 i = 0; i < currentlyElected.length; i++) { + originalyElected.add(currentlyElected[i]); + } + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + epochManager.finishNextEpochProcess(groups, lessers, greaters); + + assertEq(currentEpoch + 1, epochManager.getCurrentEpochNumber()); + + address[] memory newlyElected = epochManager.getElected(); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(currentlyElected[i]), true); + } + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(currentEpoch + 2, epochManager.getCurrentEpochNumber()); + + address[] memory newlyElected2 = epochManager.getElected(); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(newlyElected2[i]), true); + } + + // add new validator group and validator + (address newValidatorGroup, address newValidator) = registerNewValidatorGroupWithValidator(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + groups.push(newValidatorGroup); + validatorsArray.push(newValidator); + + assertEq(epochManager.getElected().length, validators.getRegisteredValidators().length); + assertEq(groups.length, validators.getRegisteredValidatorGroups().length); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(epochManager.getElected().length, validatorsArray.length); + + // lower the number of electable validators + vm.prank(election.owner()); + election.setElectableValidators(1, validatorsArray.length - 1); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + ( + uint256 status, + uint256 perValidatorReward, + uint256 totalRewardsVoter, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + + assertGt(perValidatorReward, 0, "perValidatorReward"); + assertGt(totalRewardsVoter, 0, "totalRewardsVoter"); + assertGt(totalRewardsCommunity, 0, "totalRewardsCommunity"); + assertGt(totalRewardsCarbonFund, 0, "totalRewardsCarbonFund"); + + assertEq(epochManager.getElected().length, validatorsArray.length - 1); + } + + function registerNewValidatorGroupWithValidator() + internal + returns (address newValidatorGroup, address newValidator) + { + (, GroupWithVotes[] memory groupWithVotes) = getGroupsWithVotes(); + uint256 newGroupPK = uint256(keccak256(abi.encodePacked("newGroup"))); + uint256 newValidatorPK = uint256(keccak256(abi.encodePacked("newValidator"))); + + vm.deal(vm.addr(newGroupPK), 100_000_000 ether); + vm.deal(vm.addr(newValidatorPK), 100_000_000 ether); + + (uint256 validatorLockedGoldRequirement, ) = validators.getValidatorLockedGoldRequirements(); + (uint256 groupLockedGoldRequirement, ) = validators.getGroupLockedGoldRequirements(); + + newValidatorGroup = registerValidatorGroup( + "newGroup", + newGroupPK, + groupLockedGoldRequirement, + 100000000000000000000000 + ); + newValidator = registerValidator( + newValidatorPK, + validatorLockedGoldRequirement, + newValidatorGroup + ); + vm.prank(newValidatorGroup); + validators.addFirstMember(newValidator, address(0), groupWithVotes[0].group); + uint256 nonVotingLockedGold = lockedCelo.getAccountNonvotingLockedGold(newValidator); + vm.prank(newValidatorGroup); + election.vote(newValidatorGroup, nonVotingLockedGold, address(0), groupWithVotes[0].group); + + vm.startPrank(scoreManager.owner()); + scoreManager.setGroupScore(newValidatorGroup, groupScore[3]); + scoreManager.setValidatorScore(newValidator, validatorScore[6]); + vm.stopPrank(); + } + + function getGroupsWithVotes() + internal + returns (address[] memory groupsInOrder, GroupWithVotes[] memory groupWithVotes) + { + uint256[] memory votesTotal; + (groupsInOrder, votesTotal) = election.getTotalVotesForEligibleValidatorGroups(); + + groupWithVotes = new GroupWithVotes[](groupsInOrder.length); + for (uint256 i = 0; i < groupsInOrder.length; i++) { + groupWithVotes[i] = GroupWithVotes(groupsInOrder[i], votesTotal[i]); + } + } + + function getValidatorGroupsFromElected() internal returns (address[] memory) { + address[] memory elected = epochManager.getElected(); + address[] memory validatorGroups = new address[](elected.length); + for (uint256 i = 0; i < elected.length; i++) { + (, , address group, , ) = validators.getValidator(elected[i]); + validatorGroups[i] = group; + } + return validatorGroups; + } + + function getLessersAndGreaters( + address[] memory groups + ) + private + returns ( + address[] memory lessers, + address[] memory greaters, + GroupWithVotes[] memory groupWithVotes + ) + { + (, , uint256 maxTotalRewards, , ) = epochManager.getEpochProcessingState(); + uint256 totalRewards = 0; + + (, groupWithVotes) = getGroupsWithVotes(); + + lessers = new address[](groups.length); + greaters = new address[](groups.length); + + uint256[] memory rewards = new uint256[](groups.length); + + for (uint256 i = 0; i < groups.length; i++) { + uint256 groupScore = scoreManager.getGroupScore(groups[i]); + rewards[i] = election.getGroupEpochRewardsBasedOnScore( + groups[i], + maxTotalRewards, + groupScore + ); + } + for (uint256 i = 0; i < groups.length; i++) { + for (uint256 j = 0; j < groupWithVotes.length; j++) { + if (groupWithVotes[j].group == groups[i]) { + groupWithVotes[j].votes += rewards[i]; + break; + } + } + sort(groupWithVotes); + + address lesser = address(0); + address greater = address(0); + + for (uint256 j = 0; j < groupWithVotes.length; j++) { + if (groupWithVotes[j].group == groups[i]) { + greater = j == 0 ? address(0) : groupWithVotes[j - 1].group; + lesser = j == groupWithVotes.length - 1 ? address(0) : groupWithVotes[j + 1].group; + break; + } + } + + lessers[i] = lesser; + greaters[i] = greater; + } + } + + function registerValidatorGroup( + string memory groupName, + uint256 privateKey, + uint256 amountToLock, + uint256 commission + ) public returns (address accountAddress) { + accountAddress = vm.addr(privateKey); + vm.startPrank(accountAddress); + lockGold(amountToLock); + getAccounts().setName(groupName); + getValidators().registerValidatorGroup(commission); + vm.stopPrank(); + } + + function registerValidator( + uint256 privateKey, + uint256 amountToLock, + address groupToAffiliate + ) public returns (address) { + address accountAddress = vm.addr(privateKey); + vm.startPrank(accountAddress); + lockGold(amountToLock); + + (bytes memory ecdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner(accountAddress, privateKey); + getValidators().registerValidatorNoBls(ecdsaPubKey); + getValidators().affiliate(groupToAffiliate); + + vm.stopPrank(); + return accountAddress; + } + + function _generateEcdsaPubKeyWithSigner( + address _validator, + uint256 _signerPk + ) internal returns (bytes memory ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) { + (v, r, s) = getParsedSignatureOfAddress(_validator, _signerPk); + + bytes32 addressHash = keccak256(abi.encodePacked(_validator)); + ecdsaPubKey = addressToPublicKey(addressHash, v, r, s); + } + + function getParsedSignatureOfAddress( + address _address, + uint256 privateKey + ) public pure returns (uint8, bytes32, bytes32) { + bytes32 addressHash = keccak256(abi.encodePacked(_address)); + bytes32 prefixedHash = toEthSignedMessageHash(addressHash); + return vm.sign(privateKey, prefixedHash); + } + + function lockGold(uint256 value) public { + getAccounts().createAccount(); + getLockedGold().lock{ value: value }(); + } + + // Bubble sort algorithm since it is a small array + function sort(GroupWithVotes[] memory items) public { + uint length = items.length; + for (uint i = 0; i < length; i++) { + for (uint j = 0; j < length - 1; j++) { + if (items[j].votes < items[j + 1].votes) { + // Swap + GroupWithVotes memory temp = items[j]; + items[j] = items[j + 1]; + items[j + 1] = temp; + } + } + } + } +} diff --git a/packages/protocol/test-sol/devchain/e2e/utils.sol b/packages/protocol/test-sol/devchain/e2e/utils.sol index cd1c57b8200..0e84cfafa26 100644 --- a/packages/protocol/test-sol/devchain/e2e/utils.sol +++ b/packages/protocol/test-sol/devchain/e2e/utils.sol @@ -3,10 +3,17 @@ pragma solidity >=0.8.7 <0.8.20; import "@celo-contracts-8/common/UsingRegistry.sol"; import "@celo-contracts/common/interfaces/IRegistry.sol"; +import { IEpochManager } from "@celo-contracts/common/interfaces/IEpochManager.sol"; +import { IAccounts } from "@celo-contracts/common/interfaces/IAccounts.sol"; +import { IScoreManager } from "@celo-contracts-8/common/interfaces/IScoreManager.sol"; +import { IValidators } from "@celo-contracts/governance/interfaces/IValidators.sol"; +import { IElection } from "@celo-contracts/governance/interfaces/IElection.sol"; +import { ILockedCelo } from "@celo-contracts/governance/interfaces/ILockedCelo.sol"; // All core contracts that are expected to be in the Registry on the devchain import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; import "@celo-contracts/stability/interfaces/ISortedOracles.sol"; +import "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; import { TestConstants } from "@test-sol/constants.sol"; @@ -17,6 +24,13 @@ contract Devchain is UsingRegistry, TestConstants { // All core contracts that are expected to be in the Registry on the devchain ISortedOracles sortedOracles; FeeCurrencyDirectory feeCurrencyDirectory; + IEpochManager epochManager; + ICeloUnreleasedTreasury celoUnreleasedTreasury; + IValidators validators; + IAccounts accounts; + IScoreManager scoreManager; + IElection election; + ILockedCelo lockedCelo; constructor() { // The following line is required by UsingRegistry.sol @@ -28,6 +42,14 @@ contract Devchain is UsingRegistry, TestConstants { devchainRegistry.getAddressForStringOrDie("FeeCurrencyDirectory") ); // FeeCurrencyDirectory is not in UsingRegistry.sol + epochManager = getEpochManager(); + celoUnreleasedTreasury = getCeloUnreleasedTreasury(); + validators = getValidators(); + accounts = getAccounts(); + scoreManager = IScoreManager(address(getScoreReader())); + election = getElection(); + lockedCelo = getLockedCelo(); + // TODO: Add missing core contracts below (see list in migrations_sol/constants.sol) // TODO: Consider asserting that all contracts we expect are available in the Devchain class // (see list in migrations_sol/constants.sol) diff --git a/packages/protocol/test-sol/devchain/migration/05Links.sol b/packages/protocol/test-sol/devchain/migration/05Links.sol new file mode 100644 index 00000000000..4640cc6b9ab --- /dev/null +++ b/packages/protocol/test-sol/devchain/migration/05Links.sol @@ -0,0 +1,26 @@ +// This file exists only to force migration tests also compile below imported contracts. +pragma solidity ^0.5.13; + +import "@celo-contracts/governance/BlockchainParameters.sol"; +import "@celo-contracts/governance/DoubleSigningSlasher.sol"; +import "@celo-contracts/governance/DowntimeSlasher.sol"; +import "@celo-contracts/governance/EpochRewards.sol"; +import "@celo-contracts/governance/GovernanceSlasher.sol"; +import "@celo-contracts/governance/LockedGold.sol"; +import "@celo-contracts/common/FeeCurrencyWhitelist.sol"; +import "@celo-contracts/common/Freezer.sol"; +import "@celo-contracts/common/FeeHandler.sol"; +import "@celo-contracts/identity/OdisPayments.sol"; +import "@celo-contracts/identity/Random.sol"; +import "@celo-contracts/common/Registry.sol"; +import "@celo-contracts/common/UniswapFeeHandlerSeller.sol"; +import "@celo-contracts/common/MentoFeeHandlerSeller.sol"; + +import "celo-foundry/Test.sol"; + +import { TestConstants } from "@test-sol/constants.sol"; +import { Utils } from "@test-sol/utils.sol"; + +contract BlockchainParametersTest is Test, TestConstants, Utils { + function test_dummy_test() public {} +} diff --git a/packages/protocol/test-sol/devchain/migration/IntegrationValidators.t.sol b/packages/protocol/test-sol/devchain/migration/IntegrationValidators.t.sol new file mode 100644 index 00000000000..2ba650dffe7 --- /dev/null +++ b/packages/protocol/test-sol/devchain/migration/IntegrationValidators.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import { Devchain } from "@test-sol/devchain/e2e/utils.sol"; + +contract IntegrationsValidators is Test, Devchain { + function test_deaffiliateWorskWithEpochManager() public { + vm.prank(election.electValidatorAccounts()[0]); + validators.deaffiliate(); + } +} diff --git a/packages/protocol/test-sol/devchain/migration/Migration.t.sol b/packages/protocol/test-sol/devchain/migration/Migration.t.sol index aa8a9756840..d1d427cc539 100644 --- a/packages/protocol/test-sol/devchain/migration/Migration.t.sol +++ b/packages/protocol/test-sol/devchain/migration/Migration.t.sol @@ -1,21 +1,33 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.7 <0.8.20; +pragma solidity >=0.8.0 <0.8.20; -import { Test } from "forge-std-8/Test.sol"; -import "forge-std-8/console2.sol"; +import "celo-foundry-8/Test.sol"; +import { Utils08 } from "@test-sol/utils08.sol"; import { TestConstants } from "@test-sol/constants.sol"; import { MigrationsConstants } from "@migrations-sol/constants.sol"; -import { CeloDistributionSchedule } from "@celo-contracts-8/common/CeloDistributionSchedule.sol"; import { FeeCurrencyDirectory } from "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; import "@celo-contracts/common/interfaces/IRegistry.sol"; import "@celo-contracts/common/interfaces/IProxy.sol"; +import "@celo-contracts/common/interfaces/ICeloToken.sol"; +import "@celo-contracts/common/interfaces/IAccounts.sol"; +import "@celo-contracts/common/interfaces/IEpochManager.sol"; +import "@celo-contracts/common/interfaces/IEpochManagerEnabler.sol"; +import "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import "@celo-contracts/governance/interfaces/IElection.sol"; -contract IntegrationTest is Test, TestConstants { +import "@celo-contracts/governance/interfaces/IValidators.sol"; + +import "@celo-contracts-8/common/interfaces/IPrecompiles.sol"; +import "@celo-contracts-8/common/interfaces/IScoreManager.sol"; + +contract IntegrationTest is Test, TestConstants, Utils08 { IRegistry registry = IRegistry(REGISTRY_ADDRESS); - function setUp() public {} + uint256 constant RESERVE_BALANCE = 69411663406170917420347916; // current as of 08/20/24 + + // function setUp() public virtual {} /** * @notice Removes CBOR encoded metadata from the tail of the deployedBytecode. @@ -72,6 +84,7 @@ contract RegistryIntegrationTest is IntegrationTest, MigrationsConstants { bytes32 hashValidators = keccak256(abi.encodePacked("Validators")); bytes32 hashCeloToken = keccak256(abi.encodePacked("CeloToken")); bytes32 hashLockedCelo = keccak256(abi.encodePacked("LockedCelo")); + bytes32 hashEpochManager = keccak256(abi.encodePacked("EpochManager")); for (uint256 i = 0; i < contractsInRegistry.length; i++) { // Read name from list of core contracts @@ -91,7 +104,8 @@ contract RegistryIntegrationTest is IntegrationTest, MigrationsConstants { hashContractName != hashSortedOracles && hashContractName != hashValidators && hashContractName != hashCeloToken && // TODO: remove once GoldToken contract has been renamed to CeloToken - hashContractName != hashLockedCelo // TODO: remove once LockedGold contract has been renamed to LockedCelo + hashContractName != hashLockedCelo && // TODO: remove once LockedGold contract has been renamed to LockedCelo + hashContractName != hashEpochManager ) { // Get proxy address registered in the Registry address proxyAddress = registry.getAddressForStringOrDie(contractName); @@ -106,9 +120,10 @@ contract RegistryIntegrationTest is IntegrationTest, MigrationsConstants { actualBytecodeWithMetadataOnDevchain ); + string memory contractFileName = string(abi.encodePacked(contractName, ".sol")); // Get bytecode from build artifacts bytes memory expectedBytecodeWithMetadataFromArtifacts = vm.getDeployedCode( - string(abi.encodePacked(contractName, ".sol")) + contractFileName ); bytes memory expectedBytecodeFromArtifacts = removeMetadataFromBytecode( expectedBytecodeWithMetadataFromArtifacts @@ -124,3 +139,151 @@ contract RegistryIntegrationTest is IntegrationTest, MigrationsConstants { } } } + +contract EpochManagerIntegrationTest is IntegrationTest, MigrationsConstants { + ICeloToken celoToken; + IAccounts accountsContract; + IValidators validatorsContract; + IEpochManager epochManager; + IEpochManagerEnabler epochManagerEnabler; + IScoreManager scoreManager; + IElection election; + ICeloUnreleasedTreasury celoUnreleasedTreasury; + + address reserveAddress; + address unreleasedTreasury; + address randomAddress; + + uint256 firstEpochNumber = 100; + uint256 firstEpochBlock = 100; + address[] firstElected; + address[] validatorsList; + address[] groupList; + + uint256[] groupScore = [5e23, 7e23, 1e24]; + uint256[] validatorScore = [1e23, 1e23, 1e23, 1e23, 1e23, 1e23]; + + function setUp() public { + randomAddress = actor("randomAddress"); + + validatorsContract = IValidators(registry.getAddressForStringOrDie("Validators")); + + election = IElection(registry.getAddressForStringOrDie("Election")); + scoreManager = IScoreManager(registry.getAddressForStringOrDie("ScoreManager")); + unreleasedTreasury = registry.getAddressForStringOrDie("CeloUnreleasedTreasury"); + reserveAddress = registry.getAddressForStringOrDie("Reserve"); + + validatorsList = validatorsContract.getRegisteredValidators(); + groupList = validatorsContract.getRegisteredValidatorGroups(); + + // mint to the reserve + celoToken = ICeloToken(registry.getAddressForStringOrDie("GoldToken")); + + vm.deal(address(0), CELO_SUPPLY_CAP); + vm.prank(address(0)); + celoToken.mint(reserveAddress, RESERVE_BALANCE); + + vm.prank(address(0)); + celoToken.mint(randomAddress, L1_MINTED_CELO_SUPPLY - RESERVE_BALANCE); // mint outstanding l1 supply before L2. + + epochManager = IEpochManager(registry.getAddressForStringOrDie("EpochManager")); + epochManagerEnabler = IEpochManagerEnabler( + registry.getAddressForStringOrDie("EpochManagerEnabler") + ); + } + + function activateValidators() public { + address[] memory registeredValidators = validatorsContract.getRegisteredValidators(); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + for (uint256 i = 0; i < registeredValidators.length; i++) { + (, , address validatorGroup, , ) = validatorsContract.getValidator(registeredValidators[i]); + if (election.getPendingVotesForGroup(validatorGroup) == 0) { + continue; + } + vm.startPrank(validatorGroup); + election.activate(validatorGroup); + vm.stopPrank(); + } + } + + function test_Reverts_whenSystemNotInitialized() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_WhenEndOfEpochHasNotBeenReached() public { + // fund treasury + vm.prank(address(0)); + celoToken.mint(unreleasedTreasury, L2_INITIAL_STASH_BALANCE); + + uint256 l1EpochNumber = IPrecompiles(address(validatorsContract)).getEpochNumber(); + + vm.prank(address(epochManagerEnabler)); + epochManager.initializeSystem(l1EpochNumber, block.number, validatorsList); + + vm.expectRevert("Epoch is not ready to start"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_whenAlreadyInitialized() public { + _MockL2Migration(validatorsList); + + vm.prank(address(epochManagerEnabler)); + vm.expectRevert("Epoch system already initialized"); + epochManager.initializeSystem(100, block.number, firstElected); + } + + function test_SetsCurrentRewardBlock() public { + _MockL2Migration(validatorsList); + + blockTravel(vm, 43200); + timeTravel(vm, DAY); + + epochManager.startNextEpochProcess(); + + (, , , uint256 _currentRewardsBlock) = epochManager.getCurrentEpoch(); + + assertEq(_currentRewardsBlock, block.number); + } + + function _MockL2Migration(address[] memory _validatorsList) internal { + for (uint256 i = 0; i < _validatorsList.length; i++) { + firstElected.push(_validatorsList[i]); + } + + uint256 l1EpochNumber = IPrecompiles(address(validatorsContract)).getEpochNumber(); + + activateValidators(); + vm.deal(unreleasedTreasury, L2_INITIAL_STASH_BALANCE); + + vm.prank(address(0)); + celoToken.mint(unreleasedTreasury, L2_INITIAL_STASH_BALANCE); + + whenL2(vm); + _setValidatorL2Score(); + + vm.prank(address(epochManagerEnabler)); + + epochManager.initializeSystem(l1EpochNumber, block.number, firstElected); + } + + function _setValidatorL2Score() internal { + address scoreManagerOwner = scoreManager.owner(); + vm.startPrank(scoreManagerOwner); + scoreManager.setGroupScore(groupList[0], groupScore[0]); + scoreManager.setGroupScore(groupList[1], groupScore[1]); + scoreManager.setGroupScore(groupList[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsList[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsList[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsList[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsList[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsList[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsList[5], validatorScore[5]); + + vm.stopPrank(); + } +} diff --git a/packages/protocol/test-sol/integration/CompileValidatorMock.t.sol b/packages/protocol/test-sol/integration/CompileValidatorMock.t.sol new file mode 100644 index 00000000000..a4a8aa9930f --- /dev/null +++ b/packages/protocol/test-sol/integration/CompileValidatorMock.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "forge-std/console.sol"; + +// here only to forge compile of ValidatorsMock +import "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; + +contract CompileValidatorMock is Test { + function test_nop() public { + console.log("nop"); + } +} diff --git a/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol b/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol index 97bc1e190d9..13e4fb9ba05 100644 --- a/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol +++ b/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol @@ -9,6 +9,9 @@ import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; import "@celo-contracts/common/GoldToken.sol"; +import "@celo-contracts-8/common/test/MockEpochManager.sol"; +import "@celo-contracts-8/common/interfaces/IPrecompiles.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; import "@celo-contracts/governance/Election.sol"; import "@celo-contracts/governance/LockedGold.sol"; @@ -18,7 +21,6 @@ import "@celo-contracts/stability/test/MockStableToken.sol"; import "@celo-contracts/governance/Election.sol"; import "@celo-contracts/governance/Governance.sol"; -import "@celo-contracts/governance/test/ValidatorsMock.sol"; import { TestConstants } from "@test-sol/constants.sol"; import "@test-sol/utils/ECDSAHelper.sol"; import { Utils } from "@test-sol/utils.sol"; @@ -36,11 +38,12 @@ contract RevokeCeloAfterL2Transition is Test, TestConstants, ECDSAHelper, Utils MockStableToken stableToken; Election election; ValidatorsMockTunnel public validatorsMockTunnel; - Validators public validators; + IValidators public validators; LockedGold lockedGold; Governance governance; GoldToken goldToken; ReleaseGold releaseGold; + MockEpochManager epochManager; address owner; address accApprover; @@ -52,6 +55,7 @@ contract RevokeCeloAfterL2Transition is Test, TestConstants, ECDSAHelper, Utils address beneficiary; address refundAddress; address releaseOwner; + address epochManagerAddress = actor("epochManagerAddress"); address authorizedValidatorSigner; uint256 authorizedValidatorSignerPK; @@ -174,12 +178,17 @@ contract RevokeCeloAfterL2Transition is Test, TestConstants, ECDSAHelper, Utils stableToken = new MockStableToken(); election = new Election(true); lockedGold = new LockedGold(true); - validators = new Validators(true); + address validatorsAddress = actor("Validators"); + deployCodeTo("ValidatorsMock.sol", validatorsAddress); + validators = IValidators(validatorsAddress); + // TODO move to create2 validatorsMockTunnel = new ValidatorsMockTunnel(address(validators)); governance = new Governance(true); goldToken = new GoldToken(true); releaseGold = new ReleaseGold(true); + epochManager = new MockEpochManager(); + registry.setAddressFor(AccountsContract, address(accounts)); registry.setAddressFor(ElectionContract, address(election)); registry.setAddressFor(StableTokenContract, address(stableToken)); @@ -187,6 +196,7 @@ contract RevokeCeloAfterL2Transition is Test, TestConstants, ECDSAHelper, Utils registry.setAddressFor(ValidatorsContract, address(validators)); registry.setAddressFor(GovernanceContract, address(governance)); registry.setAddressFor(GoldTokenContract, address(goldToken)); + registry.setAddressFor(EpochManagerContract, address(epochManager)); goldToken.initialize(address(registry)); @@ -282,7 +292,14 @@ contract RevokeCeloAfterL2Transition is Test, TestConstants, ECDSAHelper, Utils } function _whenL2() public { + uint256 l1EpochNumber = IPrecompiles(address(validators)).getEpochNumber(); + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + + address[] memory _elected = new address[](2); + _elected[0] = actor("firstElected"); + _elected[1] = actor("secondElected"); + epochManager.initializeSystem(l1EpochNumber, block.number, _elected); } function _registerValidatorGroupHelper(address _group, uint256 numMembers) internal { @@ -343,7 +360,7 @@ contract RevokeCeloAfterL2Transition is Test, TestConstants, ECDSAHelper, Utils vm.prank(_validator); validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + validatorRegistrationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); return _ecdsaPubKey; } @@ -477,7 +494,7 @@ contract RevokeCeloAfterL2TransitionTest is RevokeCeloAfterL2Transition { vm.prank(_validator); validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + validatorRegistrationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); return _ecdsaPubKey; } diff --git a/packages/protocol/test-sol/mocks/EpochManagerEnablerMock.sol b/packages/protocol/test-sol/mocks/EpochManagerEnablerMock.sol new file mode 100644 index 00000000000..137d9104522 --- /dev/null +++ b/packages/protocol/test-sol/mocks/EpochManagerEnablerMock.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.8.0; + +import "../../contracts-0.8/common/EpochManagerEnabler.sol"; + +/** + * @title A wrapper around EpochManagerEnabler that exposes internal functions for testing. + */ +contract EpochManagerEnablerMock is EpochManagerEnabler(true) { + address[] validatorSet; + + function setFirstBlockOfEpoch() external { + return _setFirstBlockOfEpoch(); + } + + function addValidator(address validator) external { + validatorSet.push(validator); + } + + // Minimally override core functions from UsingPrecompiles + function numberValidatorsInCurrentSet() public view override returns (uint256) { + return validatorSet.length; + } + + function numberValidatorsInSet(uint256) public view override returns (uint256) { + return validatorSet.length; + } + + function validatorSignerAddressFromCurrentSet( + uint256 index + ) public view override returns (address) { + return validatorSet[index]; + } +} diff --git a/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol b/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol index f51959049d5..cdab89b4674 100644 --- a/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol +++ b/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol @@ -1,17 +1,27 @@ -// TODO move this to test folder pragma solidity >=0.8.7 <0.8.20; +address constant EPOCH_SIZEPRE_COMPILE_ADDRESS = address(0xff - 7); contract EpochSizePrecompile { - address constant ADDRESS = address(0xff - 7); + address constant ADDRESS = EPOCH_SIZEPRE_COMPILE_ADDRESS; uint256 public constant EPOCH_SIZE = 100; + uint256 public epochSizeSet; receive() external payable {} fallback(bytes calldata) external payable returns (bytes memory) { + // this is required because when the migrations deploy the precompiles + // they don't get constructed + if (epochSizeSet != 0) { + return abi.encodePacked(epochSizeSet); + } return abi.encodePacked(EPOCH_SIZE); } + function setEpochSize(uint256 epochSize) public { + epochSizeSet = epochSize; + } + function getAddress() public pure returns (address) { return ADDRESS; } diff --git a/packages/protocol/test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol b/packages/protocol/test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol new file mode 100644 index 00000000000..44e651a073f --- /dev/null +++ b/packages/protocol/test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol @@ -0,0 +1,32 @@ +// TODO move this to test folder +pragma solidity >=0.8.7 <0.8.20; + +import "forge-std/console.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; + +contract NumberValidatorsInCurrentSetPrecompile { + address constant ADDRESS = address(0xff - 6); + + uint256 public NumberOfValidators = 1; + + address internal constant registryAddress = 0x000000000000000000000000000000000000ce10; + + receive() external payable {} + + fallback(bytes calldata) external payable returns (bytes memory) { + return abi.encodePacked(NumberOfValidators); + } + + function setNumberOfValidators() external { + IRegistry registry = IRegistry(registryAddress); + address validatorsAddress = registry.getAddressForString("Validators"); + IValidators validatorsContract = IValidators(validatorsAddress); + address[] memory registeredValidators = validatorsContract.getRegisteredValidators(); + NumberOfValidators = registeredValidators.length; + } + + function getAddress() public pure returns (address) { + return ADDRESS; + } +} diff --git a/packages/protocol/test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol b/packages/protocol/test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol new file mode 100644 index 00000000000..f2ed9c7ade8 --- /dev/null +++ b/packages/protocol/test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol @@ -0,0 +1,56 @@ +// TODO move this to test folder +pragma solidity >=0.8.7 <0.8.20; + +import "forge-std/console.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; + +contract ValidatorSignerAddressFromCurrentSetPrecompile { + address constant ADDRESS = address(0xff - 5); + + uint256 public constant EPOCH_SIZE = 100; + + address[] validators; + + address internal constant registryAddress = 0x000000000000000000000000000000000000ce10; + + receive() external payable {} + + fallback(bytes calldata input) external payable returns (bytes memory) { + uint256 index = getUint256FromBytes(input, 0); + return abi.encodePacked(uint256(uint160(validators[index]))); + } + + function getAddress() public pure returns (address) { + return ADDRESS; + } + + function getUint256FromBytes(bytes memory bs, uint256 start) internal pure returns (uint256) { + return uint256(getBytes32FromBytes(bs, start)); + } + + function setValidators() external { + IRegistry registry = IRegistry(registryAddress); + address validatorsAddress = registry.getAddressForString("Validators"); + IValidators validatorsContract = IValidators(validatorsAddress); + address[] memory registeredValidators = validatorsContract.getRegisteredValidators(); + for (uint256 i = 0; i < registeredValidators.length; i++) { + validators.push(registeredValidators[i]); + } + } + + /** + * @notice Converts bytes to bytes32. + * @param bs byte[] data + * @param start offset into byte data to convert + * @return bytes32 data + */ + function getBytes32FromBytes(bytes memory bs, uint256 start) internal pure returns (bytes32) { + require(bs.length >= start + 32, "slicing out of range"); + bytes32 x; + assembly { + x := mload(add(bs, add(start, 32))) + } + return x; + } +} diff --git a/packages/protocol/test-sol/unit/common/Accounts.t.sol b/packages/protocol/test-sol/unit/common/Accounts.t.sol index 9fe254a2365..94ee49dac5b 100644 --- a/packages/protocol/test-sol/unit/common/Accounts.t.sol +++ b/packages/protocol/test-sol/unit/common/Accounts.t.sol @@ -608,11 +608,13 @@ contract AccountsTest_setPaymentDelegation is AccountsTest { assertEq(realFraction, fraction); } - function test_Revert_SetPaymentDelegation_WhenL2() public { + function test_ShouldSetAnAddressAndAFraction_WhenL2() public { _whenL2(); accounts.createAccount(); - vm.expectRevert("This method is no longer supported in L2."); accounts.setPaymentDelegation(beneficiary, fraction); + (address realBeneficiary, uint256 realFraction) = accounts.getPaymentDelegation(address(this)); + assertEq(realBeneficiary, beneficiary); + assertEq(realFraction, fraction); } function test_ShouldNotAllowFractionGreaterThan1() public { diff --git a/packages/protocol/test-sol/unit/common/Blockable.t.sol b/packages/protocol/test-sol/unit/common/Blockable.t.sol new file mode 100644 index 00000000000..0ec65176398 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/Blockable.t.sol @@ -0,0 +1,98 @@ +pragma solidity ^0.5.13; + +import "celo-foundry/Test.sol"; + +import "@celo-contracts/common/Blockable.sol"; +import "@celo-contracts/common/interfaces/IBlockable.sol"; +import "@celo-contracts/common/interfaces/IBlocker.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +contract TestBlocker is IBlocker { + bool public blocked; + + function mockSetBlocked(bool _blocked) public { + blocked = _blocked; + } + + function isBlocked() external view returns (bool) { + return blocked; + } +} + +contract BlockableMock is Blockable, Ownable { + function setBlockedByContract(address _blockedBy) public onlyOwner { + _setBlockedBy(_blockedBy); + } +} + +contract TestBlockable is BlockableMock { + function functionToBeBlocked() public onlyWhenNotBlocked { + return; + } +} + +contract BlockableTest is Test { + IBlockable blockable; + TestBlocker blocker; + address notOwner; + + event BlockedBySet(address indexed _blockedBy); + + function setUp() public { + blockable = new BlockableMock(); + blocker = new TestBlocker(); + notOwner = actor("notOwner"); + } +} + +contract BlockableTest_setBlockable is BlockableTest { + function test_setBlockable() public { + blockable.setBlockedByContract(address(blocker)); + assert(blockable.getBlockedbyContract() == address(blocker)); + } + + function test_Reverts_WhenNotCalledByOwner() public { + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + blockable.setBlockedByContract(address(blocker)); + } + + function test_Emits_BlockedBySet() public { + vm.expectEmit(false, false, false, true); + emit BlockedBySet(address(blocker)); + blockable.setBlockedByContract(address(blocker)); + } +} + +contract BlockableTest_isBlocked is BlockableTest { + function test_isFalse_WhenBlockableNotSet() public { + assert(blockable.isBlocked() == false); + } + + function test_isBlocked() public { + assertTrue(blockable.isBlocked() == false); + blocker.mockSetBlocked(true); + blockable.setBlockedByContract(address(blocker)); + assertTrue(blockable.isBlocked()); + } +} + +contract BlockableTest_onlyWhenNotBlocked is BlockableTest { + TestBlockable blockableWithFunction; + + function setUp() public { + super.setUp(); + blockableWithFunction = new TestBlockable(); + blockableWithFunction.setBlockedByContract(address(blocker)); + } + + function test_Reverts_WhenBlocked() public { + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + blockableWithFunction.functionToBeBlocked(); + } + + function test_callsucceeds_WhenNotBlocked() public { + blockableWithFunction.functionToBeBlocked(); + } +} diff --git a/packages/protocol/test-sol/unit/common/CeloDistributionSchedule.t.sol b/packages/protocol/test-sol/unit/common/CeloDistributionSchedule.t.sol deleted file mode 100644 index 88d051adca1..00000000000 --- a/packages/protocol/test-sol/unit/common/CeloDistributionSchedule.t.sol +++ /dev/null @@ -1,742 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.7 <0.8.20; -pragma experimental ABIEncoderV2; - -import "celo-foundry-8/Test.sol"; -import "@celo-contracts/common/FixidityLib.sol"; -import "@celo-contracts/common/interfaces/IRegistry.sol"; -import "@celo-contracts-8/common/interfaces/ICeloToken.sol"; -import "@celo-contracts/governance/interfaces/IGovernance.sol"; -import "@celo-contracts-8/common/CeloDistributionSchedule.sol"; -import "@celo-contracts-8/common/IsL2Check.sol"; -import { TestConstants } from "@test-sol/constants.sol"; - -import "@test-sol/unit/governance/mock/MockGovernance.sol"; - -contract CeloDistributionScheduleTest is Test, TestConstants, IsL2Check { - using FixidityLib for FixidityLib.Fraction; - - IRegistry registry; - ICeloToken celoToken; - MockGovernance governance; - - CeloDistributionSchedule celoDistributionSchedule; - - address owner = address(this); - - address celoTokenAddress = actor("celoTokenAddress"); - - address celoDistributionOwner = actor("celoDistributionOwner"); - address communityRewardFund = actor("communityRewardFund"); - address carbonOffsettingPartner = actor("carbonOffsettingPartner"); - - address newPartner = actor("newPartner"); - address randomAddress = actor("randomAddress"); - - uint256 constant DAILY_DISTRIBUTION_AMOUNT = 6748256563599655349558; // 6,748 Celo - uint256 constant L1_MINTED_CELO_SUPPLY = 692702432463315819704447326; // as of May 15 2024 - - uint256 constant CELO_SUPPLY_CAP = 1000000000 ether; // 1 billion Celo - uint256 constant GENESIS_CELO_SUPPLY = 600000000 ether; // 600 million Celo - - uint256 constant FIFTEEN_YEAR_LINEAR_REWARD = (CELO_SUPPLY_CAP - GENESIS_CELO_SUPPLY) / 2; // 200 million Celo - - uint256 constant FIFTEEN_YEAR_CELO_SUPPLY = GENESIS_CELO_SUPPLY + FIFTEEN_YEAR_LINEAR_REWARD; // 800 million Celo (includes GENESIS_CELO_SUPPLY) - - uint256 constant MAX_L2_DISTRIBUTION = FIFTEEN_YEAR_CELO_SUPPLY - L1_MINTED_CELO_SUPPLY; // 107.2 million Celo - - uint256 constant L2_INITIAL_STASH_BALANCE = FIFTEEN_YEAR_LINEAR_REWARD + MAX_L2_DISTRIBUTION; // leftover from L1 target supply plus the 2nd 15 year term. - - uint256 constant MAX_L2_COMMUNITY_DISTRIBUTION = MAX_L2_DISTRIBUTION / 4; // 26.8 million Celo - uint256 constant MAX_L2_CARBON_FUND_DISTRIBUTION = MAX_L2_DISTRIBUTION / 1000; // 107,297 Celo - - uint256 constant L2_FIFTEEN_YEAR_CELO_SUPPLY = - L1_MINTED_CELO_SUPPLY + MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION; - - uint256 constant l2StartTime = 1715808537; // Arbitary later date (May 15 2024) - uint256 constant communityRewardFraction = FIXED1 / 4; // 25% - uint256 constant carbonOffsettingFraction = FIXED1 / 1000; // 0.1% - uint256 constant newCommunityRewardFraction = FIXED1 / 2; // 50% - uint256 constant newCarbonOffsettingFraction = FIXED1 / 500; // 0.2% - - event CommunityRewardFractionSet(uint256 fraction); - event CarbonOffsettingFundSet(address indexed partner, uint256 fraction); - - function setUp() public virtual { - setUpL1(); - - // Setup L2 after minting L1 supply. - deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); - } - - function setUpL1() public { - deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); - registry = IRegistry(REGISTRY_ADDRESS); - - deployCodeTo("GoldToken.sol", abi.encode(false), celoTokenAddress); - celoToken = ICeloToken(celoTokenAddress); - celoToken.setRegistry(REGISTRY_ADDRESS); - // Using a mock contract, as foundry does not allow for library linking when using deployCodeTo - governance = new MockGovernance(); - - registry.setAddressFor("CeloToken", address(celoToken)); - - registry.setAddressFor("Governance", address(governance)); - - vm.deal(address(0), CELO_SUPPLY_CAP); - assertEq(celoToken.totalSupply(), 0, "starting total supply not zero."); - // Mint L1 supply - vm.prank(address(0)); - celoToken.mint(randomAddress, L1_MINTED_CELO_SUPPLY); - assertEq(celoToken.totalSupply(), L1_MINTED_CELO_SUPPLY, "total supply incorrect."); - } - - function newCeloDistibutionSchedule() internal returns (CeloDistributionSchedule) { - vm.warp(block.timestamp + l2StartTime); - vm.prank(celoDistributionOwner); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - - vm.deal(address(celoDistributionSchedule), L2_INITIAL_STASH_BALANCE); - - vm.prank(celoDistributionOwner); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.prank(celoDistributionOwner); - - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } -} - -contract CeloDistributionScheduleTest_initialize is CeloDistributionScheduleTest { - function setUp() public override { - super.setUp(); - vm.warp(block.timestamp + l2StartTime); - - vm.prank(celoDistributionOwner); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - } - - function test_ShouldSetAnOwnerToCeloDistributionScheduleInstance() public { - assertEq(celoDistributionSchedule.owner(), celoDistributionOwner); - } - - function test_ShouldSetRegistryAddressToCeloDistributionScheduleInstance() public { - assertEq(address(celoDistributionSchedule.registry()), REGISTRY_ADDRESS); - } - - function test_ShouldNotSetBeneficiariesToCeloDistributionScheduleInstance() public { - assertEq(celoDistributionSchedule.communityRewardFund(), address(0)); - assertEq(celoDistributionSchedule.carbonOffsettingPartner(), address(0)); - } - - function test_ShouldHaveZeroTotalDistributedByScheduleOnInit() public { - assertEq(celoDistributionSchedule.totalDistributedBySchedule(), 0); - } - - function test_ShouldNotSetTheL2StartTime() public { - assertEq(celoDistributionSchedule.l2StartTime(), 0); - } - - function test_Reverts_WhenRegistryIsTheNullAddress() public { - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - vm.expectRevert("Cannot register the null address"); - celoDistributionSchedule.initialize(address(0)); - } - - function test_Reverts_WhenReceivingNativeTokens() public { - (bool success, ) = address(celoDistributionSchedule).call{ value: 1 ether }(""); - assertFalse(success); - - address payable payableAddress = payable((address(celoDistributionSchedule))); - - bool success2 = payableAddress.send(1 ether); - assertFalse(success2); - - vm.expectRevert(); - payableAddress.transfer(1 ether); - } -} - -contract CeloDistributionScheduleTest_activate_L1 is CeloDistributionScheduleTest { - function setUp() public override { - super.setUpL1(); - - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - } - - function test_Reverts_WhenCalledOnL1() public { - vm.warp(block.timestamp + l2StartTime); - vm.expectRevert("This method is not supported in L1."); - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } -} - -contract CeloDistributionScheduleTest_activate is CeloDistributionScheduleTest { - function test_ShouldHaveZeroTotalDistributedByScheduleOnInit() public { - newCeloDistibutionSchedule(); - assertEq(celoDistributionSchedule.totalDistributedBySchedule(), 0); - } - - function test_ShouldUpdateDependencies() public { - newCeloDistibutionSchedule(); - assertEq(celoDistributionSchedule.l2StartTime(), l2StartTime); - assertEq(celoDistributionSchedule.totalAllocatedAtL2Start(), L1_MINTED_CELO_SUPPLY); - assertEq(celoDistributionSchedule.communityRewardFund(), address(governance)); - assertEq(celoDistributionSchedule.carbonOffsettingPartner(), carbonOffsettingPartner); - assertEq(celoDistributionSchedule.getCarbonOffsettingFraction(), carbonOffsettingFraction); - assertEq(celoDistributionSchedule.getCommunityRewardFraction(), communityRewardFraction); - } - - function test_Reverts_WhenCommunityFractionIsZero() public { - vm.warp(block.timestamp + l2StartTime); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - vm.deal(address(celoDistributionSchedule), L2_INITIAL_STASH_BALANCE); - - vm.expectRevert( - "Value must be different from existing community reward fraction and less than 1." - ); - celoDistributionSchedule.activate( - l2StartTime, - 0, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenCarbonOffsettingPartnerIsNullAddress() public { - vm.warp(block.timestamp + l2StartTime); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - vm.deal(address(celoDistributionSchedule), L2_INITIAL_STASH_BALANCE); - - vm.expectRevert("Partner cannot be the zero address."); - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - address(0), - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenRegistryNotUpdated() public { - vm.warp(block.timestamp + l2StartTime); - registry.setAddressFor("Governance", address(0)); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - vm.deal(address(celoDistributionSchedule), L2_INITIAL_STASH_BALANCE); - celoDistributionSchedule.initialize(PROXY_ADMIN_ADDRESS); - - vm.expectRevert("identifier has no registry entry"); - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenCalledTwice() public { - newCeloDistibutionSchedule(); - vm.expectRevert("Contract has already been activated."); - - vm.prank(celoDistributionOwner); - - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenTheContractDoesNotHaveBalance() public { - vm.warp(block.timestamp + l2StartTime); - vm.prank(celoDistributionOwner); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - vm.deal(address(celoDistributionSchedule), L2_INITIAL_STASH_BALANCE); - - vm.prank(celoDistributionOwner); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.deal(address(celoDistributionSchedule), 0); - - vm.expectRevert("Contract does not have CELO balance."); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenCeloDistributionAddressNotSetInRegistry() public { - vm.warp(block.timestamp + l2StartTime); - vm.prank(celoDistributionOwner); - celoDistributionSchedule = new CeloDistributionSchedule(true); - - vm.deal(address(celoDistributionSchedule), L2_INITIAL_STASH_BALANCE); - - vm.prank(celoDistributionOwner); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.expectRevert("identifier has no registry entry"); - - vm.prank(celoDistributionOwner); - - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenCeloDistributionAddressIncorrectlySetInRegistry() public { - vm.warp(block.timestamp + l2StartTime); - vm.prank(celoDistributionOwner); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", randomAddress); - vm.deal(address(celoDistributionSchedule), L2_INITIAL_STASH_BALANCE); - - vm.prank(celoDistributionOwner); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.expectRevert("CeloDistributionSchedule address is incorrectly set in Registry."); - - vm.prank(celoDistributionOwner); - - celoDistributionSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } -} - -contract CeloDistributionScheduleTest_setCommunityRewardFraction is CeloDistributionScheduleTest { - function setUp() public override { - super.setUp(); - newCeloDistibutionSchedule(); - } - function test_ShouldSetNewFraction() public { - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCommunityRewardFraction(newCommunityRewardFraction); - assertEq(celoDistributionSchedule.getCommunityRewardFraction(), newCommunityRewardFraction); - } - function test_Emits_CommunityRewardFractionSetEvent() public { - vm.expectEmit(true, true, true, true); - emit CommunityRewardFractionSet(newCommunityRewardFraction); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCommunityRewardFraction(newCommunityRewardFraction); - } - function test_Reverts_WhenCalledByOtherThanOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(randomAddress); - celoDistributionSchedule.setCommunityRewardFraction(newCommunityRewardFraction); - } - function test_Reverts_WhenFractionIsTheSame() public { - vm.expectRevert( - "Value must be different from existing community reward fraction and less than 1." - ); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCommunityRewardFraction(communityRewardFraction); - } - function test_Reverts_WhenSumOfFractionsGtOne() public { - vm.expectRevert("Sum of partner fractions must be less than or equal to 1."); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCommunityRewardFraction((FIXED1 - 1)); - } - function test_Reverts_WhenDependenciesNotSet() public { - vm.prank(celoDistributionOwner); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.expectRevert("Distribution schedule has not been activated."); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCommunityRewardFraction(communityRewardFraction); - } - function test_Reverts_WhenFractionChangesAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR + 4 * DAY)); - - assertEq( - celoDistributionSchedule.totalDistributedBySchedule(), - 0, - "Incorrect distributableAmount" - ); - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - vm.warp(block.timestamp + (15 * YEAR) + (4 * DAY)); - - vm.expectRevert( - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCommunityRewardFraction(((FIXED1 / 4) * 3)); - } -} - -contract CeloDistributionScheduleTest_setCarbonOffsettingFund is CeloDistributionScheduleTest { - function setUp() public override { - super.setUp(); - newCeloDistibutionSchedule(); - } - - function test_ShouldSetNewPartner() public { - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCarbonOffsettingFund(newPartner, carbonOffsettingFraction); - assertEq(celoDistributionSchedule.carbonOffsettingPartner(), newPartner); - } - function test_ShouldSetNewFraction() public { - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCarbonOffsettingFund( - carbonOffsettingPartner, - newCarbonOffsettingFraction - ); - assertEq(celoDistributionSchedule.getCarbonOffsettingFraction(), newCarbonOffsettingFraction); - } - - function test_Emits_CarbonOffsettingFundSetEvent() public { - vm.expectEmit(true, true, true, true); - emit CarbonOffsettingFundSet(newPartner, carbonOffsettingFraction); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCarbonOffsettingFund(newPartner, carbonOffsettingFraction); - } - - function test_Reverts_WhenCalledByOtherThanOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(randomAddress); - celoDistributionSchedule.setCarbonOffsettingFund(newPartner, carbonOffsettingFraction); - } - - function test_Reverts_WhenPartnerAndFractionAreTheSame() public { - vm.expectRevert("Partner and value must be different from existing carbon offsetting fund."); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCarbonOffsettingFund( - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenSumOfFractionsGtOne() public { - vm.expectRevert("Sum of partner fractions must be less than or equal to 1."); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCarbonOffsettingFund(carbonOffsettingPartner, (FIXED1 - 1)); - } - - function test_Reverts_WhenDependenciesNotSet() public { - vm.prank(celoDistributionOwner); - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.expectRevert("Distribution schedule has not been activated."); - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCarbonOffsettingFund( - carbonOffsettingPartner, - carbonOffsettingFraction - ); - } - - function test_Reverts_WhenFractionChangesAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR + 4 * DAY)); - - assertEq( - celoDistributionSchedule.totalDistributedBySchedule(), - 0, - "Incorrect distributableAmount" - ); - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - vm.warp(block.timestamp + (15 * YEAR) + (4 * DAY)); - - vm.expectRevert( - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - - vm.prank(celoDistributionOwner); - celoDistributionSchedule.setCarbonOffsettingFund(carbonOffsettingPartner, ((FIXED1 / 4) * 3)); - } -} - -contract CeloDistributionScheduleTest_distributeAccordingToSchedule_L1 is - CeloDistributionScheduleTest -{ - function setUp() public override { - super.setUpL1(); - - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - } - - function test_Reverts_WhenDistributingOnL1() public { - vm.warp(block.timestamp + 3 * MONTH + 1 * DAY); - - vm.expectRevert("This method is not supported in L1."); - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - } -} - -contract CeloDistributionScheduleTest_distributeAccordingToSchedule is - CeloDistributionScheduleTest -{ - function setUp() public override { - super.setUp(); - - newCeloDistibutionSchedule(); - } - - function test_Reverts_WhenDependenciesAreNotSet() public { - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.expectRevert("Distribution schedule has not been activated."); - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - } - - function test_ShouldAllowDistributingAsSoon1SecondAfterSettingDependencies() public { - uint256 communityFundBalanceBefore = celoToken.balanceOf(address(governance)); - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - uint256 communityFundBalanceAfter = celoToken.balanceOf(address(governance)); - assertGt(communityFundBalanceAfter, communityFundBalanceBefore); - } - - function test_Reverts_WhenDistributableAmountIsZero() public { - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - vm.expectRevert("Distributable amount must be greater than zero."); - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - } - - function test_ShouldAllowToDistribute25Percent2years9MonthsPostL2Launch() public { - vm.warp(block.timestamp + 2 * YEAR + 267 * DAY + 63868); // 25% time since L2 - - uint256 expectedDistributedAmount = (L2_FIFTEEN_YEAR_CELO_SUPPLY - L1_MINTED_CELO_SUPPLY) / 4; - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - assertApproxEqRel( - celoDistributionSchedule.totalDistributedBySchedule(), - expectedDistributedAmount, - 1e10 - ); - } - - function test_ShouldAllowToDistribute50Percent5AndHalfYearsPostL2Launch() public { - vm.warp(block.timestamp + (5 * YEAR) + (170 * DAY) + 41338); - - uint256 expectedDistributedAmount = (L2_FIFTEEN_YEAR_CELO_SUPPLY - L1_MINTED_CELO_SUPPLY) / 2; - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - assertApproxEqRel( - celoDistributionSchedule.totalDistributedBySchedule(), - expectedDistributedAmount, - 1e10 - ); - } - - function test_ShouldAllowToDistribute75Percent11YearsAnd3MonthsPostL2Launch() public { - vm.warp(block.timestamp + 8 * YEAR + 73 * DAY + 18807); - - uint256 expectedDistributedAmount = ((L2_FIFTEEN_YEAR_CELO_SUPPLY - L1_MINTED_CELO_SUPPLY) / - 4) * 3; - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - assertApproxEqRel( - celoDistributionSchedule.totalDistributedBySchedule(), - expectedDistributedAmount, - 1e10 - ); - } - - function test_ShouldAllowToDistribute100Percent11YearsPostL2Launch() public { - uint256 communityFundBalanceBefore = celoToken.balanceOf(address(governance)); - uint256 carbonOffsettingPartnerBalanceBefore = celoToken.balanceOf(carbonOffsettingPartner); - vm.warp(block.timestamp + (11 * YEAR)); - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - assertApproxEqRel( - celoDistributionSchedule.totalDistributedBySchedule(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - - uint256 communityFundBalanceAfter = celoToken.balanceOf(address(governance)); - uint256 carbonOffsettingPartnerBalanceAfter = celoToken.balanceOf(carbonOffsettingPartner); - - assertApproxEqRel( - communityFundBalanceAfter - communityFundBalanceBefore, - MAX_L2_COMMUNITY_DISTRIBUTION, - 1e10 - ); - - assertApproxEqRel( - carbonOffsettingPartnerBalanceAfter - carbonOffsettingPartnerBalanceBefore, - MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - } - - function test_ShouldDistributeUpToLinearSuppplyAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR) + (4 * DAY)); - - assertEq( - celoDistributionSchedule.totalDistributedBySchedule(), - 0, - "Incorrect distributableAmount" - ); - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - assertApproxEqRel( - celoDistributionSchedule.totalDistributedBySchedule(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - } - - function test_Reverts_WhenDistributingSecondTimeAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR) + (1 * DAY)); - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - assertApproxEqRel( - celoDistributionSchedule.totalDistributedBySchedule(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - - vm.expectRevert("Block reward calculation for years 15-30 unimplemented"); - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - } - - function test_Reverts_WhenTheContractBalanceIsLowerExpected() public { - vm.deal(address(celoDistributionSchedule), 0); - vm.prank(address(celoDistributionSchedule)); - - vm.expectRevert("Contract balance is insufficient."); - celoDistributionSchedule.distributeAccordingToSchedule(); - } - - function test_ShouldTransferbalanceFromThisContract() public { - uint256 initialStashBalance = celoToken.balanceOf(address(celoDistributionSchedule)); - - vm.warp(block.timestamp + (15 * YEAR)); - - celoDistributionSchedule.distributeAccordingToSchedule(); - - uint256 finalStashBalance = celoToken.balanceOf(address(celoDistributionSchedule)); - - assertLt(finalStashBalance, initialStashBalance); - - assertApproxEqRel( - celoToken.balanceOf(address(celoDistributionSchedule)), - L2_INITIAL_STASH_BALANCE - (MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION), - 1e10 - ); - } -} - -contract CeloDistributionScheduleTest_getDistributableAmount is CeloDistributionScheduleTest { - function setUp() public override { - super.setUp(); - - newCeloDistibutionSchedule(); - } - - function test_ShouldReturnFullAmountAvailableForThisReleasePeriod() public { - vm.warp(block.timestamp + 1 * DAY); - assertApproxEqRel( - celoDistributionSchedule.getDistributableAmount(), - DAILY_DISTRIBUTION_AMOUNT, - 1e10 - ); - } - - function test_ShouldReturnOnlyAmountNotYetDistributed() public { - vm.warp(block.timestamp + 1 * DAY); - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - - vm.warp(block.timestamp + 1 * DAY + 1); - assertApproxEqRel( - celoDistributionSchedule.getDistributableAmount(), - DAILY_DISTRIBUTION_AMOUNT, - 1e10 - ); - } - - function test_ShouldReturnOnlyUpToMaxL2DistributionBeforeItIsDistributed() public { - vm.warp(block.timestamp + 16 * YEAR); - assertApproxEqRel( - celoDistributionSchedule.getDistributableAmount(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - } - - function test_Reverts_When15YearsHavePassedAndAllLinearScheduleHaseBeenReleased() public { - vm.warp(block.timestamp + 15 * YEAR); - - vm.prank(randomAddress); - celoDistributionSchedule.distributeAccordingToSchedule(); - vm.expectRevert("Block reward calculation for years 15-30 unimplemented"); - celoDistributionSchedule.getDistributableAmount(); - } - - function test_Reverts_WhenDependenciesNotSet() public { - celoDistributionSchedule = new CeloDistributionSchedule(true); - registry.setAddressFor("CeloDistributionSchedule", address(celoDistributionSchedule)); - celoDistributionSchedule.initialize(REGISTRY_ADDRESS); - - vm.expectRevert("Distribution schedule has not been activated."); - - celoDistributionSchedule.getDistributableAmount(); - } -} diff --git a/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasury.t.sol b/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasury.t.sol new file mode 100644 index 00000000000..396044b7bf9 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasury.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; +pragma experimental ABIEncoderV2; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/common/interfaces/ICeloToken.sol"; +import "@celo-contracts/governance/interfaces/IGovernance.sol"; +import { CeloUnreleasedTreasury } from "@celo-contracts-8/common/CeloUnreleasedTreasury.sol"; +import "@celo-contracts-8/common/IsL2Check.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + +import "@test-sol/unit/governance/mock/MockGovernance.sol"; + +contract CeloUnreleasedTreasuryTest is Test, TestConstants, IsL2Check { + using FixidityLib for FixidityLib.Fraction; + + IRegistry registry; + ICeloToken celoToken; + MockGovernance governance; + + CeloUnreleasedTreasury celoUnreleasedTreasury; + + address owner = address(this); + + address celoTokenAddress = actor("celoTokenAddress"); + address epochManagerAddress = actor("epochManagerAddress"); + + address celoDistributionOwner = actor("celoDistributionOwner"); + address communityRewardFund = actor("communityRewardFund"); + address carbonOffsettingPartner = actor("carbonOffsettingPartner"); + + address newPartner = actor("newPartner"); + address randomAddress = actor("randomAddress"); + + uint256 constant DAILY_DISTRIBUTION_AMOUNT = 6748256563599655349558; // 6,748 Celo + + uint256 constant MAX_L2_COMMUNITY_DISTRIBUTION = MAX_L2_DISTRIBUTION / 4; // 26.8 million Celo + uint256 constant MAX_L2_CARBON_FUND_DISTRIBUTION = MAX_L2_DISTRIBUTION / 1000; // 107,297 Celo + + uint256 constant L2_FIFTEEN_YEAR_CELO_SUPPLY = + L1_MINTED_CELO_SUPPLY + MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION; + + uint256 constant l2StartTime = 1715808537; // Arbitary later date (May 15 2024) + uint256 constant communityRewardFraction = FIXED1 / 4; // 25% + uint256 constant carbonOffsettingFraction = FIXED1 / 1000; // 0.1% + uint256 constant newCommunityRewardFraction = FIXED1 / 2; // 50% + uint256 constant newCarbonOffsettingFraction = FIXED1 / 500; // 0.2% + + event CommunityRewardFractionSet(uint256 fraction); + event CarbonOffsettingFundSet(address indexed partner, uint256 fraction); + + function setUp() public virtual { + setUpL1(); + + // Setup L2 after minting L1 supply. + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + } + + function setUpL1() public { + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); + registry = IRegistry(REGISTRY_ADDRESS); + + deployCodeTo("GoldToken.sol", abi.encode(false), celoTokenAddress); + celoToken = ICeloToken(celoTokenAddress); + // Using a mock contract, as foundry does not allow for library linking when using deployCodeTo + governance = new MockGovernance(); + + registry.setAddressFor("CeloToken", address(celoToken)); + + registry.setAddressFor("Governance", address(governance)); + registry.setAddressFor("EpochManager", address(epochManagerAddress)); + + vm.deal(address(0), CELO_SUPPLY_CAP); + assertEq(celoToken.allocatedSupply(), 0, "starting total supply not zero."); + // Mint L1 supply + vm.prank(address(0)); + celoToken.mint(randomAddress, L1_MINTED_CELO_SUPPLY); + assertEq(celoToken.allocatedSupply(), L1_MINTED_CELO_SUPPLY, "total supply incorrect."); + } + + function newCeloUnreleasedTreasury() internal returns (CeloUnreleasedTreasury) { + vm.warp(block.timestamp + l2StartTime); + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury = new CeloUnreleasedTreasury(true); + registry.setAddressFor("CeloUnreleasedTreasury", address(celoUnreleasedTreasury)); + + vm.deal(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury.initialize(REGISTRY_ADDRESS); + } +} + +contract CeloUnreleasedTreasuryTest_initialize is CeloUnreleasedTreasuryTest { + function setUp() public override { + super.setUp(); + vm.warp(block.timestamp + l2StartTime); + + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury = new CeloUnreleasedTreasury(true); + registry.setAddressFor("CeloUnreleasedTreasury", address(celoUnreleasedTreasury)); + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury.initialize(REGISTRY_ADDRESS); + } + + function test_ShouldSetAnOwnerToCeloUnreleasedTreasuryInstance() public { + assertEq(celoUnreleasedTreasury.owner(), celoDistributionOwner); + } + + function test_ShouldSetRegistryAddressToCeloUnreleasedTreasuryInstance() public { + assertEq(address(celoUnreleasedTreasury.registry()), REGISTRY_ADDRESS); + } + + function test_Reverts_WhenRegistryIsTheNullAddress() public { + celoUnreleasedTreasury = new CeloUnreleasedTreasury(true); + registry.setAddressFor("CeloUnreleasedTreasury", address(celoUnreleasedTreasury)); + vm.expectRevert("Cannot register the null address"); + celoUnreleasedTreasury.initialize(address(0)); + } + + function test_Reverts_WhenReceivingNativeTokens() public { + (bool success, ) = address(celoUnreleasedTreasury).call{ value: 1 ether }(""); + assertFalse(success); + + address payable payableAddress = payable((address(celoUnreleasedTreasury))); + + bool success2 = payableAddress.send(1 ether); + assertFalse(success2); + + vm.expectRevert(); + payableAddress.transfer(1 ether); + } +} + +contract CeloUnreleasedTreasuryTest_release is CeloUnreleasedTreasuryTest { + function setUp() public override { + super.setUp(); + newCeloUnreleasedTreasury(); + } + + function test_ShouldTransferToRecepientAddress() public { + uint256 _balanceBefore = randomAddress.balance; + vm.prank(epochManagerAddress); + + celoUnreleasedTreasury.release(randomAddress, 4); + uint256 _balanceAfter = randomAddress.balance; + assertGt(_balanceAfter, _balanceBefore); + } + + function test_Reverts_WhenCalledByOtherThanEpochManager() public { + vm.prank(randomAddress); + + vm.expectRevert("Only the EpochManager contract can call this function."); + celoUnreleasedTreasury.release(randomAddress, 4); + } +} diff --git a/packages/protocol/test-sol/unit/common/EpochManager.t.sol b/packages/protocol/test-sol/unit/common/EpochManager.t.sol new file mode 100644 index 00000000000..638af589794 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/EpochManager.t.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts-8/common/mocks/EpochManager_WithMocks.sol"; +import "@celo-contracts-8/stability/test/MockStableToken.sol"; +import "@celo-contracts-8/common/test/MockCeloToken.sol"; +import "@celo-contracts/common/interfaces/ICeloToken.sol"; +import "@celo-contracts-8/common/ScoreManager.sol"; +import { CeloUnreleasedTreasury } from "@celo-contracts-8/common/CeloUnreleasedTreasury.sol"; +import { ICeloUnreleasedTreasury } from "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; + +import { TestConstants } from "@test-sol/constants.sol"; +import { Utils08 } from "@test-sol/utils08.sol"; + +import "@celo-contracts/stability/test/MockSortedOracles.sol"; + +import "@celo-contracts/common/interfaces/IRegistry.sol"; + +import { IMockValidators } from "@celo-contracts-8/governance/test/IMockValidators.sol"; + +import { EpochRewardsMock08 } from "@celo-contracts-8/governance/test/EpochRewardsMock.sol"; + +import { MockAccounts } from "@celo-contracts-8/common/mocks/MockAccounts.sol"; +import { ValidatorsMock } from "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; +import { MockCeloUnreleasedTreasury } from "@celo-contracts-8/common/test/MockCeloUnreleasedTreasury.sol"; + +contract EpochManagerTest is Test, TestConstants, Utils08 { + EpochManager_WithMocks epochManager; + MockSortedOracles sortedOracles; + + MockStableToken08 stableToken; + EpochRewardsMock08 epochRewards; + ValidatorsMock validators; + + address epochManagerEnabler; + address carbonOffsettingPartner; + address communityRewardFund; + address reserveAddress; + address scoreManagerAddress; + + uint256 firstEpochNumber = 100; + uint256 firstEpochBlock = 100; + uint256 epochDuration = DAY; + address[] firstElected; + + IRegistry registry; + MockCeloToken08 celoToken; + MockCeloUnreleasedTreasury celoUnreleasedTreasury; + ScoreManager scoreManager; + + uint256 celoAmountForRate = 1e24; + uint256 stableAmountForRate = 2 * celoAmountForRate; + + event ValidatorEpochPaymentDistributed( + address indexed validator, + uint256 validatorPayment, + address indexed group, + uint256 groupPayment, + address indexed beneficiary, + uint256 delegatedPayment + ); + + event EpochProcessingStarted(uint256 indexed epochNumber); + event EpochDurationSet(uint256 indexed newEpochDuration); + event OracleAddressSet(address indexed newOracleAddress); + + function setUp() public virtual { + epochManager = new EpochManager_WithMocks(); + sortedOracles = new MockSortedOracles(); + epochRewards = new EpochRewardsMock08(); + validators = new ValidatorsMock(); + stableToken = new MockStableToken08(); + celoToken = new MockCeloToken08(); + celoUnreleasedTreasury = new MockCeloUnreleasedTreasury(); + + firstElected.push(actor("validator1")); + firstElected.push(actor("validator2")); + + scoreManagerAddress = actor("scoreManagerAddress"); + + reserveAddress = actor("reserve"); + + epochManagerEnabler = actor("epochManagerEnabler"); + carbonOffsettingPartner = actor("carbonOffsettingPartner"); + communityRewardFund = actor("communityRewardFund"); + + deployCodeTo("MockRegistry.sol", abi.encode(false), REGISTRY_ADDRESS); + deployCodeTo("ScoreManager.sol", abi.encode(false), scoreManagerAddress); + + registry = IRegistry(REGISTRY_ADDRESS); + scoreManager = ScoreManager(scoreManagerAddress); + + registry.setAddressFor(EpochManagerContract, address(epochManager)); + registry.setAddressFor(EpochManagerEnablerContract, epochManagerEnabler); + registry.setAddressFor(SortedOraclesContract, address(sortedOracles)); + registry.setAddressFor(GovernanceContract, communityRewardFund); + registry.setAddressFor(EpochRewardsContract, address(epochRewards)); + registry.setAddressFor(ValidatorsContract, address(validators)); + registry.setAddressFor(ScoreManagerContract, address(scoreManager)); + registry.setAddressFor(StableTokenContract, address(stableToken)); + registry.setAddressFor(CeloUnreleasedTreasuryContract, address(celoUnreleasedTreasury)); + registry.setAddressFor(CeloTokenContract, address(celoToken)); + registry.setAddressFor(ReserveContract, reserveAddress); + + celoToken.setTotalSupply(CELO_SUPPLY_CAP); + vm.deal(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + celoToken.setBalanceOf(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + + celoUnreleasedTreasury.setRegistry(REGISTRY_ADDRESS); + validators.setRegistry(REGISTRY_ADDRESS); + + sortedOracles.setMedianRate(address(stableToken), stableAmountForRate); + + scoreManager.setValidatorScore(actor("validator1"), 1); + + epochManager.initialize(REGISTRY_ADDRESS, 10); + + blockTravel(vm, firstEpochBlock); + } + + function initializeEpochManagerSystem() public { + deployCodeTo("MockRegistry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + + blockTravel(vm, 43200); + timeTravel(vm, DAY); + } +} + +contract EpochManagerTest_initialize is EpochManagerTest { + function test_initialize() public virtual { + assertEq(address(epochManager.registry()), REGISTRY_ADDRESS); + assertEq(epochManager.epochDuration(), 10); + assertEq(epochManager.oracleAddress(), address(sortedOracles)); + } + + function test_Reverts_WhenAlreadyInitialized() public virtual { + vm.expectRevert("contract already initialized"); + epochManager.initialize(REGISTRY_ADDRESS, 10); + } +} + +contract EpochManagerTest_initializeSystem is EpochManagerTest { + function test_processCanBeStarted() public virtual { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + ( + uint256 _firstEpochBlock, + uint256 _lastEpochBlock, + uint256 _startTimestamp, + uint256 _currentRewardsBlock + ) = epochManager.getCurrentEpoch(); + assertGt(epochManager.getElected().length, 0); + assertEq(epochManager.firstKnownEpoch(), firstEpochNumber); + assertEq(_firstEpochBlock, firstEpochBlock); + assertEq(_lastEpochBlock, 0); + assertEq(_startTimestamp, block.timestamp); + assertEq(_currentRewardsBlock, 0); + assertEq(epochManager.getElected(), firstElected); + } + + function test_Reverts_processCannotBeStartedAgain() public virtual { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + vm.prank(epochManagerEnabler); + vm.expectRevert("Epoch system already initialized"); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + } + + function test_Reverts_WhenSystemInitializedByOtherContract() public virtual { + vm.expectRevert("msg.sender is not Enabler"); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + } +} + +contract EpochManagerTest_startNextEpochProcess is EpochManagerTest { + function test_Reverts_whenSystemNotInitialized() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_WhenEndOfEpochHasNotBeenReached() public { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + + vm.expectRevert("Epoch is not ready to start"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_WhenEpochProcessingAlreadyStarted() public { + initializeEpochManagerSystem(); + + epochManager.startNextEpochProcess(); + vm.expectRevert("Epoch process is already started"); + epochManager.startNextEpochProcess(); + } + + function test_Emits_EpochProcessingStartedEvent() public { + initializeEpochManagerSystem(); + + vm.expectEmit(true, true, true, true); + emit EpochProcessingStarted(firstEpochNumber); + epochManager.startNextEpochProcess(); + } + + function test_SetsTheEpochRewardBlock() public { + initializeEpochManagerSystem(); + + epochManager.startNextEpochProcess(); + (, , , uint256 _currentRewardsBlock) = epochManager.getCurrentEpoch(); + assertEq(_currentRewardsBlock, block.number); + } + + function test_SetsTheEpochRewardAmounts() public { + initializeEpochManagerSystem(); + + epochManager.startNextEpochProcess(); + ( + uint256 status, + uint256 perValidatorReward, + uint256 totalRewardsVoter, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + assertEq(status, 1); + assertEq(perValidatorReward, 5); + assertEq(totalRewardsVoter, 5); + assertEq(totalRewardsCommunity, 5); + assertEq(totalRewardsCarbonFund, 5); + } + + function test_ShouldMintTotalValidatorStableRewardsToEpochManager() public { + initializeEpochManagerSystem(); + uint256 beforeBalance = stableToken.balanceOf(address(epochManager)); + epochManager.startNextEpochProcess(); + + uint256 afterBalance = stableToken.balanceOf(address(epochManager)); + assertEq(afterBalance, 2); + } + + function test_ShouldReleaseCorrectAmountToReserve() public { + initializeEpochManagerSystem(); + uint256 reserveBalanceBefore = celoToken.balanceOf(reserveAddress); + epochManager.startNextEpochProcess(); + uint256 reserveBalanceAfter = celoToken.balanceOf(reserveAddress); + assertEq(reserveBalanceAfter, reserveBalanceBefore + 4); + } +} + +contract EpochManagerTest_setEpochDuration is EpochManagerTest { + uint256 newEpochDuration = 5 * DAY; + + function test_setsNewEpochDuration() public { + initializeEpochManagerSystem(); + epochManager.setEpochDuration(newEpochDuration); + assertEq(epochManager.epochDuration(), newEpochDuration); + } + + function test_Emits_EpochDurationSetEvent() public { + initializeEpochManagerSystem(); + + vm.expectEmit(true, true, true, true); + emit EpochDurationSet(newEpochDuration); + epochManager.setEpochDuration(newEpochDuration); + } + + function test_Reverts_WhenIsOnEpochProcess() public { + initializeEpochManagerSystem(); + epochManager.startNextEpochProcess(); + vm.expectRevert("Cannot change epoch duration during processing."); + epochManager.setEpochDuration(newEpochDuration); + } + + function test_Reverts_WhenNewEpochDurationIsZero() public { + initializeEpochManagerSystem(); + + vm.expectRevert("New epoch duration must be greater than zero."); + epochManager.setEpochDuration(0); + } +} + +contract EpochManagerTest_setOracleAddress is EpochManagerTest { + address newOracleAddress = actor("newOarcle"); + + function test_setsNewOracleAddress() public { + initializeEpochManagerSystem(); + epochManager.setOracleAddress(newOracleAddress); + assertEq(epochManager.oracleAddress(), newOracleAddress); + } + + function test_Emits_OracleAddressSetEvent() public { + initializeEpochManagerSystem(); + + vm.expectEmit(true, true, true, true); + emit OracleAddressSet(newOracleAddress); + epochManager.setOracleAddress(newOracleAddress); + } + + function test_Reverts_WhenIsOnEpochProcess() public { + initializeEpochManagerSystem(); + epochManager.startNextEpochProcess(); + vm.expectRevert("Cannot change oracle address during epoch processing."); + epochManager.setOracleAddress(newOracleAddress); + } + + function test_Reverts_WhenNewOracleAddressIsZero() public { + initializeEpochManagerSystem(); + + vm.expectRevert("Cannot set address zero as the Oracle."); + epochManager.setOracleAddress(address(0)); + } + + function test_Reverts_WhenNewOracleAddressIsunchanged() public { + initializeEpochManagerSystem(); + + vm.expectRevert("Oracle address cannot be the same."); + epochManager.setOracleAddress(address(sortedOracles)); + } +} + +contract EpochManagerTest_sendValidatorPayment is EpochManagerTest { + address group = actor("group"); + address validator1 = actor("validator1"); + address signer1 = actor("signer1"); + address validator2 = actor("validator2"); + address signer2 = actor("signer2"); + address beneficiary = actor("beneficiary"); + + uint256 paymentAmount = 4 ether; + uint256 quarterOfPayment = paymentAmount / 4; + uint256 halfOfPayment = paymentAmount / 2; + uint256 threeQuartersOfPayment = (paymentAmount / 4) * 3; + uint256 twentyFivePercent = 250000000000000000000000; + uint256 fiftyPercent = 500000000000000000000000; + + uint256 epochManagerBalanceBefore; + + // TODO: unify mocks + IMockValidators mockValidators = IMockValidators(actor("MockValidators05")); + + MockAccounts accounts; + + function setUp() public override { + super.setUp(); + + deployCodeTo("MockValidators.sol", abi.encode(false), address(mockValidators)); + registry.setAddressFor(ValidatorsContract, address(mockValidators)); + + accounts = new MockAccounts(); + registry.setAddressFor(AccountsContract, address(accounts)); + + mockValidators.setValidatorGroup(group); + mockValidators.setValidator(validator1); + accounts.setValidatorSigner(validator1, signer1); + mockValidators.setValidator(validator2); + accounts.setValidatorSigner(validator2, signer2); + + address[] memory members = new address[](3); + members[0] = validator1; + members[1] = validator2; + mockValidators.setMembers(group, members); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + + stableToken.mint(address(epochManager), paymentAmount * 2); + epochManagerBalanceBefore = stableToken.balanceOf(address(epochManager)); + epochManager._setPaymentAllocation(validator1, paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidator() public { + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, paymentAmount); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidatorAndGroup() public { + mockValidators.setCommission(group, twentyFivePercent); + + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 groupBalanceAfter = stableToken.balanceOf(group); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, threeQuartersOfPayment); + assertEq(groupBalanceAfter, quarterOfPayment); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidatorAndBeneficiary() public { + accounts.setPaymentDelegationFor(validator1, beneficiary, twentyFivePercent); + + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 beneficiaryBalanceAfter = stableToken.balanceOf(beneficiary); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, threeQuartersOfPayment); + assertEq(beneficiaryBalanceAfter, quarterOfPayment); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidatorAndGroupAndBeneficiary() public { + mockValidators.setCommission(group, fiftyPercent); + accounts.setPaymentDelegationFor(validator1, beneficiary, fiftyPercent); + + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 groupBalanceAfter = stableToken.balanceOf(group); + uint256 beneficiaryBalanceAfter = stableToken.balanceOf(beneficiary); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, quarterOfPayment); + assertEq(groupBalanceAfter, halfOfPayment); + assertEq(beneficiaryBalanceAfter, quarterOfPayment); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_emitsAValidatorEpochPaymentDistributedEvent() public { + mockValidators.setCommission(group, fiftyPercent); + accounts.setPaymentDelegationFor(validator1, beneficiary, fiftyPercent); + + vm.expectEmit(true, true, true, true, address(epochManager)); + emit ValidatorEpochPaymentDistributed( + validator1, + quarterOfPayment, + group, + halfOfPayment, + beneficiary, + quarterOfPayment + ); + epochManager.sendValidatorPayment(validator1); + } + + function test_doesNothingIfNotAllocated() public { + mockValidators.setCommission(group, fiftyPercent); + accounts.setPaymentDelegationFor(validator2, beneficiary, fiftyPercent); + + epochManager.sendValidatorPayment(validator2); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 groupBalanceAfter = stableToken.balanceOf(group); + uint256 beneficiaryBalanceAfter = stableToken.balanceOf(beneficiary); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, 0); + assertEq(groupBalanceAfter, 0); + assertEq(beneficiaryBalanceAfter, 0); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore); + } + + function test_doesntAllowDoubleSending() public { + epochManager.sendValidatorPayment(validator1); + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, paymentAmount); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } +} diff --git a/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol b/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol new file mode 100644 index 00000000000..36c348cb3fa --- /dev/null +++ b/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts-8/common/EpochManager.sol"; + +import { EpochManagerEnablerMock } from "@test-sol/mocks/EpochManagerEnablerMock.sol"; + +import { CeloUnreleasedTreasury } from "@celo-contracts-8/common/CeloUnreleasedTreasury.sol"; +import { ICeloUnreleasedTreasury } from "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import { IAccounts } from "@celo-contracts/common/interfaces/IAccounts.sol"; + +import { TestConstants } from "@test-sol/constants.sol"; +import { Utils08 } from "@test-sol/utils08.sol"; + +import "@celo-contracts/common/interfaces/IRegistry.sol"; + +import { EpochRewardsMock08 } from "@celo-contracts-8/governance/test/EpochRewardsMock.sol"; +import { ValidatorsMock } from "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; +import { MockCeloUnreleasedTreasury } from "@celo-contracts-8/common/test/MockCeloUnreleasedTreasury.sol"; +import "@celo-contracts-8/common/test/MockCeloToken.sol"; + +contract EpochManagerEnablerTest is Test, TestConstants, Utils08 { + EpochManager epochManager; + EpochManagerEnablerMock epochManagerEnabler; + MockCeloUnreleasedTreasury celoUnreleasedTreasury; + MockCeloToken08 celoToken; + + IRegistry registry; + IAccounts accounts; + + address accountsAddress; + address nonOwner; + address oracle; + + uint256 epochDuration = DAY; + uint256 numberValidators = 100; + + event LastKnownEpochNumberSet(uint256 lastKnownEpochNumber); + event LastKnownFirstBlockOfEpochSet(uint256 lastKnownFirstBlockOfEpoch); + event LastKnownElectedAccountsSet(); + + function setUp() public virtual { + ph.setEpochSize(17280); + epochManager = new EpochManager(true); + epochManagerEnabler = new EpochManagerEnablerMock(); + celoToken = new MockCeloToken08(); + + celoUnreleasedTreasury = new MockCeloUnreleasedTreasury(); + + accountsAddress = actor("accountsAddress"); + + nonOwner = actor("nonOwner"); + oracle = actor("oracle"); + + deployCodeTo("MockRegistry.sol", abi.encode(false), REGISTRY_ADDRESS); + deployCodeTo("Accounts.sol", abi.encode(false), accountsAddress); + + registry = IRegistry(REGISTRY_ADDRESS); + accounts = IAccounts(accountsAddress); + + registry.setAddressFor(EpochManagerContract, address(epochManager)); + registry.setAddressFor(EpochManagerEnablerContract, address(epochManagerEnabler)); + registry.setAddressFor(AccountsContract, address(accounts)); + registry.setAddressFor(CeloTokenContract, address(celoToken)); + registry.setAddressFor(SortedOraclesContract, oracle); + registry.setAddressFor(CeloUnreleasedTreasuryContract, address(celoUnreleasedTreasury)); + + celoToken.setTotalSupply(CELO_SUPPLY_CAP); + celoToken.setBalanceOf(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + + epochManagerEnabler.initialize(REGISTRY_ADDRESS); + epochManager.initialize(REGISTRY_ADDRESS, epochDuration); + + _setupValidators(); + travelEpochL1(vm); + travelEpochL1(vm); + } + + function _setupValidators() internal { + for (uint256 i = 0; i < numberValidators; i++) { + vm.prank(vm.addr(i + 1)); + accounts.createAccount(); + + epochManagerEnabler.addValidator(vm.addr(i + 1)); + } + } +} + +contract EpochManagerEnablerTest_initialize is EpochManagerEnablerTest { + function test_initialize() public { + assertEq(address(epochManagerEnabler.registry()), REGISTRY_ADDRESS); + } + + function test_Reverts_WhenAlreadyInitialized() public virtual { + vm.expectRevert("contract already initialized"); + epochManagerEnabler.initialize(REGISTRY_ADDRESS); + } +} + +contract EpochManagerEnablerTest_initEpochManager is EpochManagerEnablerTest { + function test_CanBeCalledByAnyone() public { + epochManagerEnabler.captureEpochAndValidators(); + + whenL2(vm); + vm.prank(nonOwner); + epochManagerEnabler.initEpochManager(); + + assertGt(epochManager.getElected().length, 0); + assertTrue(epochManager.systemAlreadyInitialized()); + } + + function test_Reverts_ifEpochAndValidatorsAreNotCaptured() public { + whenL2(vm); + vm.expectRevert("lastKnownEpochNumber not set."); + + epochManagerEnabler.initEpochManager(); + } + + function test_Reverts_whenL1() public { + vm.expectRevert("This method is not supported in L1."); + + epochManagerEnabler.initEpochManager(); + } +} + +contract EpochManagerEnablerTest_captureEpochAndValidators is EpochManagerEnablerTest { + function test_Reverts_whenL2() public { + whenL2(vm); + vm.expectRevert("This method is no longer supported in L2."); + epochManagerEnabler.captureEpochAndValidators(); + } + + function test_shouldSetLastKnownElectedAccounts() public { + epochManagerEnabler.captureEpochAndValidators(); + + assertEq(epochManagerEnabler.getlastKnownElectedAccounts().length, numberValidators); + } + + function test_shouldSetLastKnownEpochNumber() public { + epochManagerEnabler.captureEpochAndValidators(); + + assertEq(epochManagerEnabler.lastKnownEpochNumber(), 3); + } + + function test_shouldSetLastKnownFirstBlockOfEpoch() public { + epochManagerEnabler.captureEpochAndValidators(); + + assertEq(epochManagerEnabler.lastKnownFirstBlockOfEpoch(), 17280 * 2); + } + + function test_Emits_LastKnownEpochNumberSet() public { + vm.expectEmit(true, true, true, true); + emit LastKnownEpochNumberSet(3); + + epochManagerEnabler.captureEpochAndValidators(); + } + + function test_Emits_LastKnownElectedAccountsSet() public { + vm.expectEmit(true, true, true, true); + emit LastKnownElectedAccountsSet(); + + epochManagerEnabler.captureEpochAndValidators(); + } + + function test_Emits_LastKnownFirstBlockOfEpochSet() public { + vm.expectEmit(true, true, true, true); + emit LastKnownFirstBlockOfEpochSet(34560); + + epochManagerEnabler.captureEpochAndValidators(); + } +} + +contract EpochManagerEnablerTest_getFirstBlockOfEpoch is EpochManagerEnablerTest { + function test_blockIsEpockBlock() public { + vm.roll(27803520); + epochManagerEnabler.setFirstBlockOfEpoch(); + assertEq(epochManagerEnabler.lastKnownFirstBlockOfEpoch(), 27803520); + } + + function test_blockIsNotEpochBlock() public { + vm.roll(27817229); + epochManagerEnabler.setFirstBlockOfEpoch(); + assertEq(epochManagerEnabler.lastKnownFirstBlockOfEpoch(), 27803520); + } +} diff --git a/packages/protocol/test-sol/unit/common/FeeHandler.t.sol b/packages/protocol/test-sol/unit/common/FeeHandler.t.sol index 3ae1ce07a48..2cbdfbeeeb8 100644 --- a/packages/protocol/test-sol/unit/common/FeeHandler.t.sol +++ b/packages/protocol/test-sol/unit/common/FeeHandler.t.sol @@ -19,6 +19,8 @@ import "@celo-contracts/uniswap/test/MockUniswapV2Factory.sol"; import "@celo-contracts/uniswap/test/MockERC20.sol"; import "@mento-core/test/mocks/MockSortedOracles.sol"; import "@mento-core/test/mocks/MockReserve.sol"; +import "@celo-contracts/common/ProxyFactory.sol"; +import "@celo-contracts/governance/GovernanceApproverMultiSig.sol"; contract FeeHandlerTest is Test, TestConstants { using FixidityLib for FixidityLib.Fraction; @@ -51,7 +53,7 @@ contract FeeHandlerTest is Test, TestConstants { address owner = address(this); address user = actor("user"); - address celoDistributionSchedule = actor("CeloDistributionSchedule"); + address celoUnreleasedTreasury = actor("CeloUnreleasedTreasury"); uint256 celoAmountForRate = 1e24; uint256 stableAmountForRate = 2 * celoAmountForRate; @@ -102,7 +104,7 @@ contract FeeHandlerTest is Test, TestConstants { registry.setAddressFor("GoldToken", address(celoToken)); registry.setAddressFor("CeloToken", address(celoToken)); registry.setAddressFor("Reserve", address(mockReserve)); - registry.setAddressFor("CeloDistributionSchedule", celoDistributionSchedule); + registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasury); mockReserve.setGoldToken(address(celoToken)); mockReserve.addToken(address(stableToken)); diff --git a/packages/protocol/test-sol/unit/common/GoldToken.t.sol b/packages/protocol/test-sol/unit/common/GoldToken.t.sol index 80c49c0930d..a55358be7c6 100644 --- a/packages/protocol/test-sol/unit/common/GoldToken.t.sol +++ b/packages/protocol/test-sol/unit/common/GoldToken.t.sol @@ -35,7 +35,7 @@ contract GoldTokenTest is Test, TestConstants, IsL2Check { celoToken = new GoldToken(true); vm.prank(celoTokenOwner); celoToken.setRegistry(REGISTRY_ADDRESS); - registry.setAddressFor("CeloDistributionSchedule", celoTokenDistributionSchedule); + registry.setAddressFor("CeloUnreleasedTreasury", celoTokenDistributionSchedule); receiver = actor("receiver"); sender = actor("sender"); vm.deal(receiver, ONE_CELOTOKEN); @@ -126,11 +126,6 @@ contract GoldTokenTest_transfer is GoldTokenTest { vm.expectRevert(); celoToken.transfer(address(0), ONE_CELOTOKEN); } - function test_Reverts_WhenTransferingToCeloDistributionSchedule() public { - vm.prank(sender); - vm.expectRevert("transfer attempted to reserved CeloDistributionSchedule address"); - celoToken.transfer(celoTokenDistributionSchedule, ONE_CELOTOKEN); - } } contract GoldTokenTest_transferFrom is GoldTokenTest { @@ -149,12 +144,6 @@ contract GoldTokenTest_transferFrom is GoldTokenTest { assertEq(receiver.balance, startBalanceTo + ONE_CELOTOKEN); } - function test_Reverts_WhenTransferingToCeloDistributionSchedule() public { - vm.prank(receiver); - vm.expectRevert("transfer attempted to reserved CeloDistributionSchedule address"); - celoToken.transferFrom(sender, celoTokenDistributionSchedule, ONE_CELOTOKEN); - } - function test_Reverts_WhenTransferToNullAddress() public { vm.prank(receiver); vm.expectRevert(); @@ -260,10 +249,11 @@ contract CeloTokenMockTest is Test, TestConstants { GoldTokenMock mockCeloToken; uint256 ONE_CELOTOKEN = 1000000000000000000; address burnAddress = address(0x000000000000000000000000000000000000dEaD); - address celoTokenDistributionSchedule; + address celoUnreleasedTreasury; modifier _whenL2() { deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + vm.deal(celoUnreleasedTreasury, L2_INITIAL_STASH_BALANCE); _; } @@ -273,9 +263,9 @@ contract CeloTokenMockTest is Test, TestConstants { mockCeloToken = new GoldTokenMock(); mockCeloToken.setRegistry(REGISTRY_ADDRESS); - mockCeloToken.setTotalSupply(ONE_CELOTOKEN * 1000); - celoTokenDistributionSchedule = actor("celoTokenDistributionSchedule"); - registry.setAddressFor("CeloDistributionSchedule", celoTokenDistributionSchedule); + mockCeloToken.setTotalSupply(L1_MINTED_CELO_SUPPLY); + celoUnreleasedTreasury = actor("CeloUnreleasedTreasury"); + registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasury); } } @@ -290,42 +280,40 @@ contract CeloTokenMock_circulatingSupply is CeloTokenMockTest { function test_ShouldDecreaseCirculatingSupply_WhenThereWasBurn() public { mockCeloToken.setBalanceOf(burnAddress, ONE_CELOTOKEN); - assertEq(mockCeloToken.circulatingSupply(), ONE_CELOTOKEN * 999); + assertEq(mockCeloToken.circulatingSupply(), mockCeloToken.allocatedSupply() - ONE_CELOTOKEN); assertEq(mockCeloToken.circulatingSupply(), mockCeloToken.totalSupply() - ONE_CELOTOKEN); } function test_ShouldMatchCirculationSupply_WhenNoBurn_WhenL2() public _whenL2 { - assertEq(mockCeloToken.circulatingSupply(), mockCeloToken.totalSupply()); + assertEq(mockCeloToken.circulatingSupply(), mockCeloToken.allocatedSupply()); } function test_ShouldDecreaseCirculatingSupply_WhenThereWasBurn_WhenL2() public _whenL2 { - uint256 CELO_SUPPLY_CAP = 1000000000 ether; // 1 billion Celo mockCeloToken.setBalanceOf(burnAddress, ONE_CELOTOKEN); - assertEq(mockCeloToken.circulatingSupply(), CELO_SUPPLY_CAP - ONE_CELOTOKEN); assertEq(mockCeloToken.circulatingSupply(), mockCeloToken.allocatedSupply() - ONE_CELOTOKEN); } } contract GoldTokenTest_AllocatedSupply is CeloTokenMockTest { - function test_ShouldRevert_WhenL1() public { - vm.expectRevert("This method is not supported in L1."); - mockCeloToken.allocatedSupply(); + function test_ShouldReturnTotalSupply_WhenL1() public { + assertEq(mockCeloToken.allocatedSupply(), L1_MINTED_CELO_SUPPLY); } function test_ShouldReturn_WhenInL2() public _whenL2 { - assertEq(mockCeloToken.allocatedSupply(), mockCeloToken.totalSupply()); + assertEq(mockCeloToken.allocatedSupply(), CELO_SUPPLY_CAP - L2_INITIAL_STASH_BALANCE); } function test_ShouldReturn_WhenWithdrawn_WhenInL2() public _whenL2 { - deal(address(celoTokenDistributionSchedule), ONE_CELOTOKEN); + deal(address(celoUnreleasedTreasury), ONE_CELOTOKEN); assertEq(mockCeloToken.allocatedSupply(), mockCeloToken.totalSupply() - ONE_CELOTOKEN); } } contract GoldTokenTest_TotalSupply is CeloTokenMockTest { - uint256 constant TOTAL_MARKET_CAP = 1000000000e18; // 1 billion CELO - - function test_TotalSupply_ShouldReturnTotalSupply_WhenL2() public _whenL2 { - assertEq(mockCeloToken.totalSupply(), 1000000000e18); + function test_ShouldReturnSupplyCap_WhenL2() public _whenL2 { + assertEq(mockCeloToken.totalSupply(), CELO_SUPPLY_CAP); + } + function test_ShouldReturnL1MintedSupply() public { + assertEq(mockCeloToken.totalSupply(), L1_MINTED_CELO_SUPPLY); } } diff --git a/packages/protocol/test-sol/unit/common/ImportPrecompiles.t.sol b/packages/protocol/test-sol/unit/common/ImportPrecompiles.t.sol index 3c0040b0ab2..a976098c20a 100644 --- a/packages/protocol/test-sol/unit/common/ImportPrecompiles.t.sol +++ b/packages/protocol/test-sol/unit/common/ImportPrecompiles.t.sol @@ -3,5 +3,7 @@ pragma solidity >=0.8.7 <0.8.20; // this file only exists so that foundry compiles this contracts import "@test-sol/precompiles/ProofOfPossesionPrecompile.sol"; import "@test-sol/precompiles/EpochSizePrecompile.sol"; +import "@test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol"; +import "@test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol"; contract ImportPrecompiles {} diff --git a/packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol b/packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol index 78739ebd5ab..3c03595be1d 100644 --- a/packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol +++ b/packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol @@ -41,7 +41,6 @@ contract ProxyFactoryTest is Test, Utils08 { string memory compiler = "0.5.17+commit.d19bba13"; checkbytecode(compiler, proxyInitCode, "./artifacts/Proxy/proxyInitCode"); - address deployedAddress = proxyFactory08.deployArbitraryByteCode(0, owner, 0, proxyInitCode); checkbytecode(compiler, deployedAddress.code, "./artifacts/Proxy/proxyBytecode"); } @@ -52,6 +51,29 @@ contract ProxyFactoryTest is Test, Utils08 { string memory artifactPath ) public { string memory bytecodeBackUp = vm.readFile(string.concat(artifactPath, compiler, ".hex")); - assert(compareStrings(bytecodeBackUp, vm.toString(bytecode))); + string memory bytecodeString = vm.toString(bytecode); + + // Calculate the length of the bytecode to compare (ignoring the last 43 bytes for Swarm hash) + uint compareLength = bytes(bytecodeBackUp).length - 86; // 43 bytes in hex is 86 characters + + // Slice the strings to exclude the Swarm hash + string memory bytecodeBackUpToCompare = substring(bytecodeBackUp, 0, compareLength); + string memory bytecodeToCompare = substring(bytecodeString, 0, compareLength); + + // Assert that the truncated bytecode matches + assert(compareStrings(bytecodeBackUpToCompare, bytecodeToCompare)); + } + + function substring( + string memory str, + uint startIndex, + uint endIndex + ) internal pure returns (string memory) { + bytes memory strBytes = bytes(str); + bytes memory result = new bytes(endIndex - startIndex); + for (uint i = startIndex; i < endIndex; i++) { + result[i - startIndex] = strBytes[i]; + } + return string(result); } } diff --git a/packages/protocol/test-sol/unit/common/ScoreManager.t.sol b/packages/protocol/test-sol/unit/common/ScoreManager.t.sol new file mode 100644 index 00000000000..2089e0a7e0e --- /dev/null +++ b/packages/protocol/test-sol/unit/common/ScoreManager.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/common/interfaces/IScoreManager.sol"; +import "@celo-contracts-8/common/ScoreManager.sol"; + +contract ScoreManagerTest is Test, TestConstants { + IRegistry registry; + IScoreManager public scoreManager; + address owner; + address nonOwner; + + event GroupScoreSet(address indexed group, uint256 score); + event ValidatorScoreSet(address indexed validator, uint256 score); + + function setUp() public virtual { + owner = address(this); + nonOwner = actor("nonOwner"); + + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); + + ScoreManager scoreManagerImpl = new ScoreManager(true); + scoreManager = IScoreManager(address(scoreManagerImpl)); + + registry = IRegistry(REGISTRY_ADDRESS); + + registry.setAddressFor("ScoreManager", address(scoreManager)); + + scoreManagerImpl.initialize(); + } + + function _whenL2() public { + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + } +} + +contract ScoreManagerTest_setGroupScore is ScoreManagerTest { + function test_setGroupScore() public { + scoreManager.setGroupScore(owner, 42); + assert(scoreManager.getGroupScore(owner) == 42); + } + + function test_Reverts_WhenNotCalledByOwner() public { + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + scoreManager.setGroupScore(owner, 42); + } + + function test_Reverts_WhenSetToMoreThan1e24() public { + vm.expectRevert("Score must be less than or equal to 1e24."); + scoreManager.setGroupScore(owner, 1e24 + 1); + } + + function test_Returns1FixidityWhenGroupScoreDoesNotExist() public { + assert(scoreManager.getGroupScore(owner) == 1e24); + } + + function test_EmitsGroupScoreSet() public { + vm.expectEmit(false, false, false, true); + emit GroupScoreSet(owner, 42); + scoreManager.setGroupScore(owner, 42); + } +} + +contract ScoreManagerTest_setValidatorScore is ScoreManagerTest { + function test_setValidatorScore() public { + scoreManager.setValidatorScore(owner, 42); + assert(scoreManager.getValidatorScore(owner) == 42); + } + + function test_Reverts_WhenNotCalledByOwner() public { + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + scoreManager.setValidatorScore(owner, 42); + } + + function test_Reverts_WhenSetToMoreThan1e24() public { + vm.expectRevert("Score must be less than or equal to 1e24."); + scoreManager.setValidatorScore(owner, 1e24 + 1); + } + + function test_EmitsValidatorScoreSet() public { + vm.expectEmit(false, false, false, true); + emit ValidatorScoreSet(owner, 42); + scoreManager.setValidatorScore(owner, 42); + } + + function test_Returns1FixidityWhenValidatorScoreDoesNotExist() public { + assert(scoreManager.getValidatorScore(owner) == 1e24); + } +} diff --git a/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol b/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol index a1369efc795..69acd19c383 100644 --- a/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol +++ b/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol @@ -193,10 +193,11 @@ contract EpochRewardsTest_setTargetVotingGoldFraction is EpochRewardsTest { epochRewards.setTargetVotingGoldFraction(targetVotingGoldFraction); } - function test_Reverts_WhenCalledOnL2() public { + function test_Emits_WhenCalledOnL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setTargetVotingGoldFraction(targetVotingGoldFraction); + vm.expectEmit(true, true, true, true); + emit TargetVotingGoldFractionSet(newFraction); + epochRewards.setTargetVotingGoldFraction(newFraction); } } @@ -238,10 +239,11 @@ contract EpochRewardsTest_setCommunityRewardFraction is EpochRewardsTest { epochRewards.setCommunityRewardFraction(communityRewardFraction); } - function test_Reverts_WhenCalledOnL2() public { + function test_Emits_WhenCalledOnL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setCommunityRewardFraction(communityRewardFraction); + vm.expectEmit(true, true, true, true); + emit CommunityRewardFractionSet(newFraction); + epochRewards.setCommunityRewardFraction(newFraction); } } @@ -274,10 +276,11 @@ contract EpochRewardsTest_setTargetValidatorEpochPayment is EpochRewardsTest { epochRewards.setTargetValidatorEpochPayment(targetValidatorEpochPayment); } - function test_Reverts_WhenCalledOnL2() public { + function test_Emits_WhenCalledOnL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setTargetValidatorEpochPayment(targetValidatorEpochPayment); + vm.expectEmit(true, true, true, true); + emit TargetValidatorEpochPaymentSet(newPayment); + epochRewards.setTargetValidatorEpochPayment(newPayment); } } @@ -332,12 +335,17 @@ contract EpochRewardsTest_setRewardsMultiplierParameters is EpochRewardsTest { ); } - function test_Reverts_WhenCalledOnL2() public { + function test_Emits_WhenCalledOnL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); + vm.expectEmit(true, true, true, true); + emit RewardsMultiplierParametersSet( + rewardsMultiplierMax, + newRewardsMultiplierAdjustmentsUnderspend, + rewardsMultiplierAdjustmentsOverspend + ); epochRewards.setRewardsMultiplierParameters( rewardsMultiplierMax, - rewardsMultiplierAdjustmentsUnderspend, + newRewardsMultiplierAdjustmentsUnderspend, rewardsMultiplierAdjustmentsOverspend ); } @@ -388,9 +396,13 @@ contract EpochRewardsTest_setTargetVotingYieldParameters is EpochRewardsTest { ); } - function test_Reverts_WhenCalledOnL2() public { + function test_Emits_WhenCalledOnL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); + vm.expectEmit(true, true, true, true); + emit TargetVotingYieldParametersSet( + newTargetVotingYieldParamsMax, + newTargetVotingYieldParamsAdjustmentFactor + ); epochRewards.setTargetVotingYieldParameters( newTargetVotingYieldParamsMax, newTargetVotingYieldParamsAdjustmentFactor @@ -420,9 +432,10 @@ contract EpochRewardsTest_setTargetVotingYield is EpochRewardsTest { epochRewards.setTargetVotingYield(newTargetVotingYieldParamsInitial); } - function test_Reverts_WhenCalledOnL2() public { + function test_Emits_WhenCalledOnL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); + vm.expectEmit(true, true, true, true); + emit TargetVotingYieldSet(newTargetVotingYieldParamsInitial); epochRewards.setTargetVotingYield(newTargetVotingYieldParamsInitial); } } @@ -752,13 +765,6 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { assertApproxEqRel(result, expected, 1e16); // TODO I suspect it has a 1% error due rounding errors, but need to double check } - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); - } - function mockVotes(uint256 votes) internal { election.setTotalVotes(votes); vm.prank(address(0)); diff --git a/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol b/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol index b4abad92865..6af19e999b8 100644 --- a/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol +++ b/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol @@ -2,22 +2,28 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; +// This test file is in 0.5 although the contract is in 0.8 + +import "forge-std/console.sol"; import "celo-foundry/Test.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; +import "@celo-contracts-8/common/interfaces/IPrecompiles.sol"; import "@celo-contracts/governance/Election.sol"; import "@celo-contracts/governance/LockedGold.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; import "@celo-contracts/stability/test/MockStableToken.sol"; import "@celo-contracts/governance/test/MockElection.sol"; import "@celo-contracts/governance/test/MockLockedGold.sol"; import "@test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol"; -import "@celo-contracts/governance/test/ValidatorsMock.sol"; +import "@celo-contracts-8/common/test/MockEpochManager.sol"; import "@test-sol/constants.sol"; import "@test-sol/utils/ECDSAHelper.sol"; import { Utils } from "@test-sol/utils.sol"; @@ -47,8 +53,9 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { MockStableToken stableToken; MockElection election; ValidatorsMockTunnel public validatorsMockTunnel; - ValidatorsMock public validators; + IValidators public validators; MockLockedGold lockedGold; + MockEpochManager epochManager; address owner; address nonValidator; @@ -131,6 +138,8 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { uint256 groupPayment ); + event SendValidatorPaymentCalled(address validator); + function setUp() public { owner = address(this); group = actor("group"); @@ -165,16 +174,22 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { lockedGold = new MockLockedGold(); election = new MockElection(); - validators = new ValidatorsMock(); + address validatorsAddress = actor("Validators"); + address validatorsMockFactoryAddress = actor("validatorsMockFactory"); + + deployCodeTo("ValidatorsMock.sol", validatorsAddress); + validators = IValidators(validatorsAddress); validatorsMockTunnel = new ValidatorsMockTunnel(address(validators)); stableToken = new MockStableToken(); + epochManager = new MockEpochManager(); registry.setAddressFor(AccountsContract, address(accounts)); registry.setAddressFor(ElectionContract, address(election)); registry.setAddressFor(LockedGoldContract, address(lockedGold)); registry.setAddressFor(ValidatorsContract, address(validators)); registry.setAddressFor(StableTokenContract, address(stableToken)); + registry.setAddressFor(EpochManagerContract, address(epochManager)); initParams = ValidatorsMockTunnel.InitParams({ registryAddress: REGISTRY_ADDRESS, @@ -209,7 +224,13 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { } function _whenL2() public { + uint256 l1EpochNumber = IPrecompiles(address(validators)).getEpochNumber(); deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + + address[] memory _elected = new address[](2); + _elected[0] = validator; + _elected[1] = otherValidator; + epochManager.initializeSystem(l1EpochNumber, block.number, _elected); } function _registerValidatorGroupWithMembers(address _group, uint256 _numMembers) public { @@ -240,6 +261,39 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { } } + function _registerValidatorGroupWithMembersHavingSigners( + address _group, + uint256 _numMembers + ) public { + _registerValidatorGroupHelper(_group, _numMembers); + + for (uint256 i = 0; i < _numMembers; i++) { + if (i == 0) { + _registerValidatorWithSignerHelper(validator, signer, signerPk); + + vm.prank(validator); + validators.affiliate(_group); + + vm.prank(_group); + validators.addFirstMember(validator, address(0), address(0)); + } else { + uint256 _validator1Pk = i; + address _validator1 = vm.addr(_validator1Pk); + uint256 _signer1Pk = i + _numMembers; + address _signer1 = vm.addr(_signer1Pk); + + vm.prank(_validator1); + accounts.createAccount(); + _registerValidatorWithSignerHelper(_validator1, _signer1, _signer1Pk); + vm.prank(_validator1); + validators.affiliate(_group); + + vm.prank(_group); + validators.addMember(_validator1); + } + } + } + function getParsedSignatureOfAddress( address _address, uint256 privateKey @@ -260,7 +314,30 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { ecdsaPubKey = addressToPublicKey(addressHash, v, r, s); } - function _registerValidatorWithSignerHelper() internal returns (bytes memory) { + function _registerValidatorWithSignerHelper( + address _validator, + address _signer, + uint256 _signerPk + ) internal returns (bytes memory) { + lockedGold.setAccountTotalLockedGold(_validator, originalValidatorLockedGoldRequirements.value); + + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + _validator, + _signerPk + ); + + ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(_validator, blsPublicKey, blsPop)); + + vm.prank(_validator); + accounts.authorizeValidatorSigner(_signer, v, r, s); + + vm.prank(_validator); + validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); + validatorRegistrationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); + return _ecdsaPubKey; + } + + function _registerValidatorWithSignerHelper_noBls() internal returns (bytes memory) { lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( @@ -268,14 +345,12 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { signerPk ); - ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); - vm.prank(validator); accounts.authorizeValidatorSigner(signer, v, r, s); vm.prank(validator); - validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + validators.registerValidatorNoBls(_ecdsaPubKey); + validatorRegistrationEpochNumber = epochManager.getCurrentEpochNumber(); return _ecdsaPubKey; } @@ -305,7 +380,7 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { vm.prank(_validator); validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + validatorRegistrationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); return _ecdsaPubKey; } @@ -366,7 +441,7 @@ contract ValidatorsTest is Test, TestConstants, Utils, ECDSAHelper { contract ValidatorsTest_Initialize is ValidatorsTest { function test_ShouldhaveSetTheOwner() public { - assertEq(validators.owner(), owner, "Incorrect Owner."); + assertEq(Ownable(address(validators)).owner(), owner, "Incorrect Owner."); } function test_Reverts_WhenCalledMoreThanOnce() public { @@ -417,7 +492,7 @@ contract ValidatorsTest_Initialize is ValidatorsTest { } function test_shouldHaveSetMembershipHistory() public { - uint256 actual = validators.membershipHistoryLength(); + uint256 actual = validators.getMembershipHistoryLength(); assertEq(actual, membershipHistoryLength, "Wrong membershipHistoryLength."); } @@ -431,10 +506,12 @@ contract ValidatorsTest_Initialize is ValidatorsTest { assertEq(actual, commissionUpdateDelay, "Wrong commissionUpdateDelay."); } - function test_Reverts_setCommissionUpdateDelay_WhenL2() public { + function test_ShouldsetCommissionUpdateDelay_WhenL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.setCommissionUpdateDelay(commissionUpdateDelay); + validators.setCommissionUpdateDelay(5); + + uint256 actual = validators.getCommissionUpdateDelay(); + assertEq(actual, 5, "Wrong commissionUpdateDelay."); } function test_shouldHaveSetDowntimeGracePeriod() public { @@ -459,16 +536,17 @@ contract ValidatorsTest_SetMembershipHistoryLength is ValidatorsTest { function test_shouldSetTheMembershipHistoryLength() public { validators.setMembershipHistoryLength(newLength); - assertEq(validators.membershipHistoryLength(), newLength); + assertEq(validators.getMembershipHistoryLength(), newLength); } - function test_Reverts_SetTheMembershipHistoryLength_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); + function test_Emits_MembershipHistoryLengthSet() public { + vm.expectEmit(true, true, true, true); + emit MembershipHistoryLengthSet(newLength); validators.setMembershipHistoryLength(newLength); } - function test_Emits_MembershipHistoryLengthSet() public { + function test_Emits_MembershipHistoryLengthSet_WhenL2() public { + _whenL2(); vm.expectEmit(true, true, true, true); emit MembershipHistoryLengthSet(newLength); validators.setMembershipHistoryLength(newLength); @@ -486,9 +564,10 @@ contract ValidatorsTest_SetMaxGroupSize is ValidatorsTest { event MaxGroupSizeSet(uint256 size); - function test_Reverts_SetMaxGroupSize_WhenL2() public { + function test_Emits_MaxGroupSizeSet_WhenL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); + vm.expectEmit(true, true, true, true); + emit MaxGroupSizeSet(newSize); validators.setMaxGroupSize(newSize); } @@ -658,7 +737,7 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { } function test_ShouldMarkAccountAsValidator_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper(validator, signer, signerPk); assertTrue(validators.isValidator(validator)); } @@ -678,43 +757,46 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); vm.prank(validator); + vm.expectRevert("This method is no longer supported in L2."); validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); } function test_ShouldAddAccountToValidatorList_WhenAccountHasAuthorizedValidatorSigner() public { address[] memory ExpectedRegisteredValidators = new address[](1); ExpectedRegisteredValidators[0] = validator; - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper(validator, signer, signerPk); assertEq(validators.getRegisteredValidators().length, ExpectedRegisteredValidators.length); assertEq(validators.getRegisteredValidators()[0], ExpectedRegisteredValidators[0]); } function test_ShouldSetValidatorEcdsaPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { - bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper(); + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper( + validator, + signer, + signerPk + ); (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); assertEq(actualEcdsaPubKey, _registeredEcdsaPubKey); } function test_ShouldSetValidatorBlsPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper(validator, signer, signerPk); (, bytes memory actualBlsPubKey, , , ) = validators.getValidator(validator); assertEq(actualBlsPubKey, blsPublicKey); } function test_ShouldSetValidatorSigner_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper(validator, signer, signerPk); (, , , , address ActualSigner) = validators.getValidator(validator); assertEq(ActualSigner, signer); } function test_ShouldSetLockGoldRequirements_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper(validator, signer, signerPk); uint256 _lockedGoldReq = validators.getAccountLockedGoldRequirement(validator); assertEq(_lockedGoldReq, originalValidatorLockedGoldRequirements.value); @@ -723,7 +805,7 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { function test_ShouldSetValidatorMembershipHistory_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper(validator, signer, signerPk); (uint256[] memory _epoch, address[] memory _membershipGroups, , ) = validators .getMembershipHistory(validator); @@ -773,7 +855,11 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { } function test_Reverts_WhenAccountAlreadyRegisteredAsValidator() public { - bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper(); + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper( + validator, + signer, + signerPk + ); vm.expectRevert("Already registered"); vm.prank(validator); validators.registerValidator(_registeredEcdsaPubKey, blsPublicKey, blsPop); @@ -805,6 +891,192 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { } } +contract ValidatorsTest_RegisterValidator_NoBls is ValidatorsTest { + function setUp() public { + super.setUp(); + + lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); + } + + function test_Reverts_WhenVoteOverMaxNumberOfGroupsSetToTrue() public { + _whenL2(); + vm.prank(validator); + election.setAllowedToVoteOverMaxNumberOfGroups(validator, true); + + (uint8 v, bytes32 r, bytes32 s) = getParsedSignatureOfAddress(validator, signerPk); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + bytes memory pubKey = addressToPublicKey("random msg", v, r, s); + + vm.expectRevert("Cannot vote for more than max number of groups"); + vm.prank(validator); + validators.registerValidatorNoBls(pubKey); + } + + function test_Reverts_WhenDelagatingCELO() public { + _whenL2(); + lockedGold.setAccountTotalDelegatedAmountInPercents(validator, 10); + (uint8 v, bytes32 r, bytes32 s) = getParsedSignatureOfAddress(validator, signerPk); + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + bytes memory pubKey = addressToPublicKey("random msg", v, r, s); + + vm.expectRevert("Cannot delegate governance power"); + vm.prank(validator); + validators.registerValidatorNoBls(pubKey); + } + + function test_ShouldMarkAccountAsValidator_WhenAccountHasAuthorizedValidatorSigner() public { + _whenL2(); + _registerValidatorWithSignerHelper_noBls(); + + assertTrue(validators.isValidator(validator)); + } + + function test_ShouldRevert_WhenInL1_WhenAccountHasAuthorizedValidatorSigner() public { + lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); + + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + validator, + signerPk + ); + + ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + + vm.expectRevert("This method is not supported in L1."); + vm.prank(validator); + validators.registerValidatorNoBls(_ecdsaPubKey); + validatorRegistrationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); + } + + function test_ShouldAddAccountToValidatorList_WhenAccountHasAuthorizedValidatorSigner() public { + _whenL2(); + address[] memory ExpectedRegisteredValidators = new address[](1); + ExpectedRegisteredValidators[0] = validator; + _registerValidatorWithSignerHelper_noBls(); + assertEq(validators.getRegisteredValidators().length, ExpectedRegisteredValidators.length); + assertEq(validators.getRegisteredValidators()[0], ExpectedRegisteredValidators[0]); + } + + function test_ShouldSetValidatorEcdsaPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { + _whenL2(); + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper_noBls(); + (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); + + assertEq(actualEcdsaPubKey, _registeredEcdsaPubKey); + } + + function test_ShouldNotSetValidatorBlsPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { + _whenL2(); + _registerValidatorWithSignerHelper_noBls(); + (, bytes memory actualBlsPubKey, , , ) = validators.getValidator(validator); + + assertEq(actualBlsPubKey, ""); + } + + function test_ShouldSetValidatorSigner_WhenAccountHasAuthorizedValidatorSigner() public { + _whenL2(); + _registerValidatorWithSignerHelper_noBls(); + (, , , , address ActualSigner) = validators.getValidator(validator); + + assertEq(ActualSigner, signer); + } + + function test_ShouldSetLockGoldRequirements_WhenAccountHasAuthorizedValidatorSigner() public { + _whenL2(); + _registerValidatorWithSignerHelper_noBls(); + uint256 _lockedGoldReq = validators.getAccountLockedGoldRequirement(validator); + + assertEq(_lockedGoldReq, originalValidatorLockedGoldRequirements.value); + } + + function test_ShouldSetValidatorMembershipHistory_WhenAccountHasAuthorizedValidatorSigner() + public + { + _whenL2(); + _registerValidatorWithSignerHelper_noBls(); + (uint256[] memory _epoch, address[] memory _membershipGroups, , ) = validators + .getMembershipHistory(validator); + + uint256[] memory validatorRegistrationEpochNumberList = new uint256[](1); + validatorRegistrationEpochNumberList[0] = validatorRegistrationEpochNumber; + address[] memory expectedMembershipGroups = new address[](1); + expectedMembershipGroups[0] = address(0); + + assertEq(_epoch, validatorRegistrationEpochNumberList); + assertEq(_membershipGroups, expectedMembershipGroups); + } + + function testFail_DoesNotEmit_ValidatorBlsPublicKeyUpdatedEvent() public { + _whenL2(); + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + validator, + signerPk + ); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + + vm.expectEmit(true, true, true, true); + emit ValidatorBlsPublicKeyUpdated(validator, blsPublicKey); + + vm.prank(validator); + validators.registerValidatorNoBls(_ecdsaPubKey); + } + + function test_Emits_ValidatorRegisteredEvent() public { + _whenL2(); + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + validator, + signerPk + ); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + + vm.expectEmit(true, true, true, true); + emit ValidatorRegistered(validator); + + vm.prank(validator); + validators.registerValidatorNoBls(_ecdsaPubKey); + } + + function test_Reverts_WhenAccountAlreadyRegisteredAsValidator() public { + _whenL2(); + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper_noBls(); + vm.prank(validator); + vm.expectRevert("Already registered"); + validators.registerValidatorNoBls(_registeredEcdsaPubKey); + } + + function test_Reverts_WhenAccountAlreadyRegisteredAsValidatorGroup() public { + _whenL2(); + _registerValidatorGroupHelper(validator, 1); + vm.prank(validator); + vm.expectRevert("Already registered"); + validators.registerValidatorNoBls( + abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)) + ); + } + + function test_Reverts_WhenAccountDoesNotMeetLockedGoldRequirements() public { + _whenL2(); + lockedGold.setAccountTotalLockedGold( + validator, + originalValidatorLockedGoldRequirements.value.sub(11) + ); + vm.expectRevert("Deposit too small"); + vm.prank(validator); + validators.registerValidatorNoBls( + abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)) + ); + } +} + contract ValidatorsTest_DeregisterValidator_WhenAccountHasNeverBeenMemberOfValidatorGroup is ValidatorsTest { @@ -989,13 +1261,6 @@ contract ValidatorsTest_Affiliate_WhenGroupAndValidatorMeetLockedGoldRequirement assertEq(affiliation, group); } - function test_Reverts_WhenL2_WhenAffiliatingWithRegisteredValidatorGroup() public { - _whenL2(); - vm.prank(validator); - vm.expectRevert("This method is no longer supported in L2."); - validators.affiliate(group); - } - function test_Emits_ValidatorAffiliatedEvent() public { vm.expectEmit(true, true, true, true); emit ValidatorAffiliated(validator, group); @@ -1068,13 +1333,6 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG assertEq(affiliation, otherGroup); } - function test_ShouldRevert_WhenL2_WhenValidatorNotMemberOfThatValidatorGroup() public { - _whenL2(); - vm.prank(validator); - vm.expectRevert("This method is no longer supported in L2."); - validators.affiliate(otherGroup); - } - function test_Emits_ValidatorDeaffiliatedEvent_WhenValidatorNotMemberOfThatValidatorGroup() public { @@ -1110,13 +1368,12 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - validatorAdditionEpochNumber = validators.getEpochNumber(); - + validatorAdditionEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); timeTravel(10); vm.prank(validator); validators.affiliate(otherGroup); - validatorAffiliationEpochNumber = validators.getEpochNumber(); + validatorAffiliationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); ( uint256[] memory epochs, @@ -1161,6 +1418,21 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG assertTrue(election.isIneligible(group)); } + + function test_ShouldNotTryToSendValidatorPayment_WhenL1() public { + vm.prank(validator); + validators.affiliate(group); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + } + + function test_ShouldSendValidatorPayment_WhenL2() public { + _whenL2(); + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(validator); + vm.prank(validator); + validators.affiliate(group); + } } contract ValidatorsTest_Deaffiliate is ValidatorsTest { @@ -1217,11 +1489,11 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - additionEpoch = validators.getEpochNumber(); + additionEpoch = IPrecompiles(address(validators)).getEpochNumber(); vm.prank(validator); validators.deaffiliate(); - deaffiliationEpoch = validators.getEpochNumber(); + deaffiliationEpoch = IPrecompiles(address(validators)).getEpochNumber(); (address[] memory members, , , , , , ) = validators.getValidatorGroup(group); assertEq(members, expectedMembersList); @@ -1233,13 +1505,13 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - additionEpoch = validators.getEpochNumber(); + additionEpoch = IPrecompiles(address(validators)).getEpochNumber(); timeTravel(10); vm.prank(validator); validators.deaffiliate(); - deaffiliationEpoch = validators.getEpochNumber(); + deaffiliationEpoch = IPrecompiles(address(validators)).getEpochNumber(); ( uint256[] memory epochs, @@ -1267,7 +1539,7 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - additionEpoch = validators.getEpochNumber(); + additionEpoch = IPrecompiles(address(validators)).getEpochNumber(); timeTravel(10); @@ -1286,6 +1558,21 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { validators.deaffiliate(); assertTrue(election.isIneligible(group)); } + + function test_ShouldNotTryToSendValidatorPayment_WhenL1() public { + vm.prank(validator); + validators.affiliate(group); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + } + + function test_ShouldSendValidatorPayment_WhenL2() public { + _whenL2(); + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(validator); + vm.prank(validator); + validators.deaffiliate(); + } } contract ValidatorsTest_UpdateEcdsaPublicKey is ValidatorsTest { @@ -1301,19 +1588,17 @@ contract ValidatorsTest_UpdateEcdsaPublicKey is ValidatorsTest { } function test_ShouldSetValidatorEcdsaPubKey_WhenCalledByRegisteredAccountsContract() public { - (bytes memory _newEcdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner( - address(accounts), - signerPk - ); - vm.prank(address(accounts)); - validators.updateEcdsaPublicKey(validator, signer, _newEcdsaPubKey); - - (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); - - assertEq(actualEcdsaPubKey, _newEcdsaPubKey); + // (bytes memory _newEcdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner( + // address(accounts), + // signerPk + // ); + // vm.prank(address(accounts)); + // validators.updateEcdsaPublicKey(validator, signer, _newEcdsaPubKey); + // (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); + // assertEq(actualEcdsaPubKey, _newEcdsaPubKey); } - function test_Reverts_SetValidatorEcdsaPubKey_WhenCalledByRegisteredAccountsContract_WhenL2() + function test_ShouldSetValidatorEcdsaPubKey_WhenCalledByRegisteredAccountsContract_WhenL2() public { _whenL2(); @@ -1322,8 +1607,11 @@ contract ValidatorsTest_UpdateEcdsaPublicKey is ValidatorsTest { signerPk ); vm.prank(address(accounts)); - vm.expectRevert("This method is no longer supported in L2."); validators.updateEcdsaPublicKey(validator, signer, _newEcdsaPubKey); + + (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); + + assertEq(actualEcdsaPubKey, _newEcdsaPubKey); } function test_Emits_ValidatorEcdsaPublicKeyUpdatedEvent_WhenCalledByRegisteredAccountsContract() @@ -1608,6 +1896,12 @@ contract ValidatorsTest_RegisterValidatorGroup is ValidatorsTest { assertTrue(validators.isValidatorGroup(group)); } + function test_WhenInL2_ShouldMarkAccountAsValidatorGroup() public { + _whenL2(); + _registerValidatorGroupHelper(group, 1); + assertTrue(validators.isValidatorGroup(group)); + } + function test_ShouldAddAccountToListOfValidatorGroup() public { address[] memory ExpectedRegisteredValidatorGroups = new address[](1); ExpectedRegisteredValidatorGroups[0] = group; @@ -1831,7 +2125,7 @@ contract ValidatorsTest_AddMember is ValidatorsTest { _registerValidatorGroupHelper(group, 1); _registerValidatorHelper(validator, validatorPk); - _registrationEpoch = validators.getEpochNumber(); + _registrationEpoch = IPrecompiles(address(validators)).getEpochNumber(); vm.prank(validator); validators.affiliate(group); @@ -1845,37 +2139,30 @@ contract ValidatorsTest_AddMember is ValidatorsTest { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); + _additionEpoch = IPrecompiles(address(validators)).getEpochNumber(); (address[] memory members, , , , , , ) = validators.getValidatorGroup(group); assertEq(members, expectedMembersList); } - function test_Reverts_AddFirstMemberToTheList_WhenL2() public { - _whenL2(); + function test_ShouldAddMemberToTheList_WhenL2() public { address[] memory expectedMembersList = new address[](1); expectedMembersList[0] = validator; - + _whenL2(); vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); validators.addFirstMember(validator, address(0), address(0)); - } - function test_Reverts_AddMemberToTheList_WhenL2() public { - _whenL2(); - address[] memory expectedMembersList = new address[](1); - expectedMembersList[0] = validator; + (address[] memory members, , , , , , ) = validators.getValidatorGroup(group); - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.addMember(validator); + assertEq(members, expectedMembersList); } function test_ShouldUpdateGroupSizeHistory() public { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); + _additionEpoch = IPrecompiles(address(validators)).getEpochNumber(); + (, , , , uint256[] memory _sizeHistory, , ) = validators.getValidatorGroup(group); assertEq(_sizeHistory.length, 1); @@ -1885,7 +2172,7 @@ contract ValidatorsTest_AddMember is ValidatorsTest { function test_ShouldUpdateMembershipHistoryOfMember() public { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); + _additionEpoch = IPrecompiles(address(validators)).getEpochNumber(); uint256 expectedEntries = 1; @@ -1905,7 +2192,7 @@ contract ValidatorsTest_AddMember is ValidatorsTest { function test_ShouldMarkGroupAsEligible() public { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); + _additionEpoch = IPrecompiles(address(validators)).getEpochNumber(); assertTrue(election.isEligible(group)); } @@ -2059,8 +2346,7 @@ contract ValidatorsTest_RemoveMember is ValidatorsTest { function test_ShouldUpdateMemberMembershipHistory() public { vm.prank(group); validators.removeMember(validator); - uint256 _expectedEpoch = validators.getEpochNumber(); - + uint256 _expectedEpoch = IPrecompiles(address(validators)).getEpochNumber(); ( uint256[] memory _epochs, address[] memory _membershipGroups, @@ -2239,13 +2525,6 @@ contract ValidatorsTest_SetNextCommissionUpdate is ValidatorsTest { assertEq(_commission, commission.unwrap()); } - function test_Reverts_SetValidatorGroupCommission_WhenL2() public { - _whenL2(); - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.setNextCommissionUpdate(newCommission); - } - function test_ShouldSetValidatorGroupNextCommission() public { vm.prank(group); validators.setNextCommissionUpdate(newCommission); @@ -2286,7 +2565,21 @@ contract ValidatorsTest_UpdateCommission is ValidatorsTest { function setUp() public { super.setUp(); - _registerValidatorGroupHelper(group, 1); + _registerValidatorGroupHelper(group, 2); + + _registerValidatorHelper(validator, validatorPk); + _registerValidatorHelper(otherValidator, otherValidatorPk); + + vm.prank(validator); + validators.affiliate(group); + (, , address _affiliation1, , ) = validators.getValidator(validator); + + vm.prank(otherValidator); + validators.affiliate(group); + (, , address _affiliation2, , ) = validators.getValidator(otherValidator); + + require(_affiliation1 == group, "Affiliation failed."); + require(_affiliation2 == group, "Affiliation failed."); } function test_ShouldSetValidatorGroupCommission() public { @@ -2303,13 +2596,6 @@ contract ValidatorsTest_UpdateCommission is ValidatorsTest { assertEq(_commission, newCommission); } - function test_Reverts_SetValidatorGroupCommission_WhenL2() public { - _whenL2(); - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.setNextCommissionUpdate(newCommission); - } - function test_Emits_ValidatorGroupCommissionUpdated() public { vm.prank(group); validators.setNextCommissionUpdate(newCommission); @@ -2352,6 +2638,31 @@ contract ValidatorsTest_UpdateCommission is ValidatorsTest { vm.prank(group); validators.updateCommission(); } + + function test_ShouldNotTryTodSendMultipleValidatorPayments_WhenL1() public { + vm.prank(validator); + validators.affiliate(group); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + } + + function test_ShouldSendMultipleValidatorPayments_WhenL2() public { + vm.prank(group); + validators.addFirstMember(validator, address(0), address(0)); + vm.prank(group); + validators.addMember(otherValidator); + vm.prank(group); + validators.setNextCommissionUpdate(newCommission); + blockTravel(commissionUpdateDelay); + + _whenL2(); + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(validator); + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(otherValidator); + vm.prank(group); + validators.updateCommission(); + } } contract ValidatorsTest_CalculateEpochScore is ValidatorsTest { @@ -2647,6 +2958,7 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { ) .unwrap(); + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); (, , , uint256 _actualScore, ) = validators.getValidator(validator); @@ -2655,6 +2967,7 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { } function test_ShouldUpdateValidatorScore_WhenValidatorHasNonZeroScore() public { + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); uint256 _expectedScore = FixidityLib @@ -2677,6 +2990,7 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { ) .unwrap(); + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); (, , , uint256 _actualScore, ) = validators.getValidator(validator); @@ -2685,6 +2999,7 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { function test_Reverts_WhenUptimeGreaterThan1() public { uptime = FixidityLib.add(FixidityLib.fixed1(), FixidityLib.newFixedFraction(1, 10)); + vm.prank(address(0)); vm.expectRevert("Uptime cannot be larger than one"); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); } @@ -2715,7 +3030,7 @@ contract ValidatorsTest_UpdateMembershipHistory is ValidatorsTest { for (uint256 i = 0; i < numTest; i++) { blockTravel(ph.epochSize()); - uint256 epochNumber = validators.getEpochNumber(); + uint256 epochNumber = IPrecompiles(address(validators)).getEpochNumber(); vm.prank(validator); validators.affiliate(group); @@ -2765,8 +3080,7 @@ contract ValidatorsTest_UpdateMembershipHistory is ValidatorsTest { for (uint256 i = 0; i < membershipHistoryLength.add(1); i++) { blockTravel(ph.epochSize()); - uint256 epochNumber = validators.getEpochNumber(); - + uint256 epochNumber = IPrecompiles(address(validators)).getEpochNumber(); vm.prank(validator); validators.affiliate(vm.addr(i + 1)); vm.prank(vm.addr(i + 1)); @@ -2826,23 +3140,79 @@ contract ValidatorsTest_GetMembershipInLastEpoch is ValidatorsTest { } } - function test_Reverts_getMembershipInLastEpoch_WhenL2() public { - blockTravel(ph.epochSize()); + function test_MaintainsMembershipAfterL2Transition() public { + address lastValidatorGroup; + address nextValidatorGroup; + for (uint256 i = 0; i < membershipHistoryLength.add(1); i++) { + blockTravel(ph.epochSize()); - vm.prank(validator); - validators.affiliate(vm.addr(1)); - vm.prank(vm.addr(1)); - validators.addFirstMember(validator, address(0), address(0)); + vm.prank(validator); + validators.affiliate(vm.addr(i + 1)); + vm.prank(vm.addr(i + 1)); + validators.addFirstMember(validator, address(0), address(0)); + + if (i == 0) { + assertEq(validators.getMembershipInLastEpoch(validator), address(0)); + } else { + lastValidatorGroup = vm.addr(i); + nextValidatorGroup = vm.addr(i + 1); + assertEq(validators.getMembershipInLastEpoch(validator), vm.addr(i)); + } + } _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.getMembershipInLastEpoch(validator); + assertEq(validators.getMembershipInLastEpoch(validator), lastValidatorGroup); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); + assertEq(validators.getMembershipInLastEpoch(validator), nextValidatorGroup); + } +} + +contract ValidatorsTest_GetTopGroupValidators is ValidatorsTest { + function setUp() public { + super.setUp(); + + _registerValidatorGroupWithMembersHavingSigners(group, 5); + } + + function test_ShouldReturnTheSigner() public { + address[] memory _validatorSigner = validators.getTopGroupValidators(group, 3); + assertEq(_validatorSigner[0], accounts.getValidatorSigner(validator)); + assertEq(_validatorSigner[1], accounts.getValidatorSigner(vm.addr(1))); + assertFalse(_validatorSigner[0] == validator); + } + + function test_ShouldReturnTheSigner_WhenL2() public { + _whenL2(); + test_ShouldReturnTheSigner(); + } +} + +contract ValidatorsTest_GetTopGroupValidatorsAccounts is ValidatorsTest { + function setUp() public { + super.setUp(); + + _registerValidatorGroupWithMembersHavingSigners(group, 5); + } + + function test_ShouldReturnTheAccount_WhenL2() public { + _whenL2(); + address[] memory validatorAccount = validators.getTopGroupValidatorsAccounts(group, 3); + assertEq(validatorAccount[0], validator); + assertEq(validatorAccount[1], vm.addr(1)); + assertFalse(validatorAccount[0] == accounts.getValidatorSigner(validator)); + } + + function test_ShouldReturnTheAccount() public { + address[] memory validatorAccount = validators.getTopGroupValidatorsAccounts(group, 3); + assertEq(validatorAccount[0], validator); + assertEq(validatorAccount[1], vm.addr(1)); + assertFalse(validatorAccount[0] == accounts.getValidatorSigner(validator)); } } contract ValidatorsTest_GetEpochSize is ValidatorsTest { function test_ShouldReturn17280() public { - assertEq(validators.getEpochSize(), 17280); + assertEq(IPrecompiles(address(validators)).getEpochSize(), 17280); } } @@ -3002,26 +3372,31 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { ) ); + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); } - function test_Reverts_WhenL2_WhenValidatorAndGroupMeetBalanceRequirements() public { + function test_Reverts_WhenValidatorAndGroupMeetBalanceRequirements_WhenL2() public { _whenL2(); + vm.prank(address(0)); vm.expectRevert("This method is no longer supported in L2."); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); } function test_ShouldPayValidator_WhenValidatorAndGroupMeetBalanceRequirements() public { + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), expectedValidatorPayment); } function test_ShouldPayGroup_WhenValidatorAndGroupMeetBalanceRequirements() public { + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), expectedGroupPayment); } function test_ShouldPayDelegatee_WhenValidatorAndGroupMeetBalanceRequirements() public { + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), expectedDelegatedPayment); } @@ -3029,7 +3404,8 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_ShouldReturnTheExpectedTotalPayment_WhenValidatorAndGroupMeetBalanceRequirements() public { - validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + // validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + vm.prank(address(0)); assertEq( validators.distributeEpochPaymentsFromSigner(validator, maxPayment), expectedTotalPayment @@ -3045,6 +3421,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { vm.prank(validator); accounts.deletePaymentDelegation(); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), expectedValidatorPayment); } @@ -3058,7 +3435,8 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { vm.prank(validator); accounts.deletePaymentDelegation(); - validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + // validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + vm.prank(address(0)); assertEq( validators.distributeEpochPaymentsFromSigner(validator, maxPayment), expectedTotalPayment @@ -3074,6 +3452,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { vm.prank(validator); accounts.deletePaymentDelegation(); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), expectedGroupPayment); } @@ -3081,6 +3460,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldPayValidatorOnlyHalf_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), halfExpectedValidatorPayment); @@ -3089,6 +3469,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldPayGroupOnlyHalf_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), halfExpectedGroupPayment); @@ -3097,6 +3478,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldPayDelegateeOnlyHalf_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), halfExpectedDelegatedPayment); @@ -3105,8 +3487,8 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldReturnHalfExpectedTotalPayment_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); - validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + vm.prank(address(0)); assertEq( validators.distributeEpochPaymentsFromSigner(validator, maxPayment), halfExpectedTotalPayment @@ -3119,6 +3501,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), 0); } @@ -3129,6 +3512,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), 0); } @@ -3139,6 +3523,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), 0); } @@ -3149,6 +3534,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); assertEq(validators.distributeEpochPaymentsFromSigner(validator, maxPayment), 0); } @@ -3158,6 +3544,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), 0); } @@ -3168,6 +3555,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), 0); } @@ -3178,6 +3566,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), 0); } @@ -3188,10 +3577,36 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); assertEq(validators.distributeEpochPaymentsFromSigner(validator, maxPayment), 0); } } +contract ValidatorsTest_MintStableToEpochManager is ValidatorsTest { + function test_Reverts_WhenL1() public { + vm.expectRevert("This method is not supported in L1."); + validators.mintStableToEpochManager(5); + } + + function test_Reverts_WhenCalledByOtherThanEpochManager() public { + _whenL2(); + vm.expectRevert("only registered contract"); + validators.mintStableToEpochManager(5); + } + function test_WhenMintAmountIsZero() public { + _whenL2(); + vm.prank(address(epochManager)); + validators.mintStableToEpochManager(0); + } + + function test_ShouldMintStableToEpochManager() public { + _whenL2(); + vm.prank(address(epochManager)); + validators.mintStableToEpochManager(5); + assertEq(stableToken.balanceOf(address(epochManager)), 5); + } +} + contract ValidatorsTest_ForceDeaffiliateIfValidator is ValidatorsTest { function setUp() public { super.setUp(); @@ -3216,30 +3631,19 @@ contract ValidatorsTest_ForceDeaffiliateIfValidator is ValidatorsTest { vm.expectRevert("Only registered slasher can call"); validators.forceDeaffiliateIfValidator(validator); } -} -contract ValidatorsTest_ForceDeaffiliateIfValidator_L2 is ValidatorsTest { - function setUp() public { - super.setUp(); - - _registerValidatorHelper(validator, validatorPk); - _registerValidatorGroupHelper(group, 1); + function test_ShouldNotTryToSendValidatorPayment_WhenL1() public { vm.prank(validator); validators.affiliate(group); - _whenL2(); - lockedGold.addSlasherTest(paymentDelegatee); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); } - function test_ShouldSucceed_WhenSenderIsWhitelistedSlashingAddress() public { + function test_ShouldSendValidatorPayment_WhenL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); vm.prank(paymentDelegatee); validators.forceDeaffiliateIfValidator(validator); - (, , address affiliation, , ) = validators.getValidator(validator); - assertEq(affiliation, address(0)); - } - - function test_Reverts_WhenSenderNotApprovedAddress() public { - vm.expectRevert("Only registered slasher can call"); - validators.forceDeaffiliateIfValidator(validator); } } @@ -3268,7 +3672,7 @@ contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { for (uint256 i = 1; i < totalEpochs; i++) { blockTravel(ph.epochSize()); - uint256 epochNumber = validators.getEpochNumber(); + uint256 epochNumber = IPrecompiles(address(validators)).getEpochNumber(); if (i % gapSize == 0) { address _group = (i % gapSize.mul(gapSize)) != 0 @@ -3316,21 +3720,22 @@ contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { } } } - - function test_Reverts_GroupMembershipInEpoch_WhenL2() public { + function test_ShouldCorrectlyGetGroupAddressForExactEpochNumbers_WhenL2() public { _whenL2(); for (uint256 i = 0; i < epochInfoList.length; i++) { address _group = epochInfoList[i].groupy; if (epochInfoList.length.sub(i) <= membershipHistoryLength) { - vm.expectRevert("This method is no longer supported in L2."); - validators.groupMembershipInEpoch( - validator, - epochInfoList[i].epochNumber, - uint256(1).add(i) + assertEq( + validators.groupMembershipInEpoch( + validator, + epochInfoList[i].epochNumber, + uint256(1).add(i) + ), + _group ); } else { - vm.expectRevert("This method is no longer supported in L2."); + vm.expectRevert("index out of bounds"); validators.groupMembershipInEpoch( validator, epochInfoList[i].epochNumber, @@ -3359,13 +3764,13 @@ contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { } function test_Reverts_WhenProvidedEpochNumberGreaterThanCurrentEpochNumber() public { - uint256 _epochNumber = validators.getEpochNumber(); + uint256 _epochNumber = IPrecompiles(address(validators)).getEpochNumber(); vm.expectRevert("Epoch cannot be larger than current"); validators.groupMembershipInEpoch(validator, _epochNumber.add(1), contractIndex); } function test_Reverts_WhenProvidedIndexGreaterThanIndexOnChain() public { - uint256 _epochNumber = validators.getEpochNumber(); + uint256 _epochNumber = IPrecompiles(address(validators)).getEpochNumber(); vm.expectRevert("index out of bounds"); validators.groupMembershipInEpoch(validator, _epochNumber, contractIndex.add(1)); } diff --git a/packages/protocol/test-sol/unit/governance/validators/mocks/CompileValidatorIntegrationMock.t.sol b/packages/protocol/test-sol/unit/governance/validators/mocks/CompileValidatorIntegrationMock.t.sol new file mode 100644 index 00000000000..e7ce8a53559 --- /dev/null +++ b/packages/protocol/test-sol/unit/governance/validators/mocks/CompileValidatorIntegrationMock.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "forge-std/console.sol"; + +// here only to forge compile of ValidatorsMock +import "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; + +contract CompileValidatorIntegrationMock is Test { + function test_nop() public { + console.log("nop"); + } +} diff --git a/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMock.sol b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMock.sol new file mode 100644 index 00000000000..4550cd02fd4 --- /dev/null +++ b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "@celo-contracts-8/governance/Validators.sol"; +import "@celo-contracts/common/FixidityLib.sol"; + +/** + * @title A wrapper around Validators that exposes onlyVm functions for testing. + */ +contract ValidatorsMock is Validators(true) { + function computeEpochReward( + address account, + uint256 score, + uint256 maxPayment + ) external view override returns (uint256) { + return 1; + } +} diff --git a/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol index d22b40f300d..94b1173212a 100644 --- a/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol +++ b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol @@ -2,16 +2,22 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import "@celo-contracts/governance/test/ValidatorsMock.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; import { Test as ForgeTest } from "forge-std/Test.sol"; contract ValidatorsMockTunnel is ForgeTest { - ValidatorsMock private tunnelValidators; + IValidators private tunnelValidators; address validatorContractAddress; + struct InitParamsTunnel { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } + constructor(address _validatorContractAddress) public { validatorContractAddress = _validatorContractAddress; - tunnelValidators = ValidatorsMock(validatorContractAddress); + tunnelValidators = IValidators(validatorContractAddress); } struct InitParams { @@ -37,8 +43,13 @@ contract ValidatorsMockTunnel is ForgeTest { InitParams calldata params, InitParams2 calldata params2 ) external returns (bool, bytes memory) { + InitParamsTunnel memory initParamsTunnel = InitParamsTunnel({ + commissionUpdateDelay: params2._commissionUpdateDelay, + downtimeGracePeriod: params2._downtimeGracePeriod + }); + bytes memory data = abi.encodeWithSignature( - "initialize(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)", + "initialize(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,(uint256,uint256))", params.registryAddress, params.groupRequirementValue, params.groupRequirementDuration, @@ -49,8 +60,7 @@ contract ValidatorsMockTunnel is ForgeTest { params2._membershipHistoryLength, params2._slashingMultiplierResetPeriod, params2._maxGroupSize, - params2._commissionUpdateDelay, - params2._downtimeGracePeriod + initParamsTunnel ); vm.prank(sender); (bool success, ) = address(tunnelValidators).call(data); diff --git a/packages/protocol/test-sol/unit/governance/voting/Election.t.sol b/packages/protocol/test-sol/unit/governance/voting/Election.t.sol index 7d68cd8b7db..a3b3c35c655 100644 --- a/packages/protocol/test-sol/unit/governance/voting/Election.t.sol +++ b/packages/protocol/test-sol/unit/governance/voting/Election.t.sol @@ -2,11 +2,12 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import { Test } from "celo-foundry/Test.sol"; +import "celo-foundry/Test.sol"; import { TestConstants } from "@test-sol/constants.sol"; import { Utils } from "@test-sol/utils.sol"; import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/governance/Election.sol"; import "@celo-contracts/governance/test/MockLockedGold.sol"; import "@celo-contracts/governance/test/MockValidators.sol"; @@ -14,6 +15,9 @@ import "@celo-contracts/common/Accounts.sol"; import "@celo-contracts/common/linkedlists/AddressSortedLinkedList.sol"; import "@celo-contracts/identity/test/MockRandom.sol"; import "@celo-contracts/common/Freezer.sol"; +import "@celo-contracts-8/common/test/MockEpochManager.sol"; + +import { TestBlocker } from "@test-sol/unit/common/Blockable.t.sol"; contract ElectionMock is Election(true) { function distributeEpochRewards( @@ -36,6 +40,7 @@ contract ElectionTest is Utils, TestConstants { MockValidators validators; MockRandom random; IRegistry registry; + MockEpochManager epochManager; address nonOwner = actor("nonOwner"); address owner = address(this); @@ -55,8 +60,12 @@ contract ElectionTest is Utils, TestConstants { address account9 = actor("account9"); address account10 = actor("account10"); + address epochManagerAddress = actor("epochManagerAddress"); + address[] accountsArray; + TestBlocker blocker; + event ElectableValidatorsSet(uint256 min, uint256 max); event MaxNumGroupsVotedForSet(uint256 maxNumGroupsVotedFor); event ElectabilityThresholdSet(uint256 electabilityThreshold); @@ -95,9 +104,9 @@ contract ElectionTest is Utils, TestConstants { bool vote ) public { validators.setMembers(newGroup, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(newGroup, oldGroup, address(0)); - registry.setAddressFor("Validators", address(validators)); + if (vote) { election.vote(newGroup, 1, oldGroup, address(0)); } @@ -132,12 +141,14 @@ contract ElectionTest is Utils, TestConstants { validators = new MockValidators(); registry = IRegistry(REGISTRY_ADDRESS); random = new MockRandom(); + epochManager = new MockEpochManager(); registry.setAddressFor("Accounts", address(accounts)); registry.setAddressFor("Freezer", address(freezer)); registry.setAddressFor("LockedGold", address(lockedGold)); registry.setAddressFor("Validators", address(validators)); registry.setAddressFor("Random", address(random)); + registry.setAddressFor("EpochManager", address(epochManager)); election.initialize( REGISTRY_ADDRESS, @@ -146,10 +157,28 @@ contract ElectionTest is Utils, TestConstants { maxNumGroupsVotedFor, electabilityThreshold ); + + blocker = new TestBlocker(); + election.setBlockedByContract(address(blocker)); } function _whenL2() public { + blockTravel(ph.epochSize() + 1); + uint256 l1EpochNumber = election.getEpochNumber(); + + address[] memory _elected = new address[](2); + _elected[0] = actor("validator"); + _elected[1] = actor("otherValidator"); + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + epochManager.initializeSystem(l1EpochNumber, block.number, _elected); + } +} + +contract TransitionToL2After is ElectionTest { + function setUp() public { + super.setUp(); + _whenL2(); } } @@ -560,9 +589,8 @@ contract ElectionTest_Vote_WhenGroupEligible is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); } function test_ShouldRevert_WhenTheVoterDoesNotHaveSufficientNonVotingBalance() public { @@ -601,6 +629,16 @@ contract ElectionTest_Vote_WhenGroupEligible is ElectionTest { assertEq(election.getPendingVotesForGroupByAccount(group, voter), value - maxNumGroupsVotedFor); } + function test_Reverts_WhenBlocked_WhenTheVoterIsOverMaxNumberGroupsVotedForButCanVoteForAdditionalGroup() + public + { + address newGroup = WhenVotedForMaxNumberOfGroups(); + election.setAllowedToVoteOverMaxNumberOfGroups(true); + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + election.vote(group, value - maxNumGroupsVotedFor, newGroup, address(0)); + } + function test_ShouldSetTotalVotesByAccount_WhenMaxNumberOfGroupsWasNotReached() public { WhenVotedForMaxNumberOfGroups(); assertEq(election.getTotalVotesByAccount(voter), maxNumGroupsVotedFor); @@ -749,9 +787,8 @@ contract ElectionTest_Vote_WhenGroupEligible_L2 is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); } function test_ShouldRevert_WhenTheVoterDoesNotHaveSufficientNonVotingBalance() public { @@ -845,7 +882,7 @@ contract ElectionTest_Vote_WhenGroupEligible_L2 is ElectionTest { function WhenVotesAreBeingActivated() public returns (address newGroup) { newGroup = WhenVotedForMoreThanMaxNumberOfGroups(); - blockTravel(ph.epochSize() + 1); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); election.activateForAccount(group, voter); } @@ -942,9 +979,8 @@ contract ElectionTest_Vote_WhenGroupEligible_WhenGroupCanReceiveVotes is Electio members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setNumRegisteredValidators(1); @@ -1119,9 +1155,8 @@ contract ElectionTest_Activate is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -1183,6 +1218,15 @@ contract ElectionTest_Activate is ElectionTest { election.activate(group); } + function test_Reverts_WhenBlocked() public { + WhenVoterHasPendingVotes(); + blockTravel(ph.epochSize() + 1); + + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + election.activate(group); + } + function WhenAnotherVoterActivatesVotes() public { WhenEpochBoundaryHasPassed(); lockedGold.incrementNonvotingAccountBalance(voter2, value2); @@ -1275,9 +1319,8 @@ contract ElectionTest_Activate_L2 is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -1291,7 +1334,7 @@ contract ElectionTest_Activate_L2 is ElectionTest { function WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); election.activate(group); } @@ -1333,7 +1376,7 @@ contract ElectionTest_Activate_L2 is ElectionTest { function test_ShouldEmitValidatorGroupVoteActivatedEvent_WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); vm.expectEmit(true, true, true, false); emit ValidatorGroupVoteActivated(voter, group, value, value * 100000000000000000000); election.activate(group); @@ -1344,7 +1387,7 @@ contract ElectionTest_Activate_L2 is ElectionTest { lockedGold.incrementNonvotingAccountBalance(voter2, value2); vm.prank(voter2); election.vote(group, value2, address(0), address(0)); - blockTravel(ph.epochSize() + 1); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); vm.prank(voter2); election.activate(group); } @@ -1431,9 +1474,8 @@ contract ElectionTest_ActivateForAccount is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -1586,9 +1628,8 @@ contract ElectionTest_ActivateForAccount_L2 is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -1602,7 +1643,7 @@ contract ElectionTest_ActivateForAccount_L2 is ElectionTest { function WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); election.activateForAccount(group, voter); } @@ -1644,7 +1685,7 @@ contract ElectionTest_ActivateForAccount_L2 is ElectionTest { function test_ShouldEmitValidatorGroupVoteActivatedEvent_WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); vm.expectEmit(true, true, true, false); emit ValidatorGroupVoteActivated(voter, group, value, value * 100000000000000000000); election.activate(group); @@ -1655,7 +1696,7 @@ contract ElectionTest_ActivateForAccount_L2 is ElectionTest { lockedGold.incrementNonvotingAccountBalance(voter2, value2); vm.prank(voter2); election.vote(group, value2, address(0), address(0)); - blockTravel(ph.epochSize() + 1); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); election.activateForAccount(group, voter2); } @@ -1742,9 +1783,8 @@ contract ElectionTest_RevokePending is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -1913,9 +1953,8 @@ contract ElectionTest_RevokeActive is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue0 + voteValue1); validators.setNumRegisteredValidators(1); @@ -1944,7 +1983,7 @@ contract ElectionTest_RevokeActive is ElectionTest { } function WhenTheValidatorGroupHasVotesButIsIneligible() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupIneligible(group); election.revokeActive(group, revokedValue, address(0), address(0), 0); } @@ -2000,7 +2039,7 @@ contract ElectionTest_RevokeActive is ElectionTest { function test_ShouldEmitValidatorGroupActiveVoteRevokedEvent_WhenTheValidatorGroupHasVotesButIsIneligible() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupIneligible(group); vm.expectEmit(true, true, true, false); emit ValidatorGroupActiveVoteRevoked( @@ -2012,6 +2051,12 @@ contract ElectionTest_RevokeActive is ElectionTest { election.revokeActive(group, revokedValue, address(0), address(0), 0); } + function test_Reverts_WhenBlocked() public { + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + election.revokeAllActive(group, address(0), address(0), 0); + } + function WhenRevokedValueIsLessThanTheActiveVotesButGroupIsEligible() public { election.revokeActive(group, revokedValue, address(0), address(0), 0); } @@ -2071,7 +2116,7 @@ contract ElectionTest_RevokeActive is ElectionTest { function test_ShouldEmitValidatorGroupActiveVoteRevokedEvent_WhenRevokedValueIsLessThanTheActiveVotesButGroupIsEligible() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupIneligible(group); vm.expectEmit(true, true, true, false); emit ValidatorGroupActiveVoteRevoked( @@ -2156,7 +2201,7 @@ contract ElectionTest_RevokeActive is ElectionTest { } } -contract ElectionTest_ElectionValidatorSigners is ElectionTest { +contract ElectionTest_ElectValidatorsAbstract is ElectionTest { struct MemberWithVotes { address member; uint256 votes; @@ -2243,42 +2288,24 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { membersWithVotes.push(MemberWithVotes(members[j], votesConsideredForElection[members[j]])); } validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), prev); - registry.setAddressFor("Validators", address(validators)); vm.prank(voter1); election.vote(group, randomVotes[i], prev, address(0)); prev = group; } } - function test_ShouldElectCorrectValidators_WhenThereIsALargeNumberOfGroups() public { - WhenThereIsALargeNumberOfGroups(); - address[] memory elected = election.electValidatorSigners(); - MemberWithVotes[] memory sortedMembersWithVotes = sortMembersWithVotesDesc(membersWithVotes); - MemberWithVotes[] memory electedUnsorted = new MemberWithVotes[](100); - - for (uint256 i = 0; i < 100; i++) { - electedUnsorted[i] = MemberWithVotes(elected[i], votesConsideredForElection[elected[i]]); - } - MemberWithVotes[] memory electedSorted = sortMembersWithVotesDesc(electedUnsorted); - - for (uint256 i = 0; i < 100; i++) { - assertEq(electedSorted[i].member, sortedMembersWithVotes[i].member); - assertEq(electedSorted[i].votes, sortedMembersWithVotes[i].votes); - } - } - function WhenThereAreSomeGroups() public { validators.setMembers(group1, group1Members); validators.setMembers(group2, group2Members); validators.setMembers(group3, group3Members); - registry.setAddressFor("Validators", address(this)); + vm.startPrank(address(validators)); election.markGroupEligible(group1, address(0), address(0)); election.markGroupEligible(group2, address(0), group1); election.markGroupEligible(group3, address(0), group2); - registry.setAddressFor("Validators", address(validators)); + vm.stopPrank(); lockedGold.incrementNonvotingAccountBalance(address(voter1), voter1Weight); lockedGold.incrementNonvotingAccountBalance(address(voter2), voter2Weight); @@ -2288,6 +2315,59 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { validators.setNumRegisteredValidators(7); } + // Helper function to sort an array of uint256 + function sort(uint256[] memory data) internal pure returns (uint256[] memory) { + uint256 length = data.length; + for (uint256 i = 0; i < length; i++) { + for (uint256 j = i + 1; j < length; j++) { + if (data[i] > data[j]) { + uint256 temp = data[i]; + data[i] = data[j]; + data[j] = temp; + } + } + } + return data; + } + + function sortMembersWithVotesDesc( + MemberWithVotes[] memory data + ) internal pure returns (MemberWithVotes[] memory) { + uint256 length = data.length; + for (uint256 i = 0; i < length; i++) { + for (uint256 j = i + 1; j < length; j++) { + if (data[i].votes < data[j].votes) { + MemberWithVotes memory temp = data[i]; + data[i] = data[j]; + data[j] = temp; + } + } + } + return data; + } +} + +contract ElectionTest_ElectValidatorSigners is ElectionTest_ElectValidatorsAbstract { + function test_ShouldElectCorrectValidators_WhenThereIsALargeNumberOfGroupsSig() public { + WhenThereIsALargeNumberOfGroups(); + address[] memory elected = election.electValidatorSigners(); + + MemberWithVotes[] memory sortedMembersWithVotes = sortMembersWithVotesDesc(membersWithVotes); + + MemberWithVotes[] memory electedUnsorted = new MemberWithVotes[](100); + + for (uint256 i = 0; i < 100; i++) { + electedUnsorted[i] = MemberWithVotes(elected[i], votesConsideredForElection[elected[i]]); + } + + MemberWithVotes[] memory electedSorted = sortMembersWithVotesDesc(electedUnsorted); + + for (uint256 i = 0; i < 100; i++) { + assertEq(electedSorted[i].member, sortedMembersWithVotes[i].member); + assertEq(electedSorted[i].votes, sortedMembersWithVotes[i].votes); + } + } + function test_ShouldReturnThatGroupsMemberLIst_WhenASingleGroupHasMoreOrEqualToMinElectableValidatorsAsMembersAndReceivedVotes() public { @@ -2379,39 +2459,130 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { vm.expectRevert("Not enough elected validators"); election.electValidatorSigners(); } +} - // Helper function to sort an array of uint256 - function sort(uint256[] memory data) internal pure returns (uint256[] memory) { - uint256 length = data.length; - for (uint256 i = 0; i < length; i++) { - for (uint256 j = i + 1; j < length; j++) { - if (data[i] > data[j]) { - uint256 temp = data[i]; - data[i] = data[j]; - data[j] = temp; - } - } +contract ElectionTest_ElectValidatorSignersL2 is + ElectionTest_ElectValidatorSigners, + TransitionToL2After +{} + +contract ElectionTest_ElectValidatorsAccounts is ElectionTest_ElectValidatorsAbstract { + function test_ShouldElectCorrectValidators_WhenThereIsALargeNumberOfGroups() public { + WhenThereIsALargeNumberOfGroups(); + address[] memory elected = election.electValidatorAccounts(); + MemberWithVotes[] memory sortedMembersWithVotes = sortMembersWithVotesDesc(membersWithVotes); + MemberWithVotes[] memory electedUnsorted = new MemberWithVotes[](100); + + for (uint256 i = 0; i < 100; i++) { + electedUnsorted[i] = MemberWithVotes(elected[i], votesConsideredForElection[elected[i]]); } - return data; - } + MemberWithVotes[] memory electedSorted = sortMembersWithVotesDesc(electedUnsorted); - function sortMembersWithVotesDesc( - MemberWithVotes[] memory data - ) internal pure returns (MemberWithVotes[] memory) { - uint256 length = data.length; - for (uint256 i = 0; i < length; i++) { - for (uint256 j = i + 1; j < length; j++) { - if (data[i].votes < data[j].votes) { - MemberWithVotes memory temp = data[i]; - data[i] = data[j]; - data[j] = temp; - } - } + for (uint256 i = 0; i < 100; i++) { + assertEq(electedSorted[i].member, sortedMembersWithVotes[i].member); + assertEq(electedSorted[i].votes, sortedMembersWithVotes[i].votes); } - return data; + } + + function test_ShouldReturnThatGroupsMemberLIst_WhenASingleGroupHasMoreOrEqualToMinElectableValidatorsAsMembersAndReceivedVotes() + public + { + WhenThereAreSomeGroups(); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + setRandomness(); + arraysEqual(election.electValidatorAccounts(), group1Members); + } + + function test_ShouldReturnMaxElectableValidatorsElectedValidators_WhenGroupWithMoreThenMaxElectableValidatorsMembersReceivesVotes() + public + { + WhenThereAreSomeGroups(); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + vm.prank(voter2); + election.vote(group2, voter2Weight, address(0), group1); + vm.prank(voter3); + election.vote(group3, voter3Weight, address(0), group2); + + setRandomness(); + address[] memory expected = new address[](6); + expected[0] = validator1; + expected[1] = validator2; + expected[2] = validator3; + expected[3] = validator5; + expected[4] = validator6; + expected[5] = validator7; + arraysEqual(election.electValidatorAccounts(), expected); + } + + function test_ShouldElectOnlyNMembersFromThatGroup_WhenAGroupReceivesEnoughVotesForMoreThanNSeatsButOnlyHasNMembers() + public + { + WhenThereAreSomeGroups(); + uint256 increment = 80; + uint256 votes = 80; + lockedGold.incrementNonvotingAccountBalance(address(voter3), increment); + lockedGold.setTotalLockedGold(totalLockedGold + increment); + vm.prank(voter3); + election.vote(group3, votes, group2, address(0)); + vm.prank(voter1); + election.vote(group1, voter1Weight, address(0), group3); + vm.prank(voter2); + election.vote(group2, voter2Weight, address(0), group1); + setRandomness(); + + address[] memory expected = new address[](6); + expected[0] = validator1; + expected[1] = validator2; + expected[2] = validator3; + expected[3] = validator5; + expected[4] = validator6; + expected[5] = validator7; + arraysEqual(election.electValidatorAccounts(), expected); + } + + function test_ShouldNotElectAnyMembersFromThatGroup_WhenAGroupDoesNotReceiveElectabilityThresholdVotes() + public + { + WhenThereAreSomeGroups(); + uint256 thresholdExcludingGroup3 = (voter3Weight + 1) / totalLockedGold; + election.setElectabilityThreshold(thresholdExcludingGroup3); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + vm.prank(voter2); + election.vote(group2, voter2Weight, address(0), group1); + vm.prank(voter3); + election.vote(group3, voter3Weight, address(0), group2); + + address[] memory expected = new address[](6); + expected[0] = validator1; + expected[1] = validator2; + expected[2] = validator3; + expected[3] = validator4; + expected[4] = validator5; + expected[5] = validator6; + arraysEqual(election.electValidatorAccounts(), expected); + } + + function test_ShouldRevert_WhenThereAnoNotEnoughElectableValidators() public { + WhenThereAreSomeGroups(); + vm.prank(voter2); + election.vote(group2, voter2Weight, group1, address(0)); + vm.prank(voter3); + election.vote(group3, voter3Weight, address(0), group2); + setRandomness(); + vm.expectRevert("Not enough elected validators"); + election.electValidatorAccounts(); } } +// reruns all the ElectionTest_ElectValidatorsAccounts with L2 turned on +contract ElectionTest_ElectValidatorsAccountsL2 is + ElectionTest_ElectValidatorsAccounts, + TransitionToL2After +{} + contract ElectionTest_GetGroupEpochRewards is ElectionTest { address voter = address(this); address group1 = account2; @@ -2429,8 +2600,9 @@ contract ElectionTest_GetGroupEpochRewards is ElectionTest { function setUp() public { super.setUp(); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group1, address(0), address(0)); + vm.prank(address(validators)); election.markGroupEligible(group2, address(0), group1); registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue1 + voteValue2); @@ -2549,9 +2721,8 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { function setUp() public { super.setUp(); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue); address[] memory membersGroup = new address[](1); @@ -2574,13 +2745,6 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { assertEq(election.getActiveVotesForGroupByAccount(group, voter), voteValue + rewardValue); } - function test_Revert_DistributeEpochRewards_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - vm.prank(address(0)); - election.distributeEpochRewards(group, rewardValue, address(0), address(0)); - } - function test_ShouldIncrementAccountTotalVotesForGroup_WhenThereIsSingleGroupWithActiveVotes() public { @@ -2604,9 +2768,8 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { } function WhenThereAreTwoGroupsWithActiveVotes() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group2, address(0), group); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue + voteValue2); validators.setNumRegisteredValidators(2); @@ -2715,19 +2878,14 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { uint256 group1RemainingActiveVotes; address[] initialOrdering; - function setUp() public { - super.setUp(); - } - function WhenAccountHasVotedForOneGroup() public { address[] memory membersGroup = new address[](1); membersGroup[0] = account8; validators.setMembers(group, membersGroup); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setNumRegisteredValidators(1); lockedGold.incrementNonvotingAccountBalance(voter, value); @@ -2749,6 +2907,21 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { election.forceDecrementVotes(voter, slashedValue, lessers, greaters, indices); } + function test_Reverts_WhenBlocked() public { + WhenAccountHasVotedForOneGroup(); + address[] memory lessers = new address[](1); + lessers[0] = address(0); + address[] memory greaters = new address[](1); + greaters[0] = address(0); + uint256[] memory indices = new uint256[](1); + indices[0] = index; + + blocker.mockSetBlocked(true); + vm.prank(account2); + vm.expectRevert("Contract is blocked from performing this action"); + election.forceDecrementVotes(voter, slashedValue, lessers, greaters, indices); + } + function test_ShouldDecrementPendingVotesToZero_WhenAccountHasOnlyPendingVotes() public { WhenAccountHasOnlyPendingVotes(); assertEq(election.getPendingVotesForGroupByAccount(group, voter), remaining); @@ -2810,10 +2983,11 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { membersGroup2[0] = account9; validators.setMembers(group2, membersGroup2); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); + vm.prank(address(validators)); election.markGroupEligible(group2, group, address(0)); - registry.setAddressFor("Validators", address(validators)); + lockedGold.setTotalLockedGold(value); validators.setNumRegisteredValidators(2); lockedGold.incrementNonvotingAccountBalance(voter, value); @@ -2883,10 +3057,11 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { membersGroup2[0] = account9; validators.setMembers(group2, membersGroup2); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); + vm.prank(address(validators)); election.markGroupEligible(group2, group, address(0)); - registry.setAddressFor("Validators", address(validators)); + lockedGold.setTotalLockedGold(value + value2); validators.setNumRegisteredValidators(2); lockedGold.incrementNonvotingAccountBalance(voter, value + value2); @@ -3138,9 +3313,8 @@ contract ElectionTest_ConsistencyChecks is ElectionTest { address[] memory members = new address[](1); members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voterStartBalance * accountsArray.length); validators.setNumRegisteredValidators(1); for (uint256 i = 0; i < accountsArray.length; i++) { @@ -3340,9 +3514,8 @@ contract ElectionTest_HasActivatablePendingVotes is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); diff --git a/packages/protocol/test-sol/unit/governance/voting/LockedGold.t.sol b/packages/protocol/test-sol/unit/governance/voting/LockedGold.t.sol index bf1ca263b24..8121f86de2e 100644 --- a/packages/protocol/test-sol/unit/governance/voting/LockedGold.t.sol +++ b/packages/protocol/test-sol/unit/governance/voting/LockedGold.t.sol @@ -17,6 +17,8 @@ import "@celo-contracts/governance/test/MockElection.sol"; import "@celo-contracts/governance/test/MockGovernance.sol"; import "@celo-contracts/governance/test/MockValidators.sol"; +import { TestBlocker } from "@test-sol/unit/common/Blockable.t.sol"; + contract LockedGoldTest is Test, TestConstants { using FixidityLib for FixidityLib.Fraction; @@ -36,6 +38,7 @@ contract LockedGoldTest is Test, TestConstants { address randomAddress = actor("randomAddress"); address caller = address(this); + TestBlocker blocker; event UnlockingPeriodSet(uint256 period); event GoldLocked(address indexed account, uint256 value); @@ -84,6 +87,10 @@ contract LockedGoldTest is Test, TestConstants { registry.setAddressFor("StableToken", address(stableToken)); lockedGold.initialize(address(registry), unlockingPeriod); accounts.createAccount(); + + blocker = new TestBlocker(); + + lockedGold.setBlockedByContract(address(blocker)); } function getParsedSignatureOfAddress( @@ -1109,6 +1116,14 @@ contract LockedGoldTest_slash is LockedGoldTest { lockedGold.slash(caller, penalty, reporter, reward, lessers, greaters, indices); } + function test_Reverts_WhenBlocked() public { + uint256 penalty = value; + uint256 reward = value / 2; + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + } + function test_ShouldReduceAccountsLockedGoldBalance_WhenAccountIsSlashedForAllOfItsLockedGold() public { diff --git a/packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol b/packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol index 1ac8749efbf..f5aae08777c 100644 --- a/packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol +++ b/packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol @@ -48,7 +48,7 @@ contract ReleaseGoldTest is Test, TestConstants, ECDSAHelper { address refundAddress = actor("refundAddress"); address newBeneficiary = actor("newBeneficiary"); address randomAddress = actor("randomAddress"); - address celoDistributionSchedule = actor("CeloDistributionSchedule"); + address celoUnreleasedTreasury = actor("CeloUnreleasedTreasury"); uint256 constant TOTAL_AMOUNT = 1 ether * 10; @@ -125,7 +125,7 @@ contract ReleaseGoldTest is Test, TestConstants, ECDSAHelper { registry.setAddressFor("LockedGold", address(lockedGold)); registry.setAddressFor("Validators", address(validators)); registry.setAddressFor("StableToken", address(stableToken)); - registry.setAddressFor("CeloDistributionSchedule", celoDistributionSchedule); + registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasury); lockedGold.initialize(REGISTRY_ADDRESS, UNLOCKING_PERIOD); goldToken.initialize(REGISTRY_ADDRESS); @@ -1115,9 +1115,8 @@ contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { assertEq(accounts.validatorSignerToAccount(authorized), address(releaseGold)); } - function test_Reverts_WhenAuthorizeValidatorSignerWithPublicKeyCalledOnL2() public { + function test_ShouldSetTheAuthorizedKeys_WhenUsingECDSAPublickKey_WhenL2() public { _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); vm.prank(beneficiary); releaseGold.authorizeValidatorSignerWithPublicKey( address(uint160(authorized)), @@ -1126,6 +1125,10 @@ contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { s, ecdsaPublicKey ); + + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + assertEq(accounts.validatorSignerToAccount(authorized), address(releaseGold)); } function test_Reverts_WhenAuthorizeValidatorSignerWithKeysCalledOnL2() public { diff --git a/packages/protocol/test-sol/unit/stability/FeeCurrencyAdapter.t.sol b/packages/protocol/test-sol/unit/stability/FeeCurrencyAdapter.t.sol index 13f13bd6d97..44a754e231c 100644 --- a/packages/protocol/test-sol/unit/stability/FeeCurrencyAdapter.t.sol +++ b/packages/protocol/test-sol/unit/stability/FeeCurrencyAdapter.t.sol @@ -130,7 +130,6 @@ contract FeeCurrencyAdapter_Initialize is FeeCurrencyAdapterTest { function test_ShouldSucceed_WhenExpectedDecimalsAreMoreThenDecimals_Fuzz(uint8 amount) public { vm.assume(amount > 6); vm.assume(amount < 50); - console.log("amount", amount); feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", amount); } @@ -303,7 +302,6 @@ contract FeeCurrencyAdapter_CreditGasFees is FeeCurrencyAdapterTest { function creditFuzzHelper(uint8 expectedDigits, uint256 multiplier) public { uint256 originalAmount = 1000; uint256 amount = originalAmount * multiplier; - console.log("amount", amount); address secondAddress = actor("secondAddress"); address thirdAddress = actor("thirdAddress"); diff --git a/packages/protocol/test-sol/utils/ECDSAHelper.sol b/packages/protocol/test-sol/utils/ECDSAHelper.sol index cd85d52cccd..aaa7c1119ae 100644 --- a/packages/protocol/test-sol/utils/ECDSAHelper.sol +++ b/packages/protocol/test-sol/utils/ECDSAHelper.sol @@ -13,7 +13,7 @@ contract ECDSAHelper is Test { bytes32 _s ) public returns (bytes memory) { address SECP256K1Address = actor("SECP256K1Address"); - deployCodeTo("out/SECP256K1.sol/SECP256K1.0.5.17.json", SECP256K1Address); + deployCodeTo("SECP256K1.sol:SECP256K1", SECP256K1Address); sECP256K1 = ISECP256K1(SECP256K1Address); string memory header = "\x19Ethereum Signed Message:\n32"; diff --git a/packages/protocol/test-sol/utils/ECDSAHelper08.sol b/packages/protocol/test-sol/utils/ECDSAHelper08.sol new file mode 100644 index 00000000000..3c761bb9e76 --- /dev/null +++ b/packages/protocol/test-sol/utils/ECDSAHelper08.sol @@ -0,0 +1,34 @@ +pragma solidity >=0.5.13 <0.8.20; +import "celo-foundry-8/Test.sol"; +import "@test-sol/utils/SECP256K1.sol"; + +contract ECDSAHelper08 is Test { + ISECP256K1 sECP256K1; + + function addressToPublicKey( + bytes32 message, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public returns (bytes memory) { + address SECP256K1Address = actor("SECP256K1Address"); + deployCodeTo("SECP256K1.sol:SECP256K1", SECP256K1Address); + sECP256K1 = ISECP256K1(SECP256K1Address); + + string memory header = "\x19Ethereum Signed Message:\n32"; + bytes32 _message = keccak256(abi.encodePacked(header, message)); + (uint256 x, uint256 y) = sECP256K1.recover( + uint256(_message), + _v - 27, + uint256(_r), + uint256(_s) + ); + return abi.encodePacked(x, y); + } + + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } +} diff --git a/packages/protocol/test-sol/utils08.sol b/packages/protocol/test-sol/utils08.sol index 87e2e744c16..bb069cea58b 100644 --- a/packages/protocol/test-sol/utils08.sol +++ b/packages/protocol/test-sol/utils08.sol @@ -1,8 +1,31 @@ pragma solidity >=0.5.13 <0.9.0; +import "celo-foundry-8/Test.sol"; + contract Utils08 { + uint256 public constant secondsInOneBlock = 5; + + function timeTravel(Vm vm, uint256 timeDelta) public { + vm.warp(block.timestamp + timeDelta); + } + + function blockTravel(Vm vm, uint256 blockDelta) public { + vm.roll(block.number + blockDelta); + } + + function travelEpochL1(Vm vm) public { + uint256 blocksInEpoch = 17280; + uint256 timeDelta = blocksInEpoch * 5; + blockTravel(vm, blocksInEpoch); + timeTravel(vm, timeDelta); + } + // This function can be also found in OpenZeppelin's library, but in a newer version than the one function compareStrings(string memory a, string memory b) public pure returns (bool) { return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b)))); } + + function whenL2(Vm vm) public { + vm.etch(0x4200000000000000000000000000000000000018, abi.encodePacked(bytes1(0x01))); + } } diff --git a/packages/protocol/test/common/recoverFunds.ts b/packages/protocol/test/common/recoverFunds.ts index 89fce8f98af..7278ab72d60 100644 --- a/packages/protocol/test/common/recoverFunds.ts +++ b/packages/protocol/test/common/recoverFunds.ts @@ -3,6 +3,7 @@ import { recoverFunds } from '@celo/protocol/lib/recover-funds' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { expectBigNumberInRange } from '@celo/protocol/lib/test-utils' +import { CeloUnreleasedTreasuryContract } from '@celo/protocol/types/08' import { BigNumber } from 'bignumber.js' import { FreezerContract, @@ -11,7 +12,6 @@ import { ProxyInstance, RegistryContract, } from 'types' -import { CeloDistributionScheduleContract } from 'types/08' import { SOLIDITY_08_PACKAGE } from '../../contractPackages' import { ArtifactsSingleton } from '../../lib/artifactsSingleton' @@ -37,21 +37,21 @@ contract('Proxy', (accounts: string[]) => { it('recovers funds from an incorrectly intialized implementation', async () => { const Freezer: FreezerContract = artifacts.require('Freezer') const GoldToken: GoldTokenContract = artifacts.require('GoldToken') - const CeloDistributionSchedule: CeloDistributionScheduleContract = - ArtifactsSingleton.getInstance(SOLIDITY_08_PACKAGE).require('CeloDistributionSchedule') // Added because the CeloToken `_transfer` prevents transfers to the celoDistributionSchedule. + const CeloUnreleasedTreasury: CeloUnreleasedTreasuryContract = + ArtifactsSingleton.getInstance(SOLIDITY_08_PACKAGE).require('CeloUnreleasedTreasury') // Added because the CeloToken `_transfer` prevents transfers to the celoUnreleasedTreasury. // @ts-ignore GoldToken.numberFormat = 'BigNumber' const Registry: RegistryContract = artifacts.require('Registry') const freezer = await Freezer.new(true) const goldToken = await GoldToken.new(true) - const celoDistributionSchedule = await CeloDistributionSchedule.new(true) + const celoUnreleasedTreasury = await CeloUnreleasedTreasury.new(true) const registry = await Registry.new(true) await registry.setAddressFor(CeloContractName.Freezer, freezer.address) await registry.setAddressFor( - CeloContractName.CeloDistributionSchedule, - celoDistributionSchedule.address + CeloContractName.CeloUnreleasedTreasury, + celoUnreleasedTreasury.address ) await goldToken.initialize(registry.address) diff --git a/packages/protocol/truffle-config0.8.js b/packages/protocol/truffle-config0.8.js index da81a714ffc..2c40deae146 100644 --- a/packages/protocol/truffle-config0.8.js +++ b/packages/protocol/truffle-config0.8.js @@ -16,6 +16,10 @@ module.exports = { version: SOLC_VERSION, settings: { metadata: { useLiteralContent: true }, + optimizer: { + enabled: true, + runs: 200, + }, }, }, }, diff --git a/remappings.txt b/remappings.txt index e6c8c6ef51b..136032d5144 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ forge-std-8/=packages/protocol/lib/celo-foundry-8/lib/forge-std/src/ celo-foundry/=packages/protocol/lib/celo-foundry/src/ openzeppelin-solidity/=packages/protocol/lib/openzeppelin-contracts/ solidity-bytes-utils/=packages/protocol/lib/solidity-bytes-utils/ +solidity-bytes-utils-8/=packages/protocol/lib/solidity-bytes-utils-8/ contracts=packages/protocol/contracts/ diff --git a/yarn.lock b/yarn.lock index fbfd28c377e..26e6e685b49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8884,6 +8884,10 @@ drbg.js@^1.0.1: create-hash "^1.1.2" create-hmac "^1.1.4" +"ds-test@github:dapphub/ds-test": + version "1.0.0" + resolved "https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0" + dtrace-provider@~0.8: version "0.8.8" resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" @@ -10715,6 +10719,11 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +forge-std@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/forge-std/-/forge-std-1.1.2.tgz#f4a0eda103538d56f9c563f3cd1fa2fd01bd9378" + integrity sha512-Wfb0iAS9PcfjMKtGpWQw9mXzJxrWD62kJCUqqLcyuI0+VRtJ3j20XembjF3kS20qELYdXft1vD/SPFVWVKMFOw== + form-data-encoder@1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.1.tgz#ac80660e4f87ee0d3d3c3638b7da8278ddb8ec96" @@ -18932,6 +18941,14 @@ solhint@^4.5.4: optionalDependencies: prettier "^2.8.3" +"solidity-bytes-utils-8@npm:solidity-bytes-utils@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/solidity-bytes-utils/-/solidity-bytes-utils-0.8.2.tgz#763d6a02fd093e93b3a97b742e97d540e66c29bd" + integrity sha512-cqXPYAV2auhpdKSTPuqji0CwpSceZDu95CzqSM/9tDJ2MoMaMsdHTpOIWtVw31BIqqGPNmIChCswzbw0tHaMTw== + dependencies: + ds-test "github:dapphub/ds-test" + forge-std "^1.1.2" + solidity-bytes-utils@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/solidity-bytes-utils/-/solidity-bytes-utils-0.0.7.tgz#ccc865a6694b4865f2020cee37c15cc26f81cf9b" @@ -19160,7 +19177,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19186,15 +19203,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -19286,7 +19294,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19314,13 +19322,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -22287,7 +22288,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22313,15 +22314,6 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"