diff --git a/contracts/MaticX.sol b/contracts/MaticX.sol index dce8ab6c..8b99da67 100644 --- a/contracts/MaticX.sol +++ b/contracts/MaticX.sol @@ -381,26 +381,31 @@ contract MaticX is return rewards; } - /// @notice Stake POL rewards and distribute fees to the treasury if any. + /// @notice Stakes POL rewards and distribute fees to the treasury if any. /// @param _validatorId - Validator id to stake POL rewards function stakeRewardsAndDistributeFees( uint256 _validatorId ) external override nonReentrant whenNotPaused onlyRole(BOT) { - _stakeRewardsAndDistributeFees(_validatorId, true); + _stakeRewardsAndDistributeFees(_validatorId, true, true); } - /// @notice Stake Matic rewards and distribute fees to the treasury if any. + /// @notice Stakes Matic rewards and distribute fees to the treasury if any. /// @custom:deprecated /// @param _validatorId - Validator id to stake Matic rewards function stakeRewardsAndDistributeFeesMatic( uint256 _validatorId ) external override nonReentrant whenNotPaused onlyRole(BOT) { - _stakeRewardsAndDistributeFees(_validatorId, false); + _stakeRewardsAndDistributeFees(_validatorId, false, true); } + /// @notice Stakes token rewards and distribute fees to the treasury if any. + /// @param _validatorId - Validator id to stake toke rewards + /// @param _pol - If POL tokens are used for staking and fee distribution + /// @param _revertOnZeroReward - If revert on the zero reward or not function _stakeRewardsAndDistributeFees( uint256 _validatorId, - bool _pol + bool _pol, + bool _revertOnZeroReward ) private { require( validatorRegistry.validatorIdExists(_validatorId), @@ -409,7 +414,12 @@ contract MaticX is IERC20Upgradeable token = _pol ? polToken : maticToken; uint256 reward = token.balanceOf(address(this)); - require(reward > 0, "Reward is zero"); + if (reward == 0) { + if (_revertOnZeroReward) { + revert("Reward is zero"); + } + return; + } uint256 treasuryFee = (reward * feePercent) / 100; if (treasuryFee > 0) { @@ -436,7 +446,7 @@ contract MaticX is emit StakeRewards(_validatorId, amountToStake); } - /// @notice Migrate all POL tokens to another validator. + /// @notice Migrates all POL tokens to another validator. /// @param _fromValidatorId - Validator id to migrate POL tokens from /// @param _toValidatorId - Validator id to migrate POL tokens to /// @param _amount - Amount of POL tokens @@ -468,11 +478,23 @@ contract MaticX is /// @notice Sets a fee percent. /// @param _feePercent - Fee percent (10 = 10%) + // slither-disable-next-line reentrancy-eth function setFeePercent( uint8 _feePercent - ) external override onlyRole(DEFAULT_ADMIN_ROLE) { + ) external override nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) { require(_feePercent <= 100, "Fee percent must not exceed 100"); + uint256[] memory validatorIds = validatorRegistry.getValidators(); + uint256 validatorIdCount = validatorIds.length; + + for (uint256 i = 0; i < validatorIdCount; ) { + _stakeRewardsAndDistributeFees(validatorIds[i], true, false); + + unchecked { + ++i; + } + } + feePercent = _feePercent; emit SetFeePercent(_feePercent); } diff --git a/contracts/interfaces/IMaticX.sol b/contracts/interfaces/IMaticX.sol index 870c0fb9..b7b3d978 100644 --- a/contracts/interfaces/IMaticX.sol +++ b/contracts/interfaces/IMaticX.sol @@ -134,16 +134,16 @@ interface IMaticX is IERC20Upgradeable { uint256[] calldata _validatorIds ) external returns (uint256[] memory); - /// @notice Stake POL rewards and distribute fees to the treasury if any. + /// @notice Stakes POL rewards and distribute fees to the treasury if any. /// @param _validatorId - Validator id to stake POL rewards function stakeRewardsAndDistributeFees(uint256 _validatorId) external; - /// @notice Stake Matic rewards and distribute fees to the treasury if any. + /// @notice Stakes Matic rewards and distribute fees to the treasury if any. /// @custom:deprecated /// @param _validatorId - Validator id to stake Matic rewards function stakeRewardsAndDistributeFeesMatic(uint256 _validatorId) external; - /// @notice Migrate all POL tokens to another validator. + /// @notice Migrates all POL tokens to another validator. /// @param _fromValidatorId - Validator id to migrate POL tokens from /// @param _toValidatorId - Validator id to migrate POL tokens to /// @param _amount - Amount of POL tokens diff --git a/slither.config.json b/slither.config.json index 3703d5c2..25698b16 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,6 +1,6 @@ { "compile_force_framework": "hardhat", - "detectors_to_exclude": "calls-loop,naming-convention,pragma,solc-version,unused-return,unused-state", + "detectors_to_exclude": "calls-loop,incorrect-equality,naming-convention,pragma,solc-version,unused-return,unused-state", "include_paths": "(MaticX.sol|ValidatorRegistry.sol)", "filter_paths": "(@openzeppelin/|lib/|mocks/|state-transfer/|tunnel/)" } diff --git a/test/MaticX.spec.ts b/test/MaticX.spec.ts index 55fc6c45..a10f41fa 100644 --- a/test/MaticX.spec.ts +++ b/test/MaticX.spec.ts @@ -31,6 +31,7 @@ describe("MaticX", function () { const stakeAmount = ethers.utils.parseUnits("100", 18); const tripleStakeAmount = stakeAmount.mul(3); const version = "2"; + const feePercent = 10; async function deployFixture(callMaticXInitializeV2 = true) { await reset(providerUrl, envVars.FORKING_BLOCK_NUMBER); @@ -2460,16 +2461,48 @@ describe("MaticX", function () { }); describe("Positive", function () { - it("Should emit the SetFeePercent event", async function () { + it("Should emit the SetFeePercent event if having zero rewards", async function () { const { maticX, manager } = await loadFixture(deployFixture); - const feePercent = 100; const promise = maticX .connect(manager) .setFeePercent(feePercent); await expect(promise) .to.emit(maticX, "SetFeePercent") .withArgs(feePercent); + + await expect(promise) + .not.to.emit(maticX, "StakeRewards") + .and.not.to.emit(maticX, "DistributeFees"); + }); + + it("Should emit the SetFeePercent, StakeRewards and DistributeFees events if having non zero rewards", async function () { + const { + maticX, + pol, + polygonTreasury, + manager, + preferredDepositValidatorId, + } = await loadFixture(deployFixture); + + await pol + .connect(polygonTreasury) + .transfer(maticX.address, stakeAmount); + + const feeAmount = stakeAmount.mul(5).div(100); + const netStakeAmount = stakeAmount.sub(feeAmount); + const treasuryAddress = await maticX.treasury(); + + const promise = maticX + .connect(manager) + .setFeePercent(feePercent); + await expect(promise).to.emit(maticX, "SetFeePercent"); + + await expect(promise) + .to.emit(maticX, "StakeRewards") + .withArgs(preferredDepositValidatorId, netStakeAmount) + .and.to.emit(maticX, "DistributeFees") + .withArgs(treasuryAddress, feeAmount); }); }); });