diff --git a/contracts/MaticX.sol b/contracts/MaticX.sol index 9561a45a..bd85a69a 100644 --- a/contracts/MaticX.sol +++ b/contracts/MaticX.sol @@ -386,17 +386,34 @@ contract MaticX is function stakeRewardsAndDistributeFees( uint256 _validatorId ) external override nonReentrant whenNotPaused onlyRole(BOT) { + _stakeRewardsAndDistributeFees(_validatorId, true); + } + + /// @notice Stake 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); + } + + function _stakeRewardsAndDistributeFees( + uint256 _validatorId, + bool _pol + ) private { require( validatorRegistry.validatorIdExists(_validatorId), "Doesn't exist in validator registry" ); - uint256 reward = polToken.balanceOf(address(this)); + IERC20Upgradeable token = _pol ? polToken : maticToken; + uint256 reward = token.balanceOf(address(this)); require(reward > 0, "Reward is zero"); uint256 treasuryFee = (reward * feePercent) / 100; if (treasuryFee > 0) { - polToken.safeTransfer(treasury, treasuryFee); + token.safeTransfer(treasury, treasuryFee); emit DistributeFees(treasury, treasuryFee); } @@ -405,7 +422,9 @@ contract MaticX is stakeManager.getValidatorContract(_validatorId) ); - validatorShare.buyVoucherPOL(amountToStake, 0); + _pol + ? validatorShare.buyVoucherPOL(amountToStake, 0) + : validatorShare.buyVoucher(amountToStake, 0); uint256 totalShares = totalSupply(); uint256 totalPooledAmount = getTotalStakeAcrossAllValidators(); diff --git a/contracts/interfaces/IMaticX.sol b/contracts/interfaces/IMaticX.sol index ff0e14ae..c1afc0f2 100644 --- a/contracts/interfaces/IMaticX.sol +++ b/contracts/interfaces/IMaticX.sol @@ -138,6 +138,11 @@ interface IMaticX is IERC20Upgradeable { /// @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. + /// @custom:deprecated + /// @param _validatorId - Validator id to stake Matic rewards + function stakeRewardsAndDistributeFeesMatic(uint256 _validatorId) external; + /// @notice Migrate all POL tokens to another validator. /// @param _fromValidatorId - Validator id to migrate POL tokens from /// @param _toValidatorId - Validator id to migrate POL tokens to diff --git a/test/MaticX.spec.ts b/test/MaticX.spec.ts index ac4df725..3ef0494b 100644 --- a/test/MaticX.spec.ts +++ b/test/MaticX.spec.ts @@ -1932,7 +1932,7 @@ describe("MaticX", function () { }); }); - describe("Stake rewards and distribute fees", function () { + describe("Stake rewards and distribute fees for POL", function () { describe("Negative", function () { it("Should revert with the right error if paused", async function () { const { maticX, manager, bot, preferredDepositValidatorId } = @@ -2068,6 +2068,158 @@ describe("MaticX", function () { }); }); + describe("Stake rewards and distribute fees for Matic", function () { + describe("Negative", function () { + it("Should revert with the right error if paused", async function () { + const { maticX, manager, bot, preferredDepositValidatorId } = + await loadFixture(deployFixture); + + await maticX.connect(manager).togglePause(); + + const promise = maticX + .connect(bot) + .stakeRewardsAndDistributeFeesMatic( + preferredDepositValidatorId + ); + await expect(promise).to.be.revertedWith("Pausable: paused"); + }); + + it("Should revert with the right error if called by a non bot", async function () { + const { + maticX, + executor, + botRole, + preferredDepositValidatorId, + } = await loadFixture(deployFixture); + + const promise = maticX + .connect(executor) + .stakeRewardsAndDistributeFeesMatic( + preferredDepositValidatorId + ); + await expect(promise).to.be.revertedWith( + `AccessControl: account ${executor.address.toLowerCase()} is missing role ${botRole}` + ); + }); + + it("Should revert with the right error if having an unregistered validator id", async function () { + const { maticX, bot } = await loadFixture(deployFixture); + + const promise = maticX + .connect(bot) + .stakeRewardsAndDistributeFeesMatic(0); + await expect(promise).to.be.revertedWith( + "Doesn't exist in validator registry" + ); + }); + + it("Should revert with the right error if having the zero reward", async function () { + const { maticX, bot, preferredWithdrawalValidatorId } = + await loadFixture(deployFixture); + + const promise = maticX + .connect(bot) + .stakeRewardsAndDistributeFeesMatic( + preferredWithdrawalValidatorId + ); + await expect(promise).to.be.revertedWith("Reward is zero"); + }); + }); + + describe("Positive", function () { + it("Should emit the StakeRewards and DistributeFees events if having a positive fee amount", async function () { + const { + maticX, + matic, + polygonTreasury, + bot, + preferredDepositValidatorId, + } = await loadFixture(deployFixture); + + await matic + .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(bot) + .stakeRewardsAndDistributeFeesMatic( + preferredDepositValidatorId + ); + await expect(promise) + .to.emit(maticX, "StakeRewards") + .withArgs(preferredDepositValidatorId, netStakeAmount) + .and.to.emit(maticX, "DistributeFees") + .withArgs(treasuryAddress, feeAmount); + }); + + it("Should emit the StakeRewards if having the zero fee amount", async function () { + const { + maticX, + matic, + polygonTreasury, + bot, + preferredDepositValidatorId, + } = await loadFixture(deployFixture); + + const stakeAmount = 19; + await matic + .connect(polygonTreasury) + .transfer(maticX.address, stakeAmount); + + const promise = maticX + .connect(bot) + .stakeRewardsAndDistributeFeesMatic( + preferredDepositValidatorId + ); + await expect(promise) + .to.emit(maticX, "StakeRewards") + .withArgs(preferredDepositValidatorId, stakeAmount) + .and.not.to.emit(maticX, "DistributeFees"); + }); + + it("Should return the right Matic balances", async function () { + const { + maticX, + stakeManager, + pol, + matic, + polygonTreasury, + bot, + preferredDepositValidatorId, + } = await loadFixture(deployFixture); + + await matic + .connect(polygonTreasury) + .transfer(maticX.address, stakeAmount); + + const treasuryAddress = await maticX.treasury(); + + const feeAmount = stakeAmount.mul(5).div(100); + const netStakeAmount = stakeAmount.sub(feeAmount); + + const promise = maticX + .connect(bot) + .stakeRewardsAndDistributeFeesMatic( + preferredDepositValidatorId + ); + await expect(promise).to.changeTokenBalances( + matic, + [maticX, treasuryAddress], + [stakeAmount.mul(-1), feeAmount] + ); + await expect(promise).to.changeTokenBalance( + pol, + stakeManager, + netStakeAmount + ); + }); + }); + }); + describe("Migrate a delegation", function () { describe("Negative", function () { it("Should revert with the right error if paused", async function () {