Files
staking-reward-streamer/test/RewardsStreamerMP.t.sol
r4bbit 9374025924 feat(StakeManager): add capabilities to register vaults
This commit introduces changes related to vault registrations in the
stake manager.

The stake manager needs to keep track of the vaults a users creates so
it can aggregate accumulated MP across vaults for any given user.

The `StakeVault` now comes with a `register()` function which needs to
be called to register itself with the stake manager. `StakeManager` has
a new `onlyRegisteredVault` modifier that ensures only registered vaults
can actually `stake` and `unstake`.

Closes #70
2024-12-03 16:55:34 +01:00

2132 lines
69 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol";
import { StakeVault } from "../src/StakeVault.sol";
import { IStakeManagerProxy } from "../src/interfaces/IStakeManagerProxy.sol";
import { StakeManagerProxy } from "../src/StakeManagerProxy.sol";
import { MockToken } from "./mocks/MockToken.sol";
import { StackOverflowStakeManager } from "./mocks/StackOverflowStakeManager.sol";
contract RewardsStreamerMPTest is Test {
MockToken stakingToken;
RewardsStreamerMP public streamer;
address admin = makeAddr("admin");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address charlie = makeAddr("charlie");
address dave = makeAddr("dave");
mapping(address owner => address vault) public vaults;
function setUp() public virtual {
stakingToken = new MockToken("Staking Token", "ST");
bytes memory initializeData = abi.encodeCall(RewardsStreamerMP.initialize, (admin, address(stakingToken)));
address impl = address(new RewardsStreamerMP());
address proxy = address(new StakeManagerProxy(impl, initializeData));
streamer = RewardsStreamerMP(proxy);
// Create a temporary vault just to get the codehash
StakeVault tempVault = new StakeVault(address(this), IStakeManagerProxy(address(streamer)));
bytes32 vaultCodeHash = address(tempVault).codehash;
// Register the codehash before creating any user vaults
vm.prank(admin);
streamer.setTrustedCodehash(vaultCodeHash, true);
address[4] memory accounts = [alice, bob, charlie, dave];
for (uint256 i = 0; i < accounts.length; i++) {
// ensure user has tokens
stakingToken.mint(accounts[i], 10_000e18);
// each user creates a vault
StakeVault vault = _createTestVault(accounts[i]);
vaults[accounts[i]] = address(vault);
vm.prank(accounts[i]);
stakingToken.approve(address(vault), 10_000e18);
}
}
struct CheckStreamerParams {
uint256 totalStaked;
uint256 totalMP;
uint256 totalMaxMP;
uint256 stakingBalance;
uint256 rewardBalance;
uint256 rewardIndex;
}
function checkStreamer(CheckStreamerParams memory p) public view {
assertEq(streamer.totalStaked(), p.totalStaked, "wrong total staked");
assertEq(streamer.totalMP(), p.totalMP, "wrong total MP");
assertEq(streamer.totalMaxMP(), p.totalMaxMP, "wrong totalMaxMP MP");
// assertEq(rewardToken.balanceOf(address(streamer)), p.rewardBalance, "wrong reward balance");
// assertEq(streamer.rewardIndex(), p.rewardIndex, "wrong reward index");
}
struct CheckAccountParams {
address account;
uint256 rewardBalance;
uint256 stakedBalance;
uint256 vaultBalance;
uint256 rewardIndex;
uint256 accountMP;
uint256 maxMP;
}
function checkAccount(CheckAccountParams memory p) public view {
// assertEq(rewardToken.balanceOf(p.account), p.rewardBalance, "wrong account reward balance");
RewardsStreamerMP.Account memory accountInfo = streamer.getAccount(p.account);
assertEq(accountInfo.stakedBalance, p.stakedBalance, "wrong account staked balance");
assertEq(stakingToken.balanceOf(p.account), p.vaultBalance, "wrong vault balance");
// assertEq(accountInfo.accountRewardIndex, p.rewardIndex, "wrong account reward index");
assertEq(accountInfo.accountMP, p.accountMP, "wrong account MP");
assertEq(accountInfo.maxMP, p.maxMP, "wrong account max MP");
}
struct CheckUserTotalsParams {
address user;
uint256 totalStakedBalance;
uint256 totalMP;
uint256 totalMaxMP;
}
function checkUserTotals(CheckUserTotalsParams memory p) public view {
assertEq(streamer.getUserTotalStakedBalance(p.user), p.totalStakedBalance, "wrong user total stake balance");
assertEq(streamer.getUserTotalMP(p.user), p.totalMP, "wrong user total MP");
assertEq(streamer.getUserTotalMaxMP(p.user), p.totalMaxMP, "wrong user total MP");
}
function _createTestVault(address owner) internal returns (StakeVault vault) {
vm.prank(owner);
vault = new StakeVault(owner, IStakeManagerProxy(address(streamer)));
vault.register();
if (!streamer.isTrustedCodehash(address(vault).codehash)) {
vm.prank(admin);
streamer.setTrustedCodehash(address(vault).codehash, true);
}
}
function _stake(address account, uint256 amount, uint256 lockupTime) public {
StakeVault vault = StakeVault(vaults[account]);
vm.prank(account);
vault.stake(amount, lockupTime);
}
function _unstake(address account, uint256 amount) public {
StakeVault vault = StakeVault(vaults[account]);
vm.prank(account);
vault.unstake(amount);
}
function _emergencyExit(address account) public {
StakeVault vault = StakeVault(vaults[account]);
vm.prank(account);
vault.emergencyExit(account);
}
function _leave(address account) public {
StakeVault vault = StakeVault(vaults[account]);
vm.prank(account);
vault.leave(account);
}
function _calculateBonusMP(uint256 amount, uint256 lockupTime) public view returns (uint256) {
return amount
* (lockupTime * streamer.MAX_MULTIPLIER() * streamer.SCALE_FACTOR() / streamer.MAX_LOCKUP_PERIOD())
/ streamer.SCALE_FACTOR();
}
function _calculeAccuredMP(uint256 totalStaked, uint256 timeDiff) public view returns (uint256) {
return (timeDiff * totalStaked * streamer.MP_RATE_PER_YEAR()) / (365 days * streamer.SCALE_FACTOR());
}
function _calculateTimeToMPLimit(uint256 amount) public view returns (uint256) {
uint256 maxMP = amount * streamer.MAX_MULTIPLIER();
uint256 mpPerYear = (amount * streamer.MP_RATE_PER_YEAR()) / streamer.SCALE_FACTOR();
uint256 timeInSeconds = (maxMP * 365 days) / mpPerYear;
return timeInSeconds;
}
}
contract VaultRegistrationTest is RewardsStreamerMPTest {
function setUp() public virtual override {
super.setUp();
}
function test_VaultRegistration() public view {
address[4] memory accounts = [alice, bob, charlie, dave];
for (uint256 i = 0; i < accounts.length; i++) {
address[] memory userVaults = streamer.getUserVaults(accounts[i]);
assertEq(userVaults.length, 1, "wrong number of vaults");
assertEq(userVaults[0], vaults[accounts[i]], "wrong vault address");
}
}
}
contract IntegrationTest is RewardsStreamerMPTest {
function setUp() public virtual override {
super.setUp();
}
function testStakeFoo() public {
streamer.updateGlobalState();
// T0
checkStreamer(
CheckStreamerParams({
totalStaked: 0,
totalMP: 0,
totalMaxMP: 0,
stakingBalance: 0,
rewardBalance: 0,
rewardIndex: 0
})
);
// T1
// Alice stakes 10 tokens
_stake(alice, 10e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 10e18,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
// T2
_stake(bob, 30e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 40e18,
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 10e18,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
// T3
vm.prank(admin);
streamer.updateGlobalState();
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 40e18,
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 1000e18,
rewardIndex: 125e17 // 1000 rewards / (40 staked + 40 MP) = 12.5
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 10e18,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
// T4
uint256 currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + (365 days / 2));
streamer.updateGlobalState();
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 60e18, // 6 months passed, 20 MP accrued
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 1000e18,
// 6 months passed and more MPs have been accrued
// so we need to adjust the reward index
rewardIndex: 10e18
})
);
// T5
_unstake(alice, 10e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 30e18,
totalMP: 45e18, // 60 - 15 from Alice (10 + 6 months = 5)
totalMaxMP: 150e18, // 200e18 - (10e18 * 5) = 150e18
stakingBalance: 30e18,
rewardBalance: 750e18,
rewardIndex: 10e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 250e18,
stakedBalance: 0e18,
vaultBalance: 0e18,
rewardIndex: 10e18,
accountMP: 0e18,
maxMP: 0e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
// T5
_stake(charlie, 30e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 60e18,
totalMP: 75e18,
totalMaxMP: 300e18,
stakingBalance: 60e18,
rewardBalance: 750e18,
rewardIndex: 10e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 250e18,
stakedBalance: 0e18,
vaultBalance: 0e18,
rewardIndex: 10e18,
accountMP: 0e18,
maxMP: 0e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[charlie],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 10e18,
accountMP: 30e18,
maxMP: 150e18
})
);
// T6
vm.prank(admin);
streamer.updateGlobalState();
checkStreamer(
CheckStreamerParams({
totalStaked: 60e18,
totalMP: 75e18,
totalMaxMP: 300e18,
stakingBalance: 60e18,
rewardBalance: 1750e18,
rewardIndex: 17_407_407_407_407_407_407
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 250e18,
stakedBalance: 0e18,
vaultBalance: 0e18,
rewardIndex: 10e18,
accountMP: 0e18,
maxMP: 0e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[charlie],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 10e18,
accountMP: 30e18,
maxMP: 150e18
})
);
//T7
_unstake(bob, 30e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 30e18,
totalMP: 30e18,
totalMaxMP: 150e18,
stakingBalance: 30e18,
// 1750 - (750 + 555.55) = 444.44
rewardBalance: 444_444_444_444_444_444_475,
rewardIndex: 17_407_407_407_407_407_407
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 250e18,
stakedBalance: 0e18,
vaultBalance: 0e18,
rewardIndex: 10e18,
accountMP: 0,
maxMP: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
// bob had 30 staked + 30 initial MP + 15 MP accrued in 6 months
// so in the second bucket we have 1000 rewards with
// bob's weight = 75
// charlie's weight = 60
// total weight = 135
// bobs rewards = 1000 * 75 / 135 = 555.555555555555555555
// bobs total rewards = 555.55 + 750 of the first bucket = 1305.55
rewardBalance: 1_305_555_555_555_555_555_525,
stakedBalance: 0e18,
vaultBalance: 0e18,
rewardIndex: 17_407_407_407_407_407_407,
accountMP: 0,
maxMP: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[charlie],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 10e18,
accountMP: 30e18,
maxMP: 150e18
})
);
}
}
contract StakeTest is RewardsStreamerMPTest {
function setUp() public virtual override {
super.setUp();
}
function test_StakeOneAccount() public {
// Alice stakes 10 tokens
_stake(alice, 10e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 10e18,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
}
function test_StakeOneAccountAndRewards() public {
_stake(alice, 10e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 10e18,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 1000e18,
rewardIndex: 50e18 // (1000 rewards / (10 staked + 10 MP)) = 50
})
);
}
function test_StakeOneAccountWithMinLockUp() public {
uint256 stakeAmount = 10e18;
uint256 lockUpPeriod = streamer.MIN_LOCKUP_PERIOD();
uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod);
_stake(alice, stakeAmount, lockUpPeriod);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
// 10e18 + (amount * (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR / MAX_LOCKUP_PERIOD) / SCALE_FACTOR)
totalMP: stakeAmount + expectedBonusMP,
totalMaxMP: 52_465_753_424_657_534_240,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_StakeOneAccountWithMaxLockUp() public {
uint256 stakeAmount = 10e18;
uint256 lockUpPeriod = streamer.MAX_LOCKUP_PERIOD();
uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod);
_stake(alice, stakeAmount, lockUpPeriod);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
// 10 + (amount * (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR / MAX_LOCKUP_PERIOD) / SCALE_FACTOR)
totalMP: stakeAmount + expectedBonusMP,
totalMaxMP: 90e18,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_StakeOneAccountWithRandomLockUp() public {
uint256 stakeAmount = 10e18;
uint256 lockUpPeriod = streamer.MIN_LOCKUP_PERIOD() + 13 days;
uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod);
_stake(alice, stakeAmount, lockUpPeriod);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
// 10 + (amount * (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR / MAX_LOCKUP_PERIOD) / SCALE_FACTOR)
totalMP: stakeAmount + expectedBonusMP,
totalMaxMP: 52_821_917_808_219_178_080,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() public {
uint256 stakeAmount = 15e18;
uint256 totalMaxMP = stakeAmount * streamer.MAX_MULTIPLIER() + stakeAmount;
uint256 totalMP = stakeAmount;
_stake(alice, stakeAmount, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
totalMP: stakeAmount,
totalMaxMP: totalMaxMP,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
uint256 currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + (365 days));
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
uint256 expectedMPIncrease = stakeAmount; // 1 year passed, 1 MP accrued per token staked
totalMP = totalMP + expectedMPIncrease;
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
totalMP: totalMP,
totalMaxMP: totalMaxMP,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
vaultBalance: stakeAmount,
rewardIndex: 0,
accountMP: totalMP, // accountMP == totalMP because only one account is staking
maxMP: totalMaxMP
})
);
currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + (365 days / 2));
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
expectedMPIncrease = stakeAmount / 2; // 1/2 year passed, 1/2 MP accrued per token staked
totalMP = totalMP + expectedMPIncrease;
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
totalMP: totalMP,
totalMaxMP: totalMaxMP,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
vaultBalance: stakeAmount,
rewardIndex: 0,
accountMP: totalMP, // accountMP == totalMP because only one account is staking
maxMP: totalMaxMP
})
);
}
function test_StakeOneAccountReachingMPLimit() public {
uint256 stakeAmount = 15e18;
uint256 totalMaxMP = stakeAmount * streamer.MAX_MULTIPLIER() + stakeAmount;
uint256 totalMP = stakeAmount;
_stake(alice, stakeAmount, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
totalMP: stakeAmount,
totalMaxMP: totalMaxMP,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
vaultBalance: stakeAmount,
rewardIndex: 0,
accountMP: totalMP, // accountMP == totalMP because only one account is staking
maxMP: totalMaxMP // maxMP == totalMaxMP because only one account is staking
})
);
uint256 currentTime = vm.getBlockTimestamp();
uint256 timeToMaxMP = _calculateTimeToMPLimit(stakeAmount);
vm.warp(currentTime + timeToMaxMP);
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
totalMP: totalMaxMP,
totalMaxMP: totalMaxMP,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
vaultBalance: stakeAmount,
rewardIndex: 0,
accountMP: totalMaxMP,
maxMP: totalMaxMP
})
);
// move forward in time to check we're not producing more MP
currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + 1);
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
totalMP: totalMaxMP,
totalMaxMP: totalMaxMP,
stakingBalance: stakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_StakeMultipleAccounts() public {
// Alice stakes 10 tokens
_stake(alice, 10e18, 0);
// Bob stakes 30 tokens
_stake(bob, 30e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 40e18,
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 10e18,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
}
function test_StakeMultipleAccountsAndRewards() public {
// Alice stakes 10 tokens
_stake(alice, 10e18, 0);
// Bob stakes 30 tokens
_stake(bob, 30e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 40e18,
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 10e18,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 30e18,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 40e18,
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 1000e18,
rewardIndex: 125e17 // (1000 rewards / (40 staked + 40 MP)) = 12,5
})
);
}
function test_StakeMultipleAccountsWithMinLockUp() public {
uint256 aliceStakeAmount = 10e18;
uint256 aliceLockUpPeriod = streamer.MIN_LOCKUP_PERIOD();
uint256 aliceExpectedBonusMP = _calculateBonusMP(aliceStakeAmount, aliceLockUpPeriod);
uint256 bobStakeAmount = 30e18;
uint256 bobLockUpPeriod = 0;
uint256 bobExpectedBonusMP = _calculateBonusMP(bobStakeAmount, bobLockUpPeriod);
// alice stakes with lockup period
_stake(alice, aliceStakeAmount, aliceLockUpPeriod);
// Bob stakes 30 tokens
_stake(bob, bobStakeAmount, bobLockUpPeriod);
uint256 sumOfStakeAmount = aliceStakeAmount + bobStakeAmount;
uint256 sumOfExpectedBonusMP = aliceExpectedBonusMP + bobExpectedBonusMP;
checkStreamer(
CheckStreamerParams({
totalStaked: sumOfStakeAmount,
totalMP: sumOfStakeAmount + sumOfExpectedBonusMP,
totalMaxMP: 202_465_753_424_657_534_240,
stakingBalance: sumOfStakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_StakeMultipleAccountsWithRandomLockUp() public {
uint256 aliceStakeAmount = 10e18;
uint256 aliceLockUpPeriod = streamer.MAX_LOCKUP_PERIOD() - 21 days;
uint256 aliceExpectedBonusMP = _calculateBonusMP(aliceStakeAmount, aliceLockUpPeriod);
uint256 bobStakeAmount = 30e18;
uint256 bobLockUpPeriod = streamer.MIN_LOCKUP_PERIOD() + 43 days;
uint256 bobExpectedBonusMP = _calculateBonusMP(bobStakeAmount, bobLockUpPeriod);
// alice stakes with lockup period
_stake(alice, aliceStakeAmount, aliceLockUpPeriod);
// Bob stakes 30 tokens
_stake(bob, bobStakeAmount, bobLockUpPeriod);
uint256 sumOfStakeAmount = aliceStakeAmount + bobStakeAmount;
uint256 sumOfExpectedBonusMP = aliceExpectedBonusMP + bobExpectedBonusMP;
checkStreamer(
CheckStreamerParams({
totalStaked: sumOfStakeAmount,
totalMP: sumOfStakeAmount + sumOfExpectedBonusMP,
totalMaxMP: 250_356_164_383_561_643_820,
stakingBalance: sumOfStakeAmount,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() public {
uint256 aliceStakeAmount = 15e18;
uint256 aliceMP = aliceStakeAmount;
uint256 aliceMaxMP = aliceStakeAmount * streamer.MAX_MULTIPLIER() + aliceMP;
uint256 bobStakeAmount = 5e18;
uint256 bobMP = bobStakeAmount;
uint256 bobMaxMP = bobStakeAmount * streamer.MAX_MULTIPLIER() + bobMP;
uint256 totalMP = aliceStakeAmount + bobStakeAmount;
uint256 totalStaked = aliceStakeAmount + bobStakeAmount;
uint256 totalMaxMP = aliceMaxMP + bobMaxMP;
_stake(alice, aliceStakeAmount, 0);
_stake(bob, bobStakeAmount, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: totalStaked,
totalMP: totalMP,
totalMaxMP: totalMaxMP,
stakingBalance: totalStaked,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: aliceStakeAmount,
vaultBalance: aliceStakeAmount,
rewardIndex: 0,
accountMP: aliceMP,
maxMP: aliceMaxMP
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: bobStakeAmount,
vaultBalance: bobStakeAmount,
rewardIndex: 0,
accountMP: bobMP,
maxMP: bobMaxMP
})
);
uint256 currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + (365 days));
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
streamer.updateAccountMP(vaults[bob]);
uint256 aliceExpectedMPIncrease = aliceStakeAmount; // 1 year passed, 1 MP accrued per token staked
uint256 bobExpectedMPIncrease = bobStakeAmount; // 1 year passed, 1 MP accrued per token staked
uint256 totalExpectedMPIncrease = aliceExpectedMPIncrease + bobExpectedMPIncrease;
aliceMP = aliceMP + aliceExpectedMPIncrease;
bobMP = bobMP + bobExpectedMPIncrease;
totalMP = totalMP + totalExpectedMPIncrease;
checkStreamer(
CheckStreamerParams({
totalStaked: totalStaked,
totalMP: totalMP,
totalMaxMP: totalMaxMP,
stakingBalance: totalStaked,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: aliceStakeAmount,
vaultBalance: aliceStakeAmount,
rewardIndex: 0,
accountMP: aliceMP,
maxMP: aliceMaxMP
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: bobStakeAmount,
vaultBalance: bobStakeAmount,
rewardIndex: 0,
accountMP: bobMP,
maxMP: bobMaxMP
})
);
currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + (365 days / 2));
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
streamer.updateAccountMP(vaults[bob]);
aliceExpectedMPIncrease = aliceStakeAmount / 2;
bobExpectedMPIncrease = bobStakeAmount / 2;
totalExpectedMPIncrease = aliceExpectedMPIncrease + bobExpectedMPIncrease;
aliceMP = aliceMP + aliceExpectedMPIncrease;
bobMP = bobMP + bobExpectedMPIncrease;
totalMP = totalMP + totalExpectedMPIncrease;
checkStreamer(
CheckStreamerParams({
totalStaked: totalStaked,
totalMP: totalMP,
totalMaxMP: totalMaxMP,
stakingBalance: totalStaked,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: aliceStakeAmount,
vaultBalance: aliceStakeAmount,
rewardIndex: 0,
accountMP: aliceMP,
maxMP: aliceMaxMP
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: bobStakeAmount,
vaultBalance: bobStakeAmount,
rewardIndex: 0,
accountMP: bobMP,
maxMP: bobMaxMP
})
);
}
}
contract UnstakeTest is StakeTest {
function setUp() public virtual override {
super.setUp();
}
function test_UnstakeOneAccount() public {
test_StakeOneAccount();
_unstake(alice, 8e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 2e18,
totalMP: 2e18,
totalMaxMP: 10e18,
stakingBalance: 2e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 2e18,
vaultBalance: 2e18,
rewardIndex: 0,
accountMP: 2e18,
maxMP: 10e18
})
);
_unstake(alice, 2e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 0,
totalMP: 0,
totalMaxMP: 0,
stakingBalance: 0,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_UnstakeOneAccountAndAccruedMP() public {
test_StakeOneAccount();
// wait for 1 year
uint256 currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + (365 days));
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 20e18, // total MP must have been doubled
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 0,
rewardIndex: 0
})
);
// unstake half of the tokens
_unstake(alice, 5e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 5e18, // 10 - 5
totalMP: 10e18, // 20 - 10 (5 initial + 5 accrued)
totalMaxMP: 25e18,
stakingBalance: 5e18,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_UnstakeOneAccountWithLockUpAndAccruedMP() public {
test_StakeOneAccountWithMinLockUp();
uint256 stakeAmount = 10e18;
uint256 lockUpPeriod = streamer.MIN_LOCKUP_PERIOD();
// 10e18 is what's used in `test_StakeOneAccountWithMinLockUp`
uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockUpPeriod);
// wait for 1 year
uint256 currentTime = vm.getBlockTimestamp();
vm.warp(currentTime + (365 days));
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
checkStreamer(
CheckStreamerParams({
totalStaked: stakeAmount,
totalMP: (stakeAmount + expectedBonusMP) + stakeAmount, // we do `+ stakeAmount` we've accrued
// `stakeAmount` after 1 year
totalMaxMP: 52_465_753_424_657_534_240,
stakingBalance: 10e18,
rewardBalance: 0,
rewardIndex: 0
})
);
// unstake half of the tokens
_unstake(alice, 5e18);
expectedBonusMP = _calculateBonusMP(5e18, lockUpPeriod);
checkStreamer(
CheckStreamerParams({
totalStaked: 5e18,
totalMP: (5e18 + expectedBonusMP) + 5e18,
totalMaxMP: 26_232_876_712_328_767_120,
stakingBalance: 5e18,
rewardBalance: 0,
rewardIndex: 0
})
);
}
function test_UnstakeOneAccountAndRewards() public {
test_StakeOneAccountAndRewards();
_unstake(alice, 8e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 2e18,
totalMP: 2e18,
totalMaxMP: 10e18,
stakingBalance: 2e18,
rewardBalance: 0, // rewards are all paid out to alice
rewardIndex: 50e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 1000e18,
stakedBalance: 2e18,
vaultBalance: 2e18,
rewardIndex: 50e18, // alice reward index has been updated
accountMP: 2e18,
maxMP: 10e18
})
);
}
function test_UnstakeBonusMPAndAccuredMP() public {
// setup variables
uint256 amountStaked = 10e18;
uint256 secondsLocked = streamer.MIN_LOCKUP_PERIOD();
uint256 reducedStake = 5e18;
uint256 increasedTime = 365 days;
//initialize memory placehodlders
uint256[4] memory timestamp;
uint256[4] memory increasedAccuredMP;
uint256[4] memory predictedBonusMP;
uint256[4] memory predictedAccuredMP;
uint256[4] memory predictedTotalMP;
uint256[4] memory predictedTotalMaxMP;
uint256[4] memory totalStaked;
//stages variables setup
uint256 stage = 0; // first stage: initialization
{
timestamp[stage] = block.timestamp;
totalStaked[stage] = amountStaked;
predictedBonusMP[stage] = totalStaked[stage] + _calculateBonusMP(totalStaked[stage], secondsLocked);
predictedTotalMaxMP[stage] = 52_465_753_424_657_534_240;
increasedAccuredMP[stage] = 0; //no increased accured MP in first stage
predictedAccuredMP[stage] = 0; //no accured MP in first stage
predictedTotalMP[stage] = predictedBonusMP[stage] + predictedAccuredMP[stage];
}
stage++; // second stage: progress in time
{
timestamp[stage] = timestamp[stage - 1] + increasedTime;
totalStaked[stage] = totalStaked[stage - 1];
predictedBonusMP[stage] = predictedBonusMP[stage - 1]; //no change in bonusMP in second stage
predictedTotalMaxMP[stage] = predictedTotalMaxMP[stage - 1];
increasedAccuredMP[stage] = _calculeAccuredMP(totalStaked[stage], timestamp[stage] - timestamp[stage - 1]);
predictedAccuredMP[stage] = predictedAccuredMP[stage - 1] + increasedAccuredMP[stage];
predictedTotalMP[stage] = predictedBonusMP[stage] + predictedAccuredMP[stage];
}
stage++; //third stage: reduced stake
{
timestamp[stage] = timestamp[stage - 1]; //no time increased in third stage
totalStaked[stage] = totalStaked[stage - 1] - reducedStake;
//bonusMP from this stage is a proportion from the difference of remainingStake and amountStaked
//if the account reduced 50% of its stake, the bonusMP should be reduced by 50%
predictedBonusMP[stage] = (totalStaked[stage] * predictedBonusMP[stage - 1]) / totalStaked[stage - 1];
predictedTotalMaxMP[stage] = (totalStaked[stage] * predictedTotalMaxMP[stage - 1]) / totalStaked[stage - 1];
increasedAccuredMP[stage] = 0; //no accuredMP in third stage;
//total accuredMP from this stage is a proportion from the difference of remainingStake and amountStaked
//if the account reduced 50% of its stake, the accuredMP should be reduced by 50%
predictedAccuredMP[stage] = (totalStaked[stage] * predictedAccuredMP[stage - 1]) / totalStaked[stage - 1];
predictedTotalMP[stage] = predictedBonusMP[stage] + predictedAccuredMP[stage];
}
// stages execution
stage = 0; // first stage: initialization
{
_stake(alice, amountStaked, secondsLocked);
{
RewardsStreamerMP.Account memory accountInfo = streamer.getAccount(vaults[alice]);
assertEq(accountInfo.stakedBalance, totalStaked[stage], "stage 1: wrong account staked balance");
assertEq(accountInfo.accountMP, predictedTotalMP[stage], "stage 1: wrong account MP");
assertEq(accountInfo.maxMP, predictedTotalMaxMP[stage], "stage 1: wrong account max MP");
assertEq(streamer.totalStaked(), totalStaked[stage], "stage 1: wrong total staked");
assertEq(streamer.totalMP(), predictedTotalMP[stage], "stage 1: wrong total MP");
assertEq(streamer.totalMaxMP(), predictedTotalMaxMP[stage], "stage 1: wrong totalMaxMP MP");
}
}
stage++; // second stage: progress in time
vm.warp(timestamp[stage]);
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
{
RewardsStreamerMP.Account memory accountInfo = streamer.getAccount(vaults[alice]);
assertEq(accountInfo.stakedBalance, totalStaked[stage], "stage 2: wrong account staked balance");
assertEq(accountInfo.accountMP, predictedTotalMP[stage], "stage 2: wrong account MP");
assertEq(accountInfo.maxMP, predictedTotalMaxMP[stage], "stage 2: wrong account max MP");
assertEq(streamer.totalStaked(), totalStaked[stage], "stage 2: wrong total staked");
assertEq(streamer.totalMP(), predictedTotalMP[stage], "stage 2: wrong total MP");
assertEq(streamer.totalMaxMP(), predictedTotalMaxMP[stage], "stage 2: wrong totalMaxMP MP");
}
stage++; // third stage: reduced stake
_unstake(alice, reducedStake);
{
RewardsStreamerMP.Account memory accountInfo = streamer.getAccount(vaults[alice]);
assertEq(accountInfo.stakedBalance, totalStaked[stage], "stage 3: wrong account staked balance");
assertEq(accountInfo.accountMP, predictedTotalMP[stage], "stage 3: wrong account MP");
assertEq(accountInfo.maxMP, predictedTotalMaxMP[stage], "stage 3: wrong account max MP");
assertEq(streamer.totalStaked(), totalStaked[stage], "stage 3: wrong total staked");
assertEq(streamer.totalMP(), predictedTotalMP[stage], "stage 3: wrong total MP");
assertEq(streamer.totalMaxMP(), predictedTotalMaxMP[stage], "stage 3: wrong totalMaxMP MP");
}
}
function test_UnstakeMultipleAccounts() public {
test_StakeMultipleAccounts();
_unstake(alice, 10e18);
_unstake(bob, 10e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 20e18,
totalMP: 20e18,
totalMaxMP: 100e18,
stakingBalance: 20e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 0,
vaultBalance: 0,
rewardIndex: 0,
accountMP: 0,
maxMP: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 20e18,
vaultBalance: 20e18,
rewardIndex: 0,
accountMP: 20e18,
maxMP: 100e18
})
);
}
function test_UnstakeMultipleAccountsAndRewards() public {
test_StakeMultipleAccountsAndRewards();
_unstake(alice, 10e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 30e18,
totalMP: 30e18,
totalMaxMP: 150e18,
stakingBalance: 30e18,
// alice owned a 25% of the pool, so 25% of the rewards are paid out to alice (250)
rewardBalance: 750e18,
rewardIndex: 125e17 // reward index remains unchanged
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 250e18,
stakedBalance: 0,
vaultBalance: 0,
rewardIndex: 125e17,
accountMP: 0,
maxMP: 0
})
);
_unstake(bob, 10e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 20e18,
totalMP: 20e18,
totalMaxMP: 100e18,
stakingBalance: 20e18,
rewardBalance: 0, // bob should've now gotten the rest of the rewards
rewardIndex: 125e17
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 750e18,
stakedBalance: 20e18,
vaultBalance: 20e18,
rewardIndex: 125e17,
accountMP: 20e18,
maxMP: 100e18
})
);
_unstake(bob, 20e18);
checkStreamer(
CheckStreamerParams({
totalStaked: 0,
totalMP: 0,
totalMaxMP: 0,
stakingBalance: 0,
rewardBalance: 0,
rewardIndex: 125e17
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 750e18,
stakedBalance: 0,
vaultBalance: 0,
rewardIndex: 125e17,
accountMP: 0,
maxMP: 0
})
);
}
}
contract LockTest is RewardsStreamerMPTest {
function setUp() public virtual override {
super.setUp();
}
function _lock(address account, uint256 lockPeriod) internal {
StakeVault vault = StakeVault(vaults[account]);
vm.prank(account);
vault.lock(lockPeriod);
}
function test_LockWithoutPriorLock() public {
// Setup - alice stakes 10 tokens without lock
uint256 stakeAmount = 10e18;
_stake(alice, stakeAmount, 0);
uint256 initialAccountMP = stakeAmount; // 10e18
uint256 initialMaxMP = stakeAmount * streamer.MAX_MULTIPLIER() + stakeAmount; // 50e18
// Verify initial state
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
vaultBalance: stakeAmount,
rewardIndex: 0,
accountMP: initialAccountMP,
maxMP: initialMaxMP
})
);
// Lock for 1 year
uint256 lockPeriod = 365 days;
uint256 expectedBonusMP = _calculateBonusMP(stakeAmount, lockPeriod);
_lock(alice, lockPeriod);
// Check updated state
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: stakeAmount,
vaultBalance: stakeAmount,
rewardIndex: 0,
accountMP: initialAccountMP + expectedBonusMP,
maxMP: initialMaxMP + expectedBonusMP
})
);
}
function test_LockFailsWithNoStake() public {
vm.expectRevert(RewardsStreamerMP.StakingManager__InsufficientBalance.selector);
_lock(alice, 365 days);
}
function test_LockFailsWithInvalidPeriod() public {
_stake(alice, 10e18, 0);
// Test with period = 0
vm.expectRevert(RewardsStreamerMP.StakingManager__InvalidLockingPeriod.selector);
_lock(alice, 0);
}
}
contract EmergencyExitTest is RewardsStreamerMPTest {
function setUp() public override {
super.setUp();
}
function test_CannotLeaveBeforeEmergencyMode() public {
_stake(alice, 10e18, 0);
vm.expectRevert(StakeVault.StakeVault__NotAllowedToExit.selector);
_emergencyExit(alice);
}
function test_OnlyOwnerCanEnableEmergencyMode() public {
vm.prank(alice);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
streamer.enableEmergencyMode();
}
function test_CannotEnableEmergencyModeTwice() public {
vm.prank(admin);
streamer.enableEmergencyMode();
vm.expectRevert(RewardsStreamerMP.StakingManager__EmergencyModeEnabled.selector);
vm.prank(admin);
streamer.enableEmergencyMode();
}
function test_EmergencyExitBasic() public {
uint256 aliceBalance = stakingToken.balanceOf(alice);
_stake(alice, 10e18, 0);
vm.prank(admin);
streamer.enableEmergencyMode();
_emergencyExit(alice);
// emergency exit will not perform any internal accounting
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 0,
rewardBalance: 0,
rewardIndex: 0
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 0,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
assertEq(stakingToken.balanceOf(alice), aliceBalance, "Alice should get tokens back");
assertEq(stakingToken.balanceOf(vaults[alice]), 0, "Vault should be empty");
}
function test_EmergencyExitWithRewards() public {
uint256 aliceInitialBalance = stakingToken.balanceOf(alice);
_stake(alice, 10e18, 0);
vm.prank(admin);
streamer.enableEmergencyMode();
_emergencyExit(alice);
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 1000e18,
rewardIndex: 50e18
})
);
// Check Alice staked tokens but no rewards
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice should get staked tokens back");
assertEq(stakingToken.balanceOf(address(vaults[alice])), 0, "Vault should be empty");
}
function test_EmergencyExitWithLock() public {
uint256 aliceInitialBalance = stakingToken.balanceOf(alice);
_stake(alice, 10e18, 90 days);
vm.prank(admin);
streamer.enableEmergencyMode();
_emergencyExit(alice);
// Check Alice got tokens back despite lock
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice should get tokens back despite lock");
assertEq(stakingToken.balanceOf(address(vaults[alice])), 0, "Vault should be empty");
}
function test_EmergencyExitMultipleUsers() public {
uint256 aliceInitialBalance = stakingToken.balanceOf(alice);
uint256 bobInitialBalance = stakingToken.balanceOf(bob);
// Setup multiple stakers
_stake(alice, 10e18, 0);
_stake(bob, 30e18, 0);
vm.prank(admin);
streamer.enableEmergencyMode();
// Alice exits first
_emergencyExit(alice);
// Check intermediate state
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 40e18,
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 1000e18,
rewardIndex: 125e17
})
);
// Bob exits
_emergencyExit(bob);
// Check final state
checkStreamer(
CheckStreamerParams({
totalStaked: 40e18,
totalMP: 40e18,
totalMaxMP: 200e18,
stakingBalance: 40e18,
rewardBalance: 1000e18,
rewardIndex: 125e17
})
);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 0,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
checkAccount(
CheckAccountParams({
account: vaults[bob],
rewardBalance: 0,
stakedBalance: 30e18,
vaultBalance: 0,
rewardIndex: 0,
accountMP: 30e18,
maxMP: 150e18
})
);
// Verify both users got their tokens back
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice should get staked tokens back");
assertEq(stakingToken.balanceOf(bob), bobInitialBalance, "Bob should get staked tokens back");
assertEq(stakingToken.balanceOf(vaults[alice]), 0, "Alice vault should have 0 staked tokens");
assertEq(stakingToken.balanceOf(vaults[bob]), 0, "Bob vault should have 0 staked tokens");
}
function test_EmergencyExitToAlternateAddress() public {
_stake(alice, 10e18, 0);
address alternateAddress = makeAddr("alternate");
uint256 alternateInitialBalance = stakingToken.balanceOf(alternateAddress);
vm.prank(admin);
streamer.enableEmergencyMode();
// Alice exits to alternate address
vm.prank(alice);
StakeVault aliceVault = StakeVault(vaults[alice]);
aliceVault.emergencyExit(alternateAddress);
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 10e18,
vaultBalance: 0,
rewardIndex: 0,
accountMP: 10e18,
maxMP: 50e18
})
);
// Check alternate address received everything
assertEq(
stakingToken.balanceOf(alternateAddress),
alternateInitialBalance + 10e18,
"Alternate address should get staked tokens"
);
}
}
contract UpgradeTest is RewardsStreamerMPTest {
function _upgradeStakeManager() internal {
address newImpl = address(new RewardsStreamerMP());
bytes memory initializeData;
vm.prank(admin);
UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData);
}
function setUp() public override {
super.setUp();
}
function test_RevertWhenNotOwner() public {
address newImpl = address(new RewardsStreamerMP());
bytes memory initializeData;
vm.prank(alice);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice));
UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData);
}
function test_UpgradeStakeManager() public {
// first, change state of existing stake manager
_stake(alice, 10e18, 0);
// check initial state
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 0,
rewardIndex: 0
})
);
// next, upgrade the stake manager
_upgradeStakeManager();
// ensure state is available in upgraded contract
checkStreamer(
CheckStreamerParams({
totalStaked: 10e18,
totalMP: 10e18,
totalMaxMP: 50e18,
stakingBalance: 10e18,
rewardBalance: 0,
rewardIndex: 0
})
);
}
}
contract LeaveTest is RewardsStreamerMPTest {
function _upgradeStakeManager() internal {
address newImpl = address(new RewardsStreamerMP());
bytes memory initializeData;
vm.prank(admin);
UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData);
}
function setUp() public override {
super.setUp();
}
function test_RevertWhenStakeManagerIsTrusted() public {
_stake(alice, 10e18, 0);
vm.expectRevert(StakeVault.StakeVault__NotAllowedToLeave.selector);
_leave(alice);
}
function test_LeaveShouldProperlyUpdateAccounting() public {
uint256 aliceInitialBalance = stakingToken.balanceOf(alice);
_stake(alice, 100e18, 0);
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance - 100e18, "Alice should have staked tokens");
checkStreamer(
CheckStreamerParams({
totalStaked: 100e18,
totalMP: 100e18,
totalMaxMP: 500e18,
stakingBalance: 100e18,
rewardBalance: 0,
rewardIndex: 0
})
);
_upgradeStakeManager();
_leave(alice);
// stake manager properly updates accounting
checkStreamer(
CheckStreamerParams({
totalStaked: 0,
totalMP: 0,
totalMaxMP: 0,
stakingBalance: 0,
rewardBalance: 0,
rewardIndex: 0
})
);
// vault should be empty as funds have been moved out
checkAccount(
CheckAccountParams({
account: vaults[alice],
rewardBalance: 0,
stakedBalance: 0,
vaultBalance: 0,
rewardIndex: 0,
accountMP: 0,
maxMP: 0
})
);
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice has all her funds back");
}
function test_TrustNewStakeManager() public {
// first, upgrade to new stake manager, marking it as not trusted
_upgradeStakeManager();
// ensure vault functions revert if stake manager is not trusted
vm.expectRevert(StakeVault.StakeVault__StakeManagerImplementationNotTrusted.selector);
_stake(alice, 100e18, 0);
// ensure vault functions revert if stake manager is not trusted
StakeVault vault = StakeVault(vaults[alice]);
vm.prank(alice);
vm.expectRevert(StakeVault.StakeVault__StakeManagerImplementationNotTrusted.selector);
vault.lock(365 days);
// ensure vault functions revert if stake manager is not trusted
vm.expectRevert(StakeVault.StakeVault__StakeManagerImplementationNotTrusted.selector);
_unstake(alice, 100e18);
// now, trust the new stake manager
address newStakeManagerImpl = IStakeManagerProxy(address(streamer)).implementation();
vm.prank(alice);
vault.trustStakeManager(newStakeManagerImpl);
// stake manager is now trusted, so functions are enabeled again
_stake(alice, 100e18, 0);
// however, a trusted manager cannot be left
vm.expectRevert(StakeVault.StakeVault__NotAllowedToLeave.selector);
_leave(alice);
}
}
contract MaliciousUpgradeTest is RewardsStreamerMPTest {
function _upgradeStakeManager() internal {
address newImpl = address(new RewardsStreamerMP());
bytes memory initializeData;
vm.prank(admin);
UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData);
}
function setUp() public override {
super.setUp();
}
function test_UpgradeStackOverflowStakeManager() public {
uint256 aliceInitialBalance = stakingToken.balanceOf(alice);
// first change the existing manager's state
_stake(alice, 100e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 100e18,
totalMP: 100e18,
totalMaxMP: 500e18,
stakingBalance: 100e18,
rewardBalance: 0,
rewardIndex: 0
})
);
// upgrade the manager to a malicious one
address newImpl = address(new StackOverflowStakeManager());
bytes memory initializeData;
vm.prank(admin);
UUPSUpgradeable(streamer).upgradeToAndCall(newImpl, initializeData);
// alice leaves system and is able to get funds out, despite malicious manager
_leave(alice);
assertEq(stakingToken.balanceOf(alice), aliceInitialBalance, "Alice should get her tokens back");
}
}
contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest {
function setUp() public virtual override {
super.setUp();
}
function testSetRewards() public {
assertEq(streamer.rewardStartTime(), 0);
assertEq(streamer.rewardEndTime(), 0);
assertEq(streamer.lastRewardTime(), 0);
uint256 currentTime = vm.getBlockTimestamp();
// just to be sure that currentTime is not 0
// since we are testing that it is used for rewardStartTime
currentTime += 1 days;
vm.warp(currentTime);
vm.prank(admin);
streamer.setReward(1000, 10);
assertEq(streamer.rewardStartTime(), currentTime);
assertEq(streamer.rewardEndTime(), currentTime + 10);
assertEq(streamer.lastRewardTime(), currentTime);
}
function testSetRewards_RevertsNotAuthorized() public {
vm.prank(alice);
vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector);
streamer.setReward(1000, 10);
}
function testSetRewards_RevertsBadDuration() public {
vm.prank(admin);
vm.expectRevert(RewardsStreamerMP.StakingManager__DurationCannotBeZero.selector);
streamer.setReward(1000, 0);
}
function testSetRewards_RevertsBadAmount() public {
vm.prank(admin);
vm.expectRevert(RewardsStreamerMP.StakingManager__AmountCannotBeZero.selector);
streamer.setReward(0, 10);
}
function testTotalRewardsSupply() public {
_stake(alice, 100e18, 0);
assertEq(streamer.totalRewardsSupply(), 0);
uint256 initialTime = vm.getBlockTimestamp();
vm.prank(admin);
streamer.setReward(1000e18, 10 days);
assertEq(streamer.totalRewardsSupply(), 0);
for (uint256 i = 0; i <= 10; i++) {
vm.warp(initialTime + i * 1 days);
assertEq(streamer.totalRewardsSupply(), 100e18 * i);
}
// after the end of the reward period, the total rewards supply does not increase
vm.warp(initialTime + 11 days);
assertEq(streamer.totalRewardsSupply(), 1000e18);
assertEq(streamer.totalRewardsAccrued(), 0);
uint256 secondRewardTime = initialTime + 20 days;
vm.warp(secondRewardTime);
// still the same rewards supply after 20 days
assertEq(streamer.totalRewardsSupply(), 1000e18);
assertEq(streamer.totalRewardsAccrued(), 0);
// set other 2000 rewards for other 10 days
vm.prank(admin);
streamer.setReward(2000e18, 10 days);
// accrued is 1000 from the previous reward and still 0 for the new one
assertEq(streamer.totalRewardsSupply(), 1000e18, "totalRewardsSupply should be 1000");
assertEq(streamer.totalRewardsAccrued(), 1000e18);
uint256 previousSupply = 1000e18;
for (uint256 i = 0; i <= 10; i++) {
vm.warp(secondRewardTime + i * 1 days);
assertEq(streamer.totalRewardsSupply(), previousSupply + 200e18 * i);
}
}
function testRewardsBalanceOf() public {
assertEq(streamer.totalRewardsSupply(), 0);
vm.warp(0);
uint256 initialTime = vm.getBlockTimestamp();
_stake(alice, 100e18, 0);
assertEq(streamer.rewardsBalanceOf(vaults[alice]), 0);
vm.prank(admin);
streamer.setReward(1000e18, 10 days);
assertEq(streamer.rewardsBalanceOf(vaults[alice]), 0);
vm.warp(initialTime + 1 days);
// FIXME: this is needed to update the global state and account MP
// Later we should update the functions to use "real-time" values.
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
uint256 tolerance = 300; // 300 wei
assertEq(streamer.totalRewardsSupply(), 100e18, "Total rewards supply mismatch");
assertApproxEqAbs(streamer.rewardsBalanceOf(vaults[alice]), 100e18, tolerance);
vm.warp(initialTime + 10 days);
streamer.updateGlobalState();
streamer.updateAccountMP(vaults[alice]);
assertEq(streamer.totalRewardsSupply(), 1000e18, "Total rewards supply mismatch");
assertApproxEqAbs(streamer.rewardsBalanceOf(vaults[alice]), 1000e18, tolerance);
}
}
contract MultipleVaultsStakeTest is RewardsStreamerMPTest {
StakeVault public vault1;
StakeVault public vault2;
StakeVault public vault3;
function setUp() public override {
super.setUp();
vault1 = _createTestVault(alice);
vault2 = _createTestVault(alice);
vault3 = _createTestVault(alice);
vm.startPrank(alice);
stakingToken.approve(address(vault1), 10_000e18);
stakingToken.approve(address(vault2), 10_000e18);
stakingToken.approve(address(vault3), 10_000e18);
vm.stopPrank();
}
function _stakeWithVault(address account, StakeVault vault, uint256 amount, uint256 lockupTime) public {
vm.prank(account);
vault.stake(amount, lockupTime);
}
function test_StakeMultipleVaults() public {
// Alice vault1 stakes 10 tokens
_stakeWithVault(alice, vault1, 10e18, 0);
// Alice vault2 stakes 20 tokens
_stakeWithVault(alice, vault2, 20e18, 0);
// Alice vault3 stakes 30 tokens
_stakeWithVault(alice, vault3, 60e18, 0);
checkStreamer(
CheckStreamerParams({
totalStaked: 90e18,
totalMP: 90e18,
totalMaxMP: 450e18,
stakingBalance: 90e18,
rewardBalance: 0,
rewardIndex: 0
})
);
checkUserTotals(
CheckUserTotalsParams({ user: alice, totalStakedBalance: 90e18, totalMP: 90e18, totalMaxMP: 450e18 })
);
}
}