diff --git a/packages/zevm-app-contracts/contracts/liquidity-incentives/Synthetixio/StakingRewards.sol b/packages/zevm-app-contracts/contracts/liquidity-incentives/Synthetixio/StakingRewards.sol index 3ef2a800..d2e39461 100644 --- a/packages/zevm-app-contracts/contracts/liquidity-incentives/Synthetixio/StakingRewards.sol +++ b/packages/zevm-app-contracts/contracts/liquidity-incentives/Synthetixio/StakingRewards.sol @@ -105,18 +105,33 @@ contract StakingRewards is RewardsDistributionRecipient, ReentrancyGuard, Pausab emit Withdrawn(msg.sender, amount); } - function getReward() public nonReentrant updateReward(msg.sender) { + function getReward(bool unwrap) public nonReentrant updateReward(msg.sender) { uint256 reward = rewards[msg.sender]; if (reward > 0) { rewards[msg.sender] = 0; - rewardsToken.safeTransfer(msg.sender, reward); + if (unwrap) { + // The 4-byte signature of the function "withdraw(uint256)" + // This is calculated as: bytes4(keccak256("withdraw(uint256)")) + bytes4 functionSignature = 0x2e1a7d4d; + + // Construct the call data + // Here, 'wad' is set to reward + bytes memory data = abi.encodeWithSelector(functionSignature, reward); + + // Make the low-level call + (bool success, ) = address(rewardsToken).call(data); + require(success, "Reward is not a wrapped asset"); + + (success, ) = msg.sender.call{value: reward}(""); + require(success, "Transfer failed"); + } else rewardsToken.safeTransfer(msg.sender, reward); emit RewardPaid(msg.sender, reward); } } - function exit() external { + function exit(bool unwrap) external { withdraw(_balances[msg.sender]); - getReward(); + getReward(unwrap); } /* ========== RESTRICTED FUNCTIONS ========== */ @@ -178,4 +193,7 @@ contract StakingRewards is RewardsDistributionRecipient, ReentrancyGuard, Pausab event RewardPaid(address indexed user, uint256 reward); event RewardsDurationUpdated(uint256 newDuration); event Recovered(address token, uint256 amount); + + // Function to accept Ether + receive() external payable {} } diff --git a/packages/zevm-app-contracts/test/LiquidityIncentives.spec.ts b/packages/zevm-app-contracts/test/LiquidityIncentives.spec.ts index 5c22aa55..743460f8 100644 --- a/packages/zevm-app-contracts/test/LiquidityIncentives.spec.ts +++ b/packages/zevm-app-contracts/test/LiquidityIncentives.spec.ts @@ -227,13 +227,13 @@ describe("LiquidityIncentives tests", () => { zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount2.address); expect(zetaBalance).to.be.eq(0); - await rewardDistributorContract.connect(sampleAccount1).getReward(); + await rewardDistributorContract.connect(sampleAccount1).getReward(false); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount1.address); expect(zetaBalance).to.be.closeTo(REWARDS_AMOUNT.div(2), ERROR_TOLERANCE); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount2.address); expect(zetaBalance).to.be.eq(0); - await rewardDistributorContract.connect(sampleAccount2).getReward(); + await rewardDistributorContract.connect(sampleAccount2).getReward(false); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount1.address); expect(zetaBalance).to.be.closeTo(REWARDS_AMOUNT.div(2), ERROR_TOLERANCE); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount2.address); @@ -281,13 +281,13 @@ describe("LiquidityIncentives tests", () => { zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount2.address); expect(zetaBalance).to.be.eq(0); - await rewardDistributorContract.connect(sampleAccount1).getReward(); + await rewardDistributorContract.connect(sampleAccount1).getReward(false); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount1.address); expect(zetaBalance).to.be.closeTo(REWARDS_AMOUNT.div(4).mul(3), ERROR_TOLERANCE); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount2.address); expect(zetaBalance).to.be.eq(0); - await rewardDistributorContract.connect(sampleAccount2).getReward(); + await rewardDistributorContract.connect(sampleAccount2).getReward(false); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount1.address); expect(zetaBalance).to.be.closeTo(REWARDS_AMOUNT.div(4).mul(3), ERROR_TOLERANCE); zetaBalance = await ZETA_ERC20.balanceOf(sampleAccount2.address); @@ -421,7 +421,7 @@ describe("LiquidityIncentives tests", () => { await network.provider.send("evm_increaseTime", [MIN_STAKING_PERIOD - 2]); await network.provider.send("evm_mine"); - const withdraw = rewardDistributorContract.connect(sampleAccount).exit(); + const withdraw = rewardDistributorContract.connect(sampleAccount).exit(false); await expect(withdraw).to.be.revertedWith("MinimumStakingPeriodNotMet"); }); @@ -444,7 +444,89 @@ describe("LiquidityIncentives tests", () => { await rewardDistributorContract.connect(sampleAccount).beginCoolDown(); - const withdraw = rewardDistributorContract.connect(sampleAccount).exit(); + const withdraw = rewardDistributorContract.connect(sampleAccount).exit(false); await expect(withdraw).to.be.revertedWith("MinimumStakingPeriodNotMet"); }); + + it("Should distribute rewards between two users using native token", async () => { + await ZETA.transfer(rewardDistributorContract.address, REWARDS_AMOUNT); + await rewardDistributorContract.setRewardsDuration(REWARD_DURATION); + await rewardDistributorContract.notifyRewardAmount(REWARDS_AMOUNT); + + const sampleAccount1 = accounts[1]; + const sampleAccount2 = accounts[2]; + const stakedAmount1 = parseEther("100"); + const stakedAmount2 = parseEther("100"); + + await stakeToken(sampleAccount1, stakedAmount1); + await stakeToken(sampleAccount2, stakedAmount2); + + await network.provider.send("evm_increaseTime", [REWARD_DURATION.div(2).toNumber()]); + await network.provider.send("evm_mine"); + + let earned1 = await rewardDistributorContract.earned(sampleAccount1.address); + expect(earned1).to.be.closeTo(REWARDS_AMOUNT.div(4), ERROR_TOLERANCE); + + let earned2 = await rewardDistributorContract.earned(sampleAccount2.address); + expect(earned2).to.be.closeTo(REWARDS_AMOUNT.div(4), ERROR_TOLERANCE); + + await network.provider.send("evm_increaseTime", [REWARD_DURATION.div(2).toNumber()]); + await network.provider.send("evm_mine"); + + earned1 = await rewardDistributorContract.earned(sampleAccount1.address); + expect(earned1).to.be.closeTo(REWARDS_AMOUNT.div(2), ERROR_TOLERANCE); + earned2 = await rewardDistributorContract.earned(sampleAccount2.address); + expect(earned2).to.be.closeTo(REWARDS_AMOUNT.div(2), ERROR_TOLERANCE); + + let zetaBalance = BigNumber.from(0); + const zetaInitialBalanceAccount1 = await ethers.provider.getBalance(sampleAccount1.address); + const zetaInitialBalanceAccount2 = await ethers.provider.getBalance(sampleAccount2.address); + + await rewardDistributorContract.connect(sampleAccount1).getReward(true); + zetaBalance = await ethers.provider.getBalance(sampleAccount1.address); + expect(zetaBalance.sub(zetaInitialBalanceAccount1)).to.be.closeTo(REWARDS_AMOUNT.div(2), ERROR_TOLERANCE); + zetaBalance = await ethers.provider.getBalance(sampleAccount2.address); + expect(zetaBalance.sub(zetaInitialBalanceAccount2)).to.be.eq(0); + + await rewardDistributorContract.connect(sampleAccount2).getReward(true); + zetaBalance = await ethers.provider.getBalance(sampleAccount1.address); + expect(zetaBalance.sub(zetaInitialBalanceAccount1)).to.be.closeTo(REWARDS_AMOUNT.div(2), ERROR_TOLERANCE); + zetaBalance = await ethers.provider.getBalance(sampleAccount2.address); + expect(zetaBalance.sub(zetaInitialBalanceAccount2)).to.be.closeTo(REWARDS_AMOUNT.div(2), ERROR_TOLERANCE); + }); + + it("Should fail if rewards token is not ZETA", async () => { + const rewardToken = ZRC20Contracts[1]; + const tx = await rewardDistributorFactory.createTokenIncentive( + deployer.address, + deployer.address, + rewardToken.address, + ZRC20Contract.address, + AddressZero + ); + const receipt = await tx.wait(); + const event = receipt.events?.find(e => e.event === "RewardDistributorCreated"); + expect(event).to.not.be.undefined; + + const { rewardDistributorContract: rewardDistributorContractAddress } = event?.args as any; + rewardDistributorContract = (await ethers.getContractAt( + "RewardDistributor", + rewardDistributorContractAddress + )) as RewardDistributor; + + await rewardToken.transfer(rewardDistributorContract.address, REWARDS_AMOUNT); + await rewardDistributorContract.setRewardsDuration(REWARD_DURATION); + await rewardDistributorContract.notifyRewardAmount(REWARDS_AMOUNT); + + const sampleAccount1 = accounts[1]; + const stakedAmount1 = parseEther("100"); + + await stakeToken(sampleAccount1, stakedAmount1); + + await network.provider.send("evm_increaseTime", [REWARD_DURATION.div(2).toNumber()]); + await network.provider.send("evm_mine"); + + const getReward = rewardDistributorContract.connect(sampleAccount1).getReward(true); + await expect(getReward).to.be.revertedWith("Reward is not a wrapped asset"); + }); }); diff --git a/packages/zevm-app-contracts/test/Synthetixio/StakingRewards.ts b/packages/zevm-app-contracts/test/Synthetixio/StakingRewards.ts index c76c67be..9b842fdc 100644 --- a/packages/zevm-app-contracts/test/Synthetixio/StakingRewards.ts +++ b/packages/zevm-app-contracts/test/Synthetixio/StakingRewards.ts @@ -276,7 +276,7 @@ describe("StakingRewards", () => { }); }); - describe("getReward()", () => { + describe("getReward(false)", () => { it("should increase rewards token balance", async () => { const totalToStake = toUnit("100"); const totalToDistribute = toUnit("5000"); @@ -292,7 +292,7 @@ describe("StakingRewards", () => { const initialRewardBal = await rewardsToken.balanceOf(stakingAccount1.address); const initialEarnedBal = await stakingRewards.earned(stakingAccount1.address); - await stakingRewards.connect(stakingAccount1).getReward(); + await stakingRewards.connect(stakingAccount1).getReward(false); const postRewardBal = await rewardsToken.balanceOf(stakingAccount1.address); const postEarnedBal = await stakingRewards.earned(stakingAccount1.address); @@ -365,7 +365,7 @@ describe("StakingRewards", () => { await stakingRewards.connect(rewardsDistribution).notifyRewardAmount(totalToDistribute); await fastForward(DAY * 4); - await stakingRewards.connect(stakingAccount1).getReward(); + await stakingRewards.connect(stakingAccount1).getReward(false); await fastForward(DAY * 4); // New Rewards period much lower @@ -383,7 +383,7 @@ describe("StakingRewards", () => { await stakingRewards.connect(rewardsDistribution).notifyRewardAmount(totalToDistribute); await fastForward(DAY * 71); - await stakingRewards.connect(stakingAccount1).getReward(); + await stakingRewards.connect(stakingAccount1).getReward(false); }); }); @@ -434,7 +434,7 @@ describe("StakingRewards", () => { }); }); - describe("exit()", () => { + describe("exit(false)", () => { it("should retrieve all earned and increase rewards bal", async () => { const totalToStake = toUnit("100"); const totalToDistribute = toUnit("5000"); @@ -450,7 +450,7 @@ describe("StakingRewards", () => { const initialRewardBal = await rewardsToken.balanceOf(stakingAccount1.address); const initialEarnedBal = await stakingRewards.earned(stakingAccount1.address); - await stakingRewards.connect(stakingAccount1).exit(); + await stakingRewards.connect(stakingAccount1).exit(false); const postRewardBal = await rewardsToken.balanceOf(stakingAccount1.address); const postEarnedBal = await stakingRewards.earned(stakingAccount1.address); @@ -545,12 +545,12 @@ describe("StakingRewards", () => { assert.bnClose(rewardRewardsEarned, rewardRewardsEarnedPostWithdraw, toUnit("0.1")); // Get rewards const initialRewardBal = await rewardsToken.balanceOf(stakingAccount1.address); - await stakingRewards.connect(stakingAccount1).getReward(); + await stakingRewards.connect(stakingAccount1).getReward(false); const postRewardRewardBal = await rewardsToken.balanceOf(stakingAccount1.address); assert.bnGt(postRewardRewardBal, initialRewardBal); // Exit const preExitLPBal = await stakingToken.balanceOf(stakingAccount1.address); - await stakingRewards.connect(stakingAccount1).exit(); + await stakingRewards.connect(stakingAccount1).exit(false); const postExitLPBal = await stakingToken.balanceOf(stakingAccount1.address); assert.bnGt(postExitLPBal, preExitLPBal); });