Skip to content

Commit

Permalink
chore: add moar tests
Browse files Browse the repository at this point in the history
  • Loading branch information
0xApotheosis committed Sep 12, 2024
1 parent 20329ca commit 9d7b625
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 30 deletions.
81 changes: 69 additions & 12 deletions test/StakingRewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from '@nomicfoundation/hardhat-toolbox-viem/network-helpers';
import { expect } from 'chai';
import { getAddress, parseEther } from 'viem';
import { deployStakingRewardsFixture, DEFAULT_SUPPLY } from './utils';
import { deployStakingRewardsFixture } from './utils';

describe('StakingRewards', function () {
describe('Deployment', function () {
Expand All @@ -26,18 +26,18 @@ describe('StakingRewards', function () {

describe('Function permissions', () => {
it('only owner can call notifyRewardAmount', async () => {
const { stakingRewards, otherAccount } = await loadFixture(deployStakingRewardsFixture);
const { stakingRewards, stakingAccount2 } = await loadFixture(deployStakingRewardsFixture);
const rewardValue = parseEther('1');

await expect(stakingRewards.write.notifyRewardAmount([rewardValue], { account: otherAccount.account }))
await expect(stakingRewards.write.notifyRewardAmount([rewardValue], { account: stakingAccount2.account }))
.to.be.rejectedWith('Caller is not RewardsDistribution contract');
});

it('only rewardsDistribution address can call notifyRewardAmount', async () => {
const { stakingRewards, otherAccount } = await loadFixture(deployStakingRewardsFixture);
const { stakingRewards, stakingAccount2 } = await loadFixture(deployStakingRewardsFixture);
const rewardValue = parseEther('1');

await expect(stakingRewards.write.notifyRewardAmount([rewardValue], { account: otherAccount.account }))
await expect(stakingRewards.write.notifyRewardAmount([rewardValue], { account: stakingAccount2.account }))
.to.be.rejectedWith('Caller is not RewardsDistribution contract');
});
});
Expand All @@ -54,10 +54,10 @@ describe('StakingRewards', function () {

describe('External Rewards Recovery', () => {
it('only owner can call recoverERC20', async () => {
const { stakingRewards, otherAccount } = await loadFixture(deployStakingRewardsFixture);
const { stakingRewards, stakingAccount2 } = await loadFixture(deployStakingRewardsFixture);
const amount = parseEther('5000');

await expect(stakingRewards.write.recoverERC20([getAddress('0x0000000000000000000000000000000000000000'), amount], { account: otherAccount.account }))
await expect(stakingRewards.write.recoverERC20([getAddress('0x0000000000000000000000000000000000000000'), amount], { account: stakingAccount2.account }))
.to.be.rejectedWith('Only the contract owner may perform this action');
});
});
Expand All @@ -83,31 +83,88 @@ describe('StakingRewards', function () {
});

describe('Rewards', function () {
it('Should distribute rewards', async function () {
it('Should distribute rewards for a full epoch', async function () {
const { stakingRewards, rewardsToken, stakingToken, rewardsDistribution, stakingAccount1 } = await loadFixture(deployStakingRewardsFixture);
const stakeAmount = parseEther('100');
const rewardAmount = parseEther('1000');

// Stake tokens
await stakingToken.write.transfer([stakingAccount1.account.address, stakeAmount], { account: stakingAccount1.account });
await stakingToken.write.approve([stakingRewards.address, stakeAmount], { account: stakingAccount1.account });
await stakingRewards.write.stake([stakeAmount], { account: stakingAccount1.account });

// Distribute rewards
await rewardsToken.write.transfer([stakingRewards.address, rewardAmount], { account: rewardsDistribution.account });
await stakingRewards.write.notifyRewardAmount([rewardAmount], { account: rewardsDistribution.account });

const rewardsDuration = await stakingRewards.read.rewardsDuration() as bigint;
const timeStaked = rewardsDuration; // the whole epoch (7 days)

// Fast forward time
await time.increase(7 * 24 * 60 * 60); // 7 days
await time.increase(timeStaked);

const earnedRewards = await stakingRewards.read.earned([stakingAccount1.account.address]);
expect(earnedRewards as bigint > 0n);
expect(earnedRewards as bigint === ((timeStaked / rewardsDuration) * rewardAmount) / stakeAmount);
});

it('Should distribute rewards for a partial epoch', async function () {
const { stakingRewards, rewardsToken, stakingToken, rewardsDistribution, stakingAccount1 } = await loadFixture(deployStakingRewardsFixture);
const stakeAmount = parseEther('100');
const rewardAmount = parseEther('1000');

// Stake tokens
await stakingToken.write.approve([stakingRewards.address, stakeAmount], { account: stakingAccount1.account });
await stakingRewards.write.stake([stakeAmount], { account: stakingAccount1.account });

// Distribute rewards
await rewardsToken.write.transfer([stakingRewards.address, rewardAmount], { account: rewardsDistribution.account });
await stakingRewards.write.notifyRewardAmount([rewardAmount], { account: rewardsDistribution.account });

const rewardsDuration = await stakingRewards.read.rewardsDuration() as bigint;
const timeStaked = 3n * 24n * 60n * 60n; // 3 days

// Fast forward time
await time.increase(timeStaked);

const earnedRewards = await stakingRewards.read.earned([stakingAccount1.account.address]);
expect(earnedRewards as bigint === ((timeStaked / rewardsDuration) * rewardAmount) / stakeAmount);
});

it('Should distribute rewards for two stakers with different durations and amounts', async function () {
const { stakingRewards, rewardsToken, stakingToken, rewardsDistribution, stakingAccount1, stakingAccount2 } = await loadFixture(deployStakingRewardsFixture);
const stakeAmount1 = parseEther('23');
const stakeAmount2 = parseEther('19');
const rewardAmount = parseEther('1000');

// Stake tokens
await stakingToken.write.approve([stakingRewards.address, stakeAmount1], { account: stakingAccount1.account });
await stakingToken.write.approve([stakingRewards.address, stakeAmount2], { account: stakingAccount2.account });
await stakingRewards.write.stake([stakeAmount1], { account: stakingAccount1.account });
await stakingRewards.write.stake([stakeAmount2], { account: stakingAccount2.account });

// Distribute rewards
await rewardsToken.write.transfer([stakingRewards.address, rewardAmount], { account: rewardsDistribution.account });
await stakingRewards.write.notifyRewardAmount([rewardAmount], { account: rewardsDistribution.account });

const rewardsDuration = await stakingRewards.read.rewardsDuration() as bigint;
const timeStaked1 = 3n * 24n * 60n * 60n; // 3 days
const timeStaked2 = 5n * 24n * 60n * 60n; // 5 days

await time.increase(timeStaked1);

const earnedRewards1 = await stakingRewards.read.earned([stakingAccount1.account.address]);
expect(earnedRewards1 as bigint === ((timeStaked1 / rewardsDuration) * rewardAmount) / stakeAmount1);

await time.increase(timeStaked2 - timeStaked1);

const earnedRewards2 = await stakingRewards.read.earned([stakingAccount2.account.address]);
expect(earnedRewards2 as bigint === ((timeStaked2 / rewardsDuration) * rewardAmount) / stakeAmount2);
});
});

describe('Withdrawals', function () {
it('Should allow withdrawing staked tokens', async function () {
const { stakingRewards, stakingToken, stakingAccount1 } = await loadFixture(deployStakingRewardsFixture);
const initialBalance = await stakingToken.read.balanceOf([stakingAccount1.account.address]) as bigint;
const stakeAmount = parseEther('100');

// Stake tokens
Expand All @@ -119,7 +176,7 @@ describe('StakingRewards', function () {
await stakingRewards.write.withdraw([stakeAmount], { account: stakingAccount1.account });

expect(await stakingRewards.read.balanceOf([stakingAccount1.account.address])).to.equal(0n);
expect(await stakingToken.read.balanceOf([stakingAccount1.account.address])).to.equal(parseEther(DEFAULT_SUPPLY.toString()));
expect(await stakingToken.read.balanceOf([stakingAccount1.account.address])).to.equal(initialBalance);
});
});

Expand Down
36 changes: 18 additions & 18 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,31 @@ const PERMIT_TYPEHASH = utils.keccak256(
);

export async function mockToken({
deployerAccount,
accounts,
synth = undefined,
name = 'name',
symbol = 'ABC',
supply = DEFAULT_SUPPLY,
skipInitialAllocation = false,
}: {
deployerAccount: { account: { address: string } },
accounts: { account: { address: string } }[];
synth?: string;
name?: string;
symbol?: string;
supply?: number;
skipInitialAllocation?: boolean;
}) {
const [deployerAccount, owner] = accounts;
// const [deployerAccount, owner] = accounts;

const totalSupply = parseEther(supply.toString());

const proxy = await hre.viem.deployContract('ProxyERC20', [owner.account.address]);
const tokenState = await hre.viem.deployContract('TokenState', [owner.account.address, deployerAccount.account.address]);
const proxy = await hre.viem.deployContract('ProxyERC20', [deployerAccount.account.address]);
const tokenState = await hre.viem.deployContract('TokenState', [deployerAccount.account.address, deployerAccount.account.address]);

if (!skipInitialAllocation && supply > 0) {
await tokenState.write.setBalanceOf([owner.account.address, totalSupply], { account: deployerAccount.account });
await Promise.all(accounts.map(async (account) => {
await tokenState.write.setBalanceOf([account.account.address, totalSupply / BigInt(accounts.length)], { account: deployerAccount.account.address });
}));
}

const tokenArgs = [
Expand All @@ -40,33 +42,31 @@ export async function mockToken({
name,
symbol,
totalSupply,
owner.account.address,
deployerAccount.account.address,
];

if (synth) {
tokenArgs.push(synth);
}

const token = await hre.viem.deployContract(synth ? 'MockSynth' : 'PublicEST', tokenArgs);
const token = await hre.viem.deployContract('PublicEST', tokenArgs);
await Promise.all([
tokenState.write.setAssociatedContract([token.address], { account: owner.account }),
proxy.write.setTarget([token.address], { account: owner.account }),
tokenState.write.setAssociatedContract([token.address], { account: deployerAccount.account.address }),
proxy.write.setTarget([token.address], { account: deployerAccount.account.address }),
]);

return { token, tokenState, proxy };
}

export async function deployStakingRewardsFixture() {
const [owner, rewardsDistribution, stakingAccount1, otherAccount] = await hre.viem.getWalletClients();
const [owner, rewardsDistribution, stakingAccount1, stakingAccount2] = await hre.viem.getWalletClients();

const { token: rewardsToken } = await mockToken({
accounts: [owner, rewardsDistribution],
deployerAccount: owner,
accounts: [rewardsDistribution],
name: 'Rewards Token',
symbol: 'RWD',
});

const { token: stakingToken } = await mockToken({
accounts: [owner, stakingAccount1],
deployerAccount: owner,
accounts: [stakingAccount1, stakingAccount2],
name: 'Staking Token',
symbol: 'STK',
});
Expand All @@ -87,7 +87,7 @@ export async function deployStakingRewardsFixture() {
owner,
rewardsDistribution,
stakingAccount1,
otherAccount,
stakingAccount2,
publicClient,
};
}
Expand Down

0 comments on commit 9d7b625

Please sign in to comment.