From df84c5d727ac186d9d09b6c57e49640cd64b963c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Volpe?= Date: Fri, 20 Sep 2024 15:02:02 +0200 Subject: [PATCH] Make captureEpochAndValidators work in constant time (#11210) --- .../contracts-0.8/common/EpochManager.sol | 120 +++++++++--------- .../common/EpochManagerEnabler.sol | 37 +++--- .../common/mocks/MockAccounts.sol | 25 ++-- .../interfaces/IEpochManagerEnabler.sol | 1 - .../precompiles/EpochSizePrecompile.sol | 14 +- .../unit/common/EpochManagerEnabler.t.sol | 50 ++++++++ 6 files changed, 156 insertions(+), 91 deletions(-) create mode 100644 packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol diff --git a/packages/protocol/contracts-0.8/common/EpochManager.sol b/packages/protocol/contracts-0.8/common/EpochManager.sol index f5a0f906179..f06273a273c 100644 --- a/packages/protocol/contracts-0.8/common/EpochManager.sol +++ b/packages/protocol/contracts-0.8/common/EpochManager.sol @@ -12,13 +12,15 @@ 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 + ICeloVersionedContract, + IEpochManagerInitializer { using FixidityLib for FixidityLib.Fraction; @@ -245,6 +247,60 @@ contract EpochManager is epochProcessing.status = EpochProcessStatus.NotStarted; } + /** + * @notice Sends the allocated epoch payment to a validator, their group, and + * delegation beneficiary. + * @param validator Account of the validator. + */ + function sendValidatorPayment(address validator) external { + IAccounts accounts = IAccounts(getAccounts()); + address signer = accounts.getValidatorSigner(validator); + + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorPendingPayments[signer] + ); + validatorPendingPayments[signer] = 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; + + IStableToken stableToken = IStableToken(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 + ); + } + /// returns the current epoch Info function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256) { Epoch storage _epoch = epochs[currentEpochNumber]; @@ -272,6 +328,10 @@ contract EpochManager is ); } + function isBlocked() external view returns (bool) { + return isOnEpochProcess(); + } + function getElected() external view returns (address[] memory) { return elected; } @@ -288,10 +348,6 @@ contract EpochManager is return epochs[epoch].lastBlock; } - function isBlocked() external view returns (bool) { - return isOnEpochProcess(); - } - /** * @notice Returns the storage, major, minor, and patch version of the contract. * @return Storage version of the contract. @@ -357,58 +413,4 @@ contract EpochManager is CELOequivalent ); } - - /** - * @notice Sends the allocated epoch payment to a validator, their group, and - * delegation beneficiary. - * @param validator Account of the validator. - */ - function sendValidatorPayment(address validator) external { - IAccounts accounts = IAccounts(getAccounts()); - address signer = accounts.getValidatorSigner(validator); - - FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( - validatorPendingPayments[signer] - ); - validatorPendingPayments[signer] = 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; - - IStableToken stableToken = IStableToken(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 - ); - } } diff --git a/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol index d3bd71e0c6e..2eafa24f026 100644 --- a/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol +++ b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol @@ -6,14 +6,25 @@ 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"; +import "../../contracts/common/interfaces/IEpochManagerEnabler.sol"; +import "./interfaces/IEpochManagerEnablerInitializer.sol"; -contract EpochManagerEnabler is Initializable, UsingPrecompiles, UsingRegistry { +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 @@ -48,10 +59,11 @@ contract EpochManagerEnabler is Initializable, UsingPrecompiles, UsingRegistry { */ function captureEpochAndValidators() external onlyL1 { lastKnownEpochNumber = getEpochNumber(); + emit LastKnownEpochNumberSet(lastKnownEpochNumber); uint256 numberElectedValidators = numberValidatorsInCurrentSet(); lastKnownElectedAccounts = new address[](numberElectedValidators); - lastKnownFirstBlockOfEpoch = _getFirstBlockOfEpoch(lastKnownEpochNumber); + _setFirstBlockOfEpoch(); for (uint256 i = 0; i < numberElectedValidators; i++) { // TODO: document how much gas this takes for 110 signers @@ -60,10 +72,7 @@ contract EpochManagerEnabler is Initializable, UsingPrecompiles, UsingRegistry { ); lastKnownElectedAccounts[i] = validatorAccountAddress; } - } - - function getFirstBlockOfEpoch(uint256 currentEpoch) external view returns (uint256) { - return _getFirstBlockOfEpoch(currentEpoch); + emit LastKnownElectedAccountsSet(); } /** @@ -77,14 +86,10 @@ contract EpochManagerEnabler is Initializable, UsingPrecompiles, UsingRegistry { return (1, 1, 0, 0); } - function _getFirstBlockOfEpoch(uint256 currentEpoch) internal view returns (uint256) { - uint256 blockToCheck = block.number - 1; - uint256 blockEpochNumber = getEpochNumberOfBlock(blockToCheck); - - while (blockEpochNumber == currentEpoch) { - blockToCheck--; - blockEpochNumber = getEpochNumberOfBlock(blockToCheck); - } - return blockToCheck; + 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/mocks/MockAccounts.sol b/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol index e7054bf39e8..a28357f7246 100644 --- a/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol +++ b/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol @@ -16,16 +16,12 @@ contract MockAccounts { mapping(address => PaymentDelegation) delegations; mapping(address => address) accountToSigner; - function setPaymentDelegationFor( - address validator, - address beneficiary, - uint256 fraction - ) public { - delegations[validator] = PaymentDelegation(beneficiary, FixidityLib.wrap(fraction)); + function setValidatorSigner(address account, address signer) external { + accountToSigner[account] = signer; } - function deletePaymentDelegationFor(address validator) public { - delete delegations[validator]; + function getValidatorSigner(address account) external returns (address) { + return accountToSigner[account]; } function getPaymentDelegation(address account) external view returns (address, uint256) { @@ -33,11 +29,14 @@ contract MockAccounts { return (delegation.beneficiary, delegation.fraction.unwrap()); } - function setValidatorSigner(address account, address signer) external { - accountToSigner[account] = signer; + function setPaymentDelegationFor( + address validator, + address beneficiary, + uint256 fraction + ) public { + delegations[validator] = PaymentDelegation(beneficiary, FixidityLib.wrap(fraction)); } - - function getValidatorSigner(address account) external returns (address) { - return accountToSigner[account]; + function deletePaymentDelegationFor(address validator) public { + delete delegations[validator]; } } diff --git a/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol b/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol index c245dac581a..8cf8ce591bd 100644 --- a/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol +++ b/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol @@ -3,6 +3,5 @@ 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/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/unit/common/EpochManagerEnabler.t.sol b/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol new file mode 100644 index 00000000000..b4273fb6c09 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts-8/common/EpochManagerEnabler.sol"; +import "@celo-contracts/stability/test/MockSortedOracles.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; + +import { EPOCH_SIZEPRE_COMPILE_ADDRESS, EpochSizePrecompile } from "@test-sol/precompiles/EpochSizePrecompile.sol"; + +contract EpochManagerEnablerMock is EpochManagerEnabler { + constructor(bool test) public EpochManagerEnabler(test) {} + + function setFirstBlockOfEpoch() external { + return _setFirstBlockOfEpoch(); + } +} + +contract EpochManagerEnablerTest is Test { + EpochManagerEnablerMock epochManagerEnabler; + uint256 EPOCH_SIZE_NEW = 17280; + + function setUp() public virtual { + deployCodeTo("EpochSizePrecompile", EPOCH_SIZEPRE_COMPILE_ADDRESS); + address payable payableAddress = payable(EPOCH_SIZEPRE_COMPILE_ADDRESS); + + EpochSizePrecompile(payableAddress).setEpochSize(EPOCH_SIZE_NEW); + + epochManagerEnabler = new EpochManagerEnablerMock(true); + } + + function test_precompilerWorks() public { + // Make sure epoch size is correct + assertEq(epochManagerEnabler.getEpochSize(), EPOCH_SIZE_NEW); + } +} + +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); + } +}