diff --git a/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasure.sol b/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasure.sol index 1e17e37d517..d2b263536d3 100644 --- a/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasure.sol +++ b/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasure.sol @@ -8,9 +8,7 @@ import "./UsingRegistry.sol"; import "../common/IsL2Check.sol"; import "../../contracts/common/Initializable.sol"; -import "../../contracts/common/interfaces/ICeloToken.sol"; import "./interfaces/ICeloUnreleasedTreasureInitializer.sol"; -import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; /** * @title Contract for unreleased Celo tokens. @@ -49,8 +47,7 @@ contract CeloUnreleasedTreasure is UsingRegistry, ReentrancyGuard, Initializable */ function release(address to, uint256 amount) external onlyEpochManager { require(address(this).balance >= amount, "Insufficient balance."); - IERC20 celoToken = IERC20(address(getCeloToken())); - celoToken.transfer(to, amount); + require(getCeloToken().transfer(to, amount), "CELO transfer failed."); emit Released(to, amount); } diff --git a/packages/protocol/contracts-0.8/common/EpochManager.sol b/packages/protocol/contracts-0.8/common/EpochManager.sol index 6301ac70b26..00813f44f21 100644 --- a/packages/protocol/contracts-0.8/common/EpochManager.sol +++ b/packages/protocol/contracts-0.8/common/EpochManager.sol @@ -11,6 +11,7 @@ import "../common/UsingRegistry.sol"; import "../../contracts/common/Initializable.sol"; import "../../contracts/common/interfaces/IEpochManager.sol"; import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/common/interfaces/IEpochManager.sol"; contract EpochManager is Initializable, @@ -23,7 +24,6 @@ contract EpochManager is uint256 firstBlock; uint256 lastBlock; uint256 startTimestamp; - uint256 endTimestamp; uint256 rewardsBlock; } @@ -33,7 +33,7 @@ contract EpochManager is } struct EpochProcessState { - EpochProcessStatus status; // TODO maybe a enum for future updates + EpochProcessStatus status; uint256 perValidatorReward; // The per validator epoch reward. uint256 totalRewardsVoter; // The total rewards to voters. uint256 totalRewardsCommunity; // The total community reward. @@ -59,7 +59,7 @@ contract EpochManager is mapping(address => uint256) public validatorPendingPayments; address public carbonOffsettingPartner; - address public epochManagerInitializer; + address public epochManagerEnabler; /** * @notice Event emited when epochProcessing has begun. @@ -73,8 +73,8 @@ contract EpochManager is */ event EpochProcessingEnded(uint256 indexed epochNumber); - modifier onlyEpochManagerInitializer() { - require(msg.sender == epochManagerInitializer, "msg.sender is not Initializer"); + modifier onlyEpochManagerEnabler() { + require(msg.sender == epochManagerEnabler, "msg.sender is not Initializer"); _; } @@ -93,14 +93,15 @@ contract EpochManager is address registryAddress, uint256 newEpochDuration, address _carbonOffsettingPartner, - address _epochManagerInitializer + address _epochManagerEnabler ) external initializer { - require(_epochManagerInitializer != address(0), "EpochManagerInitializer address is required"); + require(_carbonOffsettingPartner != address(0), "carbonOffsettingPartner address is required"); + require(_epochManagerEnabler != address(0), "EpochManagerEnabler address is required"); _transferOwnership(msg.sender); setRegistry(registryAddress); setEpochDuration(newEpochDuration); carbonOffsettingPartner = _carbonOffsettingPartner; - epochManagerInitializer = _epochManagerInitializer; + epochManagerEnabler = _epochManagerEnabler; } // DESIGNDESICION(XXX): we assume that the first epoch on the L2 starts as soon as the system is initialized @@ -110,7 +111,16 @@ contract EpochManager is uint256 firstEpochNumber, uint256 firstEpochBlock, address[] memory firstElected - ) external onlyEpochManagerInitializer { + ) external onlyEpochManagerEnabler { + require( + address(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURE_REGISTRY_ID)).balance > 0, + "CeloUnreleasedTreasury not yet funded." + ); + require( + getCeloToken().balanceOf(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURE_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"); @@ -127,7 +137,7 @@ contract EpochManager is _currentEpoch.startTimestamp = block.timestamp; elected = firstElected; - epochManagerInitializer = address(0); + epochManagerEnabler = address(0); } // TODO maybe "freezeEpochRewards" "prepareForNextEpoch" @@ -171,8 +181,6 @@ contract EpochManager is // TODO complete this function require(isOnEpochProcess(), "Epoch process is not started"); // finalize epoch - // TODO last block should be the block before and timestamp from previous block - epochs[currentEpochNumber].endTimestamp = block.timestamp; epochs[currentEpochNumber].lastBlock = block.number - 1; // start new epoch currentEpochNumber++; @@ -218,16 +226,9 @@ contract EpochManager is } /// returns the current epoch Info - function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256, uint256) { + function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256) { Epoch storage _epoch = epochs[currentEpochNumber]; - - return ( - _epoch.firstBlock, - _epoch.lastBlock, - _epoch.startTimestamp, - _epoch.endTimestamp, - _epoch.rewardsBlock - ); + return (_epoch.firstBlock, _epoch.lastBlock, _epoch.startTimestamp, _epoch.rewardsBlock); } /// returns the current epoch number. @@ -236,6 +237,21 @@ contract EpochManager is return currentEpochNumber; } + /// returns epoch processing state + function getEpochProcessingState() + external + view + returns (uint256, uint256, uint256, uint256, uint256) + { + return ( + uint256(epochProcessing.status), + epochProcessing.perValidatorReward, + epochProcessing.totalRewardsVoter, + epochProcessing.totalRewardsCommunity, + epochProcessing.totalRewardsCarbonFund + ); + } + function getElected() external view returns (address[] memory) { return elected; } @@ -285,11 +301,10 @@ contract EpochManager is } function systemAlreadyInitialized() public view returns (bool) { - return initialized && epochManagerInitializer == address(0); + return initialized && epochManagerEnabler == address(0); } function allocateValidatorsRewards() internal { - // TODO complete this function uint256 totalRewards = 0; IScoreReader scoreReader = getScoreReader(); IValidators validators = getValidators(); @@ -305,7 +320,7 @@ contract EpochManager is totalRewards += validatorReward; } // Mint all cUSD required for payment and the corresponding CELO - IStableToken(getStableToken()).mint(address(this), totalRewards); + validators.mintStableToEpochManager(totalRewards); // this should have a setter for the oracle. (uint256 numerator, uint256 denominator) = IOracle(address(getSortedOracles())).getExchangeRate( @@ -313,7 +328,6 @@ contract EpochManager is ); uint256 CELOequivalent = (numerator * totalRewards) / denominator; - // this is not a mint anymore getCeloUnreleasedTreasure().release( registry.getAddressForOrDie(RESERVE_REGISTRY_ID), CELOequivalent diff --git a/packages/protocol/contracts-0.8/common/EpochManagerInitializer.sol b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol similarity index 94% rename from packages/protocol/contracts-0.8/common/EpochManagerInitializer.sol rename to packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol index 2d9ef2dbac3..ba33773b9b9 100644 --- a/packages/protocol/contracts-0.8/common/EpochManagerInitializer.sol +++ b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol @@ -6,9 +6,10 @@ import "../common/UsingPrecompiles.sol"; import "../../contracts/common/Initializable.sol"; import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/common/interfaces/IEpochManagerEnabler.sol"; import "../../contracts/governance/interfaces/IEpochRewards.sol"; -contract EpochManagerInitializer is Initializable, UsingPrecompiles, UsingRegistry { +contract EpochManagerEnabler is Initializable, UsingPrecompiles, UsingRegistry { uint256 public lastKnownEpochNumber; address[] public lastKnownElectedAccounts; diff --git a/packages/protocol/contracts-0.8/common/ScoreManager.sol b/packages/protocol/contracts-0.8/common/ScoreManager.sol index d91705cdf65..c8075d52733 100644 --- a/packages/protocol/contracts-0.8/common/ScoreManager.sol +++ b/packages/protocol/contracts-0.8/common/ScoreManager.sol @@ -16,10 +16,8 @@ contract ScoreManager is Initializable, Ownable { /** * @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 { + function initialize() external initializer { _transferOwnership(msg.sender); } diff --git a/packages/protocol/contracts-0.8/common/UsingRegistry.sol b/packages/protocol/contracts-0.8/common/UsingRegistry.sol index 5815b9807ef..9b1d13dd3e7 100644 --- a/packages/protocol/contracts-0.8/common/UsingRegistry.sol +++ b/packages/protocol/contracts-0.8/common/UsingRegistry.sol @@ -12,6 +12,9 @@ import "../../contracts/common/interfaces/IAccounts.sol"; import "../../contracts/common/interfaces/IEpochManager.sol"; import "../../contracts/common/interfaces/IFreezer.sol"; import "../../contracts/common/interfaces/ICeloUnreleasedTreasure.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"; @@ -19,12 +22,8 @@ import "../../contracts/governance/interfaces/IValidators.sol"; import "../../contracts/governance/interfaces/IElection.sol"; import "../../contracts/governance/interfaces/IEpochRewards.sol"; import "../../contracts/stability/interfaces/ISortedOracles.sol"; -import "../../contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; -import "./interfaces/IScoreReader.sol"; -import "../../contracts/governance/interfaces/IElection.sol"; -import "../../contracts/common/interfaces/IFeeHandlerSeller.sol"; -import "../../contracts/governance/interfaces/IEpochRewards.sol"; +import "./interfaces/IScoreReader.sol"; contract UsingRegistry is Ownable { // solhint-disable state-visibility 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/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..119e2f8616e --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerInitializer.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerInitializer { + function initialize( + address registryAddress, + uint256 newEpochDuration, + address _carbonOffsettingPartner, + address _epochManagerEnabler + ) external; +} 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/test/MockCeloToken.sol b/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol new file mode 100644 index 00000000000..82bd1c57cc8 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol @@ -0,0 +1,60 @@ +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; + + 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. + + 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 _transfer(address from, address to, uint256 amount) internal returns (bool) { + if (balances[from] < amount) { + return false; + } + balances[from] -= amount; + balances[to] += amount; + return true; + } + + 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 allocatedSupply() public view returns (uint256) { + return CELO_SUPPLY_CAP - L2_INITIAL_STASH_BALANCE; + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasure.sol b/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasure.sol new file mode 100644 index 00000000000..8aecc77331d --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasure.sol @@ -0,0 +1,15 @@ +pragma solidity >=0.8.0 <0.9.0; +// solhint-disable no-unused-vars + +import "../../../contracts/common/interfaces/ICeloUnreleasedTreasure.sol"; +import "../UsingRegistry.sol"; + +/** + * @title A mock CeloUnreleasedTreasure for testing. + */ +contract MockCeloUnreleasedTreasure is ICeloUnreleasedTreasure, 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 index 8efb865a15b..4125c9c5843 100644 --- a/packages/protocol/contracts-0.8/common/test/MockEpochManager.sol +++ b/packages/protocol/contracts-0.8/common/test/MockEpochManager.sol @@ -22,7 +22,7 @@ contract MockEpochManager is IEpochManager { uint256 public firstKnownEpoch; uint256 private currentEpochNumber; address[] public elected; - address public epochManagerInitializer; + address public epochManagerEnabler; bool initialized; mapping(uint256 => Epoch) private epochs; @@ -45,7 +45,7 @@ contract MockEpochManager is IEpochManager { elected = firstElected; initialized = true; - epochManagerInitializer = address(0); + epochManagerEnabler = address(0); } function startNextEpochProcess() external {} @@ -55,16 +55,10 @@ contract MockEpochManager is IEpochManager { address[] calldata greaters ) external {} - function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256, uint256) { + function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256) { Epoch storage _epoch = epochs[currentEpochNumber]; - return ( - _epoch.firstBlock, - _epoch.lastBlock, - _epoch.startTimestamp, - _epoch.endTimestamp, - _epoch.rewardsBlock - ); + return (_epoch.firstBlock, _epoch.lastBlock, _epoch.startTimestamp, _epoch.rewardsBlock); } function getCurrentEpochNumber() external view returns (uint256) { 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-0.8/governance/Validators.sol b/packages/protocol/contracts-0.8/governance/Validators.sol index 68a816edfcb..98b7dc55f42 100644 --- a/packages/protocol/contracts-0.8/governance/Validators.sol +++ b/packages/protocol/contracts-0.8/governance/Validators.sol @@ -647,6 +647,19 @@ contract Validators is group.slashInfo.lastSlashed = block.timestamp; } + /** + * @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. @@ -886,7 +899,7 @@ contract Validators is address account, uint256 score, uint256 maxPayment - ) external view returns (uint256) { + ) 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"); diff --git a/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol b/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol index af929d275e5..38967cf3928 100644 --- a/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol +++ b/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.7 <0.8.20; import "../../../contracts/governance/interfaces/IEpochRewards.sol"; -// import "forge-std-8/console2.sol"; + /** * @title A wrapper around EpochRewards that exposes internal functions for testing. */ @@ -14,9 +14,7 @@ contract EpochRewardsMock08 is IEpochRewards { } // TODO: (soloseng) implement mock - function updateTargetVotingYield() external { - // console2.log("### Updating Target Voting Yield"); - } + function updateTargetVotingYield() external {} function getRewardsMultiplier( uint256 targetGoldTotalSupplyIncrease @@ -33,8 +31,7 @@ contract EpochRewardsMock08 is IEpochRewards { view returns (uint256, uint256, uint256, uint256) { - // console2.log("### calculating Target Epoch Rewards"); - return (1, 1, 1, 1); + return (5, 5, 5, 5); } function getTargetVotingYieldParameters() external view returns (uint256, uint256, uint256) { return (0, 0, 0); diff --git a/packages/protocol/contracts-0.8/governance/test/ValidatorsMock08.sol b/packages/protocol/contracts-0.8/governance/test/ValidatorsMock08.sol index 28857cb69b0..ff9b7c778da 100644 --- a/packages/protocol/contracts-0.8/governance/test/ValidatorsMock08.sol +++ b/packages/protocol/contracts-0.8/governance/test/ValidatorsMock08.sol @@ -1,250 +1,27 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.7 <0.8.20; -import "../../../contracts/governance/interfaces/IValidators.sol"; +import "../Validators.sol"; import "../../../contracts/common/FixidityLib.sol"; -// import "forge-std-8/console2.sol"; - /** * @title A wrapper around Validators that exposes onlyVm functions for testing. */ -contract ValidatorsMock08 is IValidators { - function updateValidatorScoreFromSigner(address signer, uint256 uptime) external { - // console2.log("### update Validator Score From Signer"); - } +contract ValidatorsMock08 is Validators(true) { + function updateValidatorScoreFromSigner(address signer, uint256 uptime) external override {} function distributeEpochPaymentsFromSigner( address signer, uint256 maxPayment - ) external returns (uint256) { - // console2.log("### distributeEpochPaymentsFromSigner"); - return 0; - // return _distributeEpochPaymentsFromSigner(signer, maxPayment); - } - - function registerValidator( - bytes calldata ecdsaPublicKey, - bytes calldata blsPublicKey, - bytes calldata blsPop - ) external returns (bool) { - return true; - } - - function registerValidator(bytes calldata ecdsaPublicKey) external returns (bool) { - return true; - } - - function deregisterValidator(uint256 index) external returns (bool) { - return true; - } - function affiliate(address group) external returns (bool) { - return true; - } - function deaffiliate() external returns (bool) { - return true; - } - function updateBlsPublicKey( - bytes calldata blsPublicKey, - bytes calldata blsPop - ) external returns (bool) { - return true; - } - function registerValidatorGroup(uint256 commission) external returns (bool) { - return true; - } - function deregisterValidatorGroup(uint256 index) external returns (bool) { - return true; - } - function addMember(address validator) external returns (bool) { - return true; - } - function addFirstMember( - address validator, - address lesser, - address greater - ) external returns (bool) { - return true; - } - function removeMember(address validator) external returns (bool) { - return true; - } - function reorderMember( - address validator, - address lesserMember, - address greaterMember - ) external returns (bool) { - return true; - } - function updateCommission() external {} - function setNextCommissionUpdate(uint256 commission) external {} - function resetSlashingMultiplier() external {} - - // only owner - function setCommissionUpdateDelay(uint256 delay) external {} - function setMaxGroupSize(uint256 size) external returns (bool) { - return true; - } - function setMembershipHistoryLength(uint256 length) external returns (bool) { - return true; - } - function setValidatorScoreParameters( - uint256 exponent, - uint256 adjustmentSpeed - ) external returns (bool) { - return true; - } - function setGroupLockedGoldRequirements(uint256 value, uint256 duration) external returns (bool) { - return true; - } - function setValidatorLockedGoldRequirements( - uint256 value, - uint256 duration - ) external returns (bool) { - return true; - } - function setSlashingMultiplierResetPeriod(uint256 value) external {} - function setDowntimeGracePeriod(uint256 value) external {} - - // only registered contract - function updateEcdsaPublicKey( - address account, - address signer, - bytes calldata ecdsaPublicKey - ) external returns (bool) { - return true; - } - function updatePublicKeys( - address account, - address signer, - bytes calldata ecdsaPublicKey, - bytes calldata blsPublicKey, - bytes calldata blsPop - ) external returns (bool) { - return true; - } - - // only slasher - function forceDeaffiliateIfValidator(address validatorAccount) external {} - function halveSlashingMultiplier(address account) external {} - - // view functions - function maxGroupSize() external view returns (uint256) { - return 0; - } - function downtimeGracePeriod() external view returns (uint256) { - return 0; - } - function getCommissionUpdateDelay() external view returns (uint256) { + ) external override returns (uint256) { return 0; } - function getValidatorScoreParameters() external view returns (uint256, uint256) { - return (0, 0); - } - function getMembershipHistory( - address account - ) external view returns (uint256[] memory, address[] memory, uint256, uint256) { - return (new uint256[](0), new address[](0), 0, 0); - } - function calculateEpochScore(uint256 uptime) external view returns (uint256) { - return 0; - } - - function calculateGroupEpochScore(uint256[] calldata uptimes) external view returns (uint256) { - return 0; - } - - function getAccountLockedGoldRequirement(address account) external view returns (uint256) { - return 0; - } - - function meetsAccountLockedGoldRequirements(address account) external view returns (bool) { - return true; - } - function getValidatorBlsPublicKeyFromSigner(address singer) external view returns (bytes memory) { - return "0x"; - } - function getValidator( - address account - ) external view returns (bytes memory, bytes memory, address, uint256, address) { - return ("0x", "0x", address(0), 0, address(0)); - } - function getValidatorsGroup(address account) external view returns (address affiliation) { - affiliation = address(0); - } - function getValidatorGroup( - address account - ) - external - view - returns (address[] memory, uint256, uint256, uint256, uint256[] memory, uint256, uint256) - { - return (new address[](0), 0, 0, 0, new uint256[](0), 0, 0); - } - function getGroupNumMembers(address account) external view returns (uint256) { - return 0; - } - - function getTopGroupValidators( - address account, - uint256 n - ) external view returns (address[] memory) { - return new address[](0); - } - function getGroupsNumMembers( - address[] calldata accounts - ) external view returns (uint256[] memory) { - return new uint256[](0); - } - function getNumRegisteredValidators() external view returns (uint256) { - return 0; - } - - function groupMembershipInEpoch( - address account, - uint256 epochNumber, - uint256 index - ) external view returns (address) { - return address(0); - } - - function getValidatorLockedGoldRequirements() external view returns (uint256, uint256) { - return (0, 0); - } - function getGroupLockedGoldRequirements() external view returns (uint256, uint256) { - return (0, 0); - } - function getRegisteredValidators() external view returns (address[] memory) { - return new address[](0); - } - function getRegisteredValidatorGroups() external view returns (address[] memory) { - return new address[](0); - } - function isValidatorGroup(address account) external view returns (bool) { - return true; - } - function isValidator(address account) external view returns (bool) { - return true; - } - function getValidatorGroupSlashingMultiplier(address account) external view returns (uint256) { - return 0; - } - - function getMembershipInLastEpoch(address account) external view returns (address) { - return address(0); - } - function getMembershipInLastEpochFromSigner(address signer) external view returns (address) { - return address(0); - } function computeEpochReward( address account, uint256 score, uint256 maxPayment - ) external view returns (uint256) { + ) external view override returns (uint256) { return 1; } - function getMembershipHistoryLength() external view returns (uint256) { - return 0; - } } 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/common/interfaces/IEpochManager.sol b/packages/protocol/contracts/common/interfaces/IEpochManager.sol index 964187f9f53..e70bb033b87 100644 --- a/packages/protocol/contracts/common/interfaces/IEpochManager.sol +++ b/packages/protocol/contracts/common/interfaces/IEpochManager.sol @@ -5,7 +5,6 @@ interface IEpochManager { function initializeSystem( uint256 firstEpochNumber, uint256 firstEpochBlock, - // uint256 firstEpochTimestamp, // TODO: do we need END timestamp? address[] calldata firstElected ) external; function startNextEpochProcess() external; @@ -14,9 +13,9 @@ interface IEpochManager { address[] calldata lessers, address[] calldata greaters ) external; - function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256, uint256); + function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256); function getCurrentEpochNumber() external view returns (uint256); function getElected() external view returns (address[] memory); - function epochManagerInitializer() external view returns (address); + function epochManagerEnabler() external view returns (address); function epochDuration() 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..c245dac581a --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerEnabler { + function initEpochManager() external; + function getEpochNumber() external returns (uint256); + 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/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index d6b928e6e25..86836975774 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -153,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. @@ -340,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); } diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index e04ac3c68c7..774da4f90d9 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -85,12 +85,11 @@ contract EpochRewards is event TargetVotingYieldUpdated(uint256 fraction); - modifier onlyVmOrEpochManager() { - require( - msg.sender == address(0) || - msg.sender == registry.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID), - "Only VM or Epoch Manager can call" - ); + 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"); + } _; } @@ -153,7 +152,11 @@ contract EpochRewards is * voting Gold fraction. * @dev Only called directly by the protocol. */ - function updateTargetVotingYield() external onlyVmOrEpochManager onlyWhenNotFrozen { + function updateTargetVotingYield() + external + onlyVmOrPermitted(registry.getAddressFor(EPOCH_MANAGER_REGISTRY_ID)) + onlyWhenNotFrozen + { _updateTargetVotingYield(); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 01e16821776..e7b2db4953a 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -41,6 +41,7 @@ interface IValidators { bytes calldata, bytes calldata ) external returns (bool); + function mintStableToEpochManager(uint256 amount) external; // only VM function updateValidatorScoreFromSigner(address, uint256) external; diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index 5b565848913..df9e5f79a6d 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', diff --git a/packages/protocol/migrations_sol/Migration.s.sol b/packages/protocol/migrations_sol/Migration.s.sol index 3fac354a1b8..802ce943500 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"; @@ -50,9 +51,14 @@ import "@celo-contracts/stability/interfaces/ISortedOracles.sol"; import "@celo-contracts-8/common/interfaces/IFeeCurrencyDirectoryInitializer.sol"; import "@celo-contracts-8/common/interfaces/IGasPriceMinimumInitializer.sol"; import "@celo-contracts-8/common/interfaces/ICeloUnreleasedTreasureInitializer.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); @@ -234,6 +240,9 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { migrateFeeHandler(json); migrateOdisPayments(); migrateCeloUnreleasedTreasure(); + migrateEpochManagerEnabler(); + migrateEpochManager(json); + migrateScoreManager(); migrateGovernance(json); vm.stopBroadcast(); @@ -241,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(); } /** @@ -930,6 +945,42 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { ); } + 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 carbonOffsettingPartner = abi.decode( + json.parseRaw(".epochManager.carbonOffsettingPartner"), + (address) + ); + address newEpochDuration = abi.decode( + json.parseRaw(".epochManager.newEpochDuration"), + (address) + ); + + deployProxiedContract( + "EpochManager", + abi.encodeWithSelector( + IEpochManagerInitializer.initialize.selector, + REGISTRY_ADDRESS, + newEpochDuration, + carbonOffsettingPartner, + registry.getAddressForString("EpochManagerEnabler") + ) + ); + } + function migrateGovernance(string memory json) public { bool useApprover = abi.decode(json.parseRaw(".governanceApproverMultiSig.required"), (bool)); @@ -1060,14 +1111,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 @@ -1082,6 +1131,7 @@ 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"); @@ -1091,20 +1141,12 @@ contract Migration is Script, UsingRegistry, MigrationsConstants { } 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( @@ -1123,6 +1165,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: "); @@ -1137,7 +1230,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) { @@ -1150,52 +1242,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/constants.sol b/packages/protocol/migrations_sol/constants.sol index cf4ff6c0526..0c9cf6c7f20 100644 --- a/packages/protocol/migrations_sol/constants.sol +++ b/packages/protocol/migrations_sol/constants.sol @@ -7,7 +7,7 @@ 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", "CeloUnreleasedTreasure", @@ -16,6 +16,8 @@ contract MigrationsConstants is TestConstants { "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..1836e8f1995 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,11 +90,19 @@ "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, "maxElectableValidators": 100, @@ -89,14 +111,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 +128,14 @@ "maxValidatorEpochPayment": 205479452054794520547, "maxValidatorEpochPayment_helper": "(75,000 / 365) * 10 ^ 18", "communityRewardFraction": 250000000000000000000000, - "carbonOffsettingPartner": "0x0000000000000000000000000000000000000000", + "carbonOffsettingPartner": "0xD533Ca259b330c7A88f74E000a3FaEa2d63B7972", "carbonOffsettingFraction": 1000000000000000000000, "frozen": false }, + "epochManager": { + "newEpochDuration": 86400, + "carbonOffsettingPartner": "0xD533Ca259b330c7A88f74E000a3FaEa2d63B7972" + }, "random": { "randomnessBlockRetentionWindow": "720", "randomnessBlockRetentionWindow_helper": "HOUR / 5" @@ -147,7 +173,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 +188,4 @@ "skipSetConstitution": false, "skipTransferOwnership": false } -} \ No newline at end of file +} diff --git a/packages/protocol/scripts/consts.ts b/packages/protocol/scripts/consts.ts index 729b0dfb276..d0edb652342 100644 --- a/packages/protocol/scripts/consts.ts +++ b/packages/protocol/scripts/consts.ts @@ -58,6 +58,7 @@ export const CoreContracts = [ 'Election', 'EpochRewards', 'EpochManager', + 'EpochManagerEnabler', 'Governance', 'GovernanceApproverMultiSig', 'BlockchainParameters', 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/test-sol/constants.sol b/packages/protocol/test-sol/constants.sol index f0d93e336f9..92f8422412a 100644 --- a/packages/protocol/test-sol/constants.sol +++ b/packages/protocol/test-sol/constants.sol @@ -25,7 +25,6 @@ contract TestConstants { string constant GovernanceContract = "Governance"; string constant EpochRewardsContract = "EpochRewards"; string constant EpochManagerContract = "EpochManager"; - string constant EpochManagerInitializerContract = "EpochManagerInitializer"; string constant ScoreManagerContract = "ScoreManager"; string constant ReserveContract = "Reserve"; string constant CeloUnreleasedTreasureContract = "CeloUnreleasedTreasure"; diff --git a/packages/protocol/test-sol/devchain/migration/Migration.t.sol b/packages/protocol/test-sol/devchain/migration/Migration.t.sol index c5c141a1114..463656c4376 100644 --- a/packages/protocol/test-sol/devchain/migration/Migration.t.sol +++ b/packages/protocol/test-sol/devchain/migration/Migration.t.sol @@ -1,19 +1,30 @@ // 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 "@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/IScoreManager.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; +import "@celo-contracts/governance/interfaces/IElection.sol"; -contract IntegrationTest is Test, TestConstants { +import "@celo-contracts-8/common/interfaces/IPrecompiles.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. @@ -70,6 +81,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 @@ -89,7 +101,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); @@ -123,3 +136,163 @@ contract RegistryIntegrationTest is IntegrationTest, MigrationsConstants { } } } + +contract EpochManagerIntegrationTest is IntegrationTest, MigrationsConstants { + ICeloToken celoToken; + IAccounts accountsContract; + IValidators validatorsContract; + IElection electionContract; + IScoreManager scoreManagerContract; + IEpochManager epochManager; + IEpochManagerEnabler epochManagerEnabler; + + address reserveAddress; + address unreleasedTreasury; + address randomAddress; + + uint256 firstEpochNumber = 100; + uint256 firstEpochBlock = 100; + address[] firstElected; + address[] validatorsList; + + address[] groups; + + uint256[] groupScore = [5e23, 7e23, 1e24]; + uint256[] validatorScore = [1e23, 1e23, 1e23, 1e23, 1e23, 1e23]; + + function setUp() public override { + super.setUp(); + randomAddress = actor("randomAddress"); + + validatorsContract = IValidators(registry.getAddressForStringOrDie("Validators")); + electionContract = IElection(registry.getAddressForStringOrDie("Election")); + scoreManagerContract = IScoreManager(registry.getAddressForStringOrDie("ScoreManager")); + unreleasedTreasury = registry.getAddressForStringOrDie("CeloUnreleasedTreasure"); + reserveAddress = registry.getAddressForStringOrDie("Reserve"); + + validatorsList = validatorsContract.getRegisteredValidators(); + groups = 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 (electionContract.getPendingVotesForGroup(validatorGroup) == 0) { + continue; + } + vm.startPrank(validatorGroup); + electionContract.activate(validatorGroup); + vm.stopPrank(); + } + } + + function test_IsSetupCorrect() public { + assertEq( + registry.getAddressForStringOrDie("EpochManagerEnabler"), + epochManager.epochManagerEnabler() + ); + assertEq( + registry.getAddressForStringOrDie("EpochManagerEnabler"), + address(epochManagerEnabler) + ); + assertEq(address(epochManagerEnabler), epochManager.epochManagerEnabler()); + } + + 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(0)); + 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 = scoreManagerContract.owner(); + vm.startPrank(scoreManagerOwner); + scoreManagerContract.setGroupScore(groups[0], groupScore[0]); + scoreManagerContract.setGroupScore(groups[1], groupScore[1]); + scoreManagerContract.setGroupScore(groups[2], groupScore[2]); + + scoreManagerContract.setValidatorScore(validatorsList[0], validatorScore[0]); + scoreManagerContract.setValidatorScore(validatorsList[1], validatorScore[1]); + scoreManagerContract.setValidatorScore(validatorsList[2], validatorScore[2]); + scoreManagerContract.setValidatorScore(validatorsList[3], validatorScore[3]); + scoreManagerContract.setValidatorScore(validatorsList[4], validatorScore[4]); + scoreManagerContract.setValidatorScore(validatorsList[5], validatorScore[5]); + + vm.stopPrank(); + } +} 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/CeloUnreleasedTreasure.t.sol b/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasure.t.sol index ed11f093866..953b7d7a113 100644 --- a/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasure.t.sol +++ b/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasure.t.sol @@ -5,7 +5,7 @@ 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/common/interfaces/ICeloToken.sol"; import "@celo-contracts/governance/interfaces/IGovernance.sol"; import { CeloUnreleasedTreasure } from "@celo-contracts-8/common/CeloUnreleasedTreasure.sol"; import "@celo-contracts-8/common/IsL2Check.sol"; @@ -25,6 +25,7 @@ contract CeloUnreleasedTreasureTest is Test, TestConstants, IsL2Check { address owner = address(this); address celoTokenAddress = actor("celoTokenAddress"); + address epochManagerAddress = actor("epochManagerAddress"); address celoDistributionOwner = actor("celoDistributionOwner"); address communityRewardFund = actor("communityRewardFund"); @@ -63,20 +64,20 @@ contract CeloUnreleasedTreasureTest is Test, TestConstants, IsL2Check { 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)); + registry.setAddressFor("EpochManager", address(epochManagerAddress)); vm.deal(address(0), CELO_SUPPLY_CAP); - assertEq(celoToken.totalSupply(), 0, "starting total supply not zero."); + 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.totalSupply(), L1_MINTED_CELO_SUPPLY, "total supply incorrect."); + assertEq(celoToken.allocatedSupply(), L1_MINTED_CELO_SUPPLY, "total supply incorrect."); } function newCeloUnreleasedTreasure() internal returns (CeloUnreleasedTreasure) { @@ -132,3 +133,26 @@ contract CeloUnreleasedTreasureTest_initialize is CeloUnreleasedTreasureTest { payableAddress.transfer(1 ether); } } + +contract CeloUnreleasedTreasureTest_release is CeloUnreleasedTreasureTest { + function setUp() public override { + super.setUp(); + newCeloUnreleasedTreasure(); + } + + function test_ShouldTransferToRecepientAddress() public { + uint256 _balanceBefore = randomAddress.balance; + vm.prank(epochManagerAddress); + + celoUnreleasedTreasure.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."); + celoUnreleasedTreasure.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 index 62c8cf0cb08..931ef839d0c 100644 --- a/packages/protocol/test-sol/unit/common/EpochManager.t.sol +++ b/packages/protocol/test-sol/unit/common/EpochManager.t.sol @@ -4,7 +4,8 @@ pragma solidity >=0.8.7 <0.8.20; import "celo-foundry-8/Test.sol"; import "@celo-contracts-8/common/EpochManager.sol"; import "@celo-contracts-8/stability/test/MockStableToken.sol"; -import "@celo-contracts-8/common/interfaces/ICeloToken.sol"; +import "@celo-contracts-8/common/test/MockCeloToken.sol"; +import "@celo-contracts/common/interfaces/ICeloToken.sol"; import "@celo-contracts-8/common/ScoreManager.sol"; import { CeloUnreleasedTreasure } from "@celo-contracts-8/common/CeloUnreleasedTreasure.sol"; import { ICeloUnreleasedTreasure } from "@celo-contracts/common/interfaces/ICeloUnreleasedTreasure.sol"; @@ -18,6 +19,7 @@ import "@celo-contracts/common/interfaces/IRegistry.sol"; import { EpochRewardsMock08 } from "@celo-contracts-8/governance/test/EpochRewardsMock.sol"; import { ValidatorsMock08 } from "@celo-contracts-8/governance/test/ValidatorsMock08.sol"; +import { MockCeloUnreleasedTreasure } from "@celo-contracts-8/common/test/MockCeloUnreleasedTreasure.sol"; contract EpochManagerTest is Test, TestConstants, Utils08 { EpochManager epochManager; @@ -27,47 +29,52 @@ contract EpochManagerTest is Test, TestConstants, Utils08 { EpochRewardsMock08 epochRewards; ValidatorsMock08 validators; - address epochManagerInitializer; + address epochManagerEnabler; address carbonOffsettingPartner; address communityRewardFund; + address reserveAddress; + address scoreManagerAddress; uint256 firstEpochNumber = 100; uint256 firstEpochBlock = 100; + uint256 epochDuration = DAY; address[] firstElected; IRegistry registry; - ICeloToken celoToken; - CeloUnreleasedTreasure celoUnreleasedTreasure; + MockCeloToken08 celoToken; + MockCeloUnreleasedTreasure celoUnreleasedTreasure; ScoreManager scoreManager; uint256 celoAmountForRate = 1e24; uint256 stableAmountForRate = 2 * celoAmountForRate; + event EpochProcessingStarted(uint256 indexed epochNumber); + function setUp() public virtual { epochManager = new EpochManager(true); sortedOracles = new MockSortedOracles(); epochRewards = new EpochRewardsMock08(); validators = new ValidatorsMock08(); stableToken = new MockStableToken08(); - celoUnreleasedTreasure = new CeloUnreleasedTreasure(false); + celoToken = new MockCeloToken08(); + celoUnreleasedTreasure = new MockCeloUnreleasedTreasure(); firstElected.push(actor("validator1")); firstElected.push(actor("validator2")); - address celoTokenAddress = actor("celoTokenAddress"); - address scoreManagerAddress = actor("scoreManagerAddress"); - address reserveAddress = actor("reserve"); + scoreManagerAddress = actor("scoreManagerAddress"); + + reserveAddress = actor("reserve"); - epochManagerInitializer = actor("initializer"); + epochManagerEnabler = actor("epochManagerEnabler"); carbonOffsettingPartner = actor("carbonOffsettingPartner"); communityRewardFund = actor("communityRewardFund"); - deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); - deployCodeTo("GoldToken.sol", abi.encode(false), celoTokenAddress); + deployCodeTo("MockRegistry.sol", abi.encode(false), REGISTRY_ADDRESS); + deployCodeTo("ScoreManager.sol", abi.encode(false), scoreManagerAddress); registry = IRegistry(REGISTRY_ADDRESS); - celoToken = ICeloToken(celoTokenAddress); scoreManager = ScoreManager(scoreManagerAddress); registry.setAddressFor(EpochManagerContract, address(epochManager)); @@ -81,42 +88,72 @@ contract EpochManagerTest is Test, TestConstants, Utils08 { registry.setAddressFor(CeloTokenContract, address(celoToken)); registry.setAddressFor(ReserveContract, reserveAddress); + celoToken.setTotalSupply(CELO_SUPPLY_CAP); vm.deal(address(celoUnreleasedTreasure), L2_INITIAL_STASH_BALANCE); + celoToken.setBalanceOf(address(celoUnreleasedTreasure), L2_INITIAL_STASH_BALANCE); - bool res1 = sortedOracles.setMedianRate(address(stableToken), stableAmountForRate); - (uint256 res0, uint256 res00) = sortedOracles.medianRate(address(stableToken)); + celoUnreleasedTreasure.setRegistry(REGISTRY_ADDRESS); + validators.setRegistry(REGISTRY_ADDRESS); + + sortedOracles.setMedianRate(address(stableToken), stableAmountForRate); scoreManager.setValidatorScore(actor("validator1"), 1); - uint256 res = scoreManager.getValidatorScore(actor("validator1")); - uint256 res2 = epochRewards.getCommunityRewardFraction(); - epochManager.initialize(REGISTRY_ADDRESS, 10, carbonOffsettingPartner, epochManagerInitializer); + epochManager.initialize( + REGISTRY_ADDRESS, + epochDuration, + carbonOffsettingPartner, + epochManagerEnabler + ); 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.epochDuration(), epochDuration); assertEq(epochManager.carbonOffsettingPartner(), carbonOffsettingPartner); + assertEq(epochManager.epochManagerEnabler(), epochManagerEnabler); } function test_Reverts_WhenAlreadyInitialized() public virtual { vm.expectRevert("contract already initialized"); - epochManager.initialize(REGISTRY_ADDRESS, 10, carbonOffsettingPartner, epochManagerInitializer); + epochManager.initialize(REGISTRY_ADDRESS, 10, carbonOffsettingPartner, epochManagerEnabler); } } contract EpochManagerTest_initializeSystem is EpochManagerTest { function test_processCanBeStarted() public virtual { - vm.prank(epochManagerInitializer); + vm.prank(epochManagerEnabler); epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + ( + uint256 _firstEpochBlock, + uint256 _lastEpochBlock, + uint256 _startTimestamp, + uint256 _currentRewardsBlock + ) = epochManager.getCurrentEpoch(); + assertEq(epochManager.epochManagerEnabler(), address(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(epochManagerInitializer); + vm.prank(epochManagerEnabler); epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); vm.prank(address(0)); vm.expectRevert("Epoch system already initialized"); @@ -136,13 +173,69 @@ contract EpochManagerTest_startNextEpochProcess is EpochManagerTest { } function test_Reverts_WhenEndOfEpochHasNotBeenReached() public { - vm.prank(epochManagerInitializer); + vm.prank(epochManagerEnabler); epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); - uint256 _currentEpoch = epochManager.getCurrentEpochNumber(); - (, , , uint256 _currentEpochEndTimestamp, ) = epochManager.getCurrentEpoch(); - 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); + } } 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 64de3207dfb..58d83c37850 100644 --- a/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol +++ b/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol @@ -758,7 +758,7 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { epochRewards.updateTargetVotingYield(); } } - +// TODO(soloseng): add L2 test case that uses the result from epochManager contract EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAndTheActualRemainingSupplyIs10pMoreThanTheTargetRemainingSupplyAfterRewards_calculateTargetEpochRewards is EpochRewardsTest { 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 460bd56f9f9..7a679044670 100644 --- a/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol +++ b/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol @@ -3430,6 +3430,26 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { } } +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_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(); 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 c4ba6faaf15..e055288f0b0 100644 --- a/packages/protocol/test-sol/unit/governance/voting/Election.t.sol +++ b/packages/protocol/test-sol/unit/governance/voting/Election.t.sol @@ -7,6 +7,7 @@ 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"; @@ -162,6 +163,7 @@ contract ElectionTest is Utils, TestConstants { } function _whenL2() public { + blockTravel(ph.epochSize() + 1); uint256 l1EpochNumber = election.getEpochNumber(); address[] memory _elected = new address[](2); @@ -217,7 +219,6 @@ contract ElectionTest_SetElectabilityThreshold is ElectionTest { } } -// TODO(soloseng): need to update epochNumber for L2, to make it !=0 contract ElectionTest_SetElectabilityThreshold_L2 is ElectionTest { function test_shouldSetElectabilityThreshold() public { _whenL2(); 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/utils08.sol b/packages/protocol/test-sol/utils08.sol index 681380e6e87..1ee9330ad50 100644 --- a/packages/protocol/test-sol/utils08.sol +++ b/packages/protocol/test-sol/utils08.sol @@ -11,8 +11,19 @@ contract Utils08 { 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))); + } }