Skip to content

Commit

Permalink
Add native token support to StakingReward contract
Browse files Browse the repository at this point in the history
  • Loading branch information
andresaiello committed Sep 6, 2023
1 parent 90685e8 commit 8450318
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./interfaces/IStakingRewards.sol";
import "./RewardsDistributionRecipient.sol";
import "./Pausable.sol";
import "./interfaces/IWETH9.sol";

// https://docs.synthetix.io/contracts/source/contracts/stakingrewards
contract StakingRewards is RewardsDistributionRecipient, ReentrancyGuard, Pausable {
Expand Down Expand Up @@ -105,18 +106,35 @@ 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 zero just for the purpose of the check
uint256 wad = 0;
bytes memory data = abi.encodeWithSelector(functionSignature, wad);

// Make the low-level call
(bool success, ) = address(rewardsToken).call(data);
require(success, "Reward is not a wrapped asset");

IWETH9(address(rewardsToken)).withdraw(reward);
(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 ========== */
Expand Down Expand Up @@ -178,4 +196,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 {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

interface IWETH9 {
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
event Deposit(address indexed dst, uint wad);
event Withdrawal(address indexed src, uint wad);

function totalSupply() external view returns (uint);

function balanceOf(address owner) external view returns (uint);

function allowance(address owner, address spender) external view returns (uint);

function approve(address spender, uint wad) external returns (bool);

function transfer(address to, uint wad) external returns (bool);

function transferFrom(address from, address to, uint wad) external returns (bool);

function deposit() external payable;

function withdraw(uint wad) external;
}
95 changes: 89 additions & 6 deletions packages/zevm-app-contracts/test/LiquidityIncentives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ethers, network } from "hardhat";

import {
ERC20,
ERC20__factory,
IWETH,
MockSystemContract,
MockZRC20,
Expand Down Expand Up @@ -227,13 +228,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);
Expand Down Expand Up @@ -281,13 +282,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);
Expand Down Expand Up @@ -421,7 +422,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");
});

Expand All @@ -444,7 +445,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");
});
});
16 changes: 8 additions & 8 deletions packages/zevm-app-contracts/test/Synthetixio/StakingRewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand All @@ -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);
});
});

Expand Down Expand Up @@ -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");
Expand All @@ -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);

Expand Down Expand Up @@ -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);
});
Expand Down

0 comments on commit 8450318

Please sign in to comment.