diff --git a/contracts/StakeManager.sol b/contracts/StakeManager.sol index d2171e8..f50fd0c 100644 --- a/contracts/StakeManager.sol +++ b/contracts/StakeManager.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; +import { console } from "forge-std/console.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -43,6 +44,8 @@ contract StakeManager is Ownable { uint256 public constant MAX_LOCKUP_PERIOD = 4 * YEAR; // 4 years uint256 public constant MP_APY = 1; uint256 public constant MAX_BOOST = 4; + uint256 public constant DEPOSIT_COOLDOWN_PERIOD = 2 weeks; + uint256 public constant WITHDRAW_COOLDOWN_PERIOD = 2 weeks; mapping(address index => Account value) public accounts; mapping(uint256 index => Epoch value) public epochs; @@ -108,12 +111,22 @@ contract StakeManager is Ownable { */ modifier finalizeEpoch() { if (block.timestamp >= epochEnd() && address(migration) == address(0)) { + console.log("Processing epoch..."); + console.log("--- currentEpoch: ", currentEpoch); + + uint256 _epochReward = epochReward(); + console.log("--- epochReward: ", _epochReward); //finalize current epoch - epochs[currentEpoch].epochReward = epochReward(); + epochs[currentEpoch].epochReward = _epochReward; epochs[currentEpoch].totalSupply = totalSupply(); pendingReward += epochs[currentEpoch].epochReward; + + console.log("--- totalSupply: ", totalSupply()); + console.log("--- pendingReward: ", pendingReward); //create new epoch currentEpoch++; + console.log("Done. New epoch started: ", currentEpoch); + console.log("\n"); epochs[currentEpoch].startTime = block.timestamp; } _; @@ -133,6 +146,7 @@ contract StakeManager is Ownable { * @dev Reverts when resulting locked time is not in range of [MIN_LOCKUP_PERIOD, MAX_LOCKUP_PERIOD] */ function stake(uint256 _amount, uint256 _timeToIncrease) external onlyVault noPendingMigration finalizeEpoch { + console.log("--- Staking: ", _amount); Account storage account = accounts[msg.sender]; if (account.lockUntil == 0) { @@ -248,6 +262,7 @@ contract StakeManager is Ownable { onlyAccountInitialized(_vault) finalizeEpoch { + console.log("Processing account: ", _vault); _processAccount(accounts[_vault], _limitEpoch); } @@ -367,14 +382,30 @@ contract StakeManager is Ownable { uint256 userEpoch = account.epoch; uint256 mpDifference = account.totalMP; for (Epoch storage iEpoch = epochs[userEpoch]; userEpoch < _limitEpoch; userEpoch++) { + console.log("--- processing account epoch: ", userEpoch); + uint256 userSupply = account.balance + account.totalMP; + console.log("--- userSupply in epoch (before): ", userSupply); + console.log("------ account.balance: ", account.balance); + console.log("------ account.totalMP: ", account.totalMP); + console.log("--- epoch totalSupply (before): ", iEpoch.totalSupply); + console.log("------ Minting multiplier points for epoch..."); //mint multiplier points to that epoch _mintMP(account, iEpoch.startTime + EPOCH_SIZE, iEpoch); - uint256 userSupply = account.balance + account.totalMP; + userSupply = account.balance + account.totalMP; + console.log("--- userSupply in epoch (after): ", userSupply); + console.log("------ account.balance: ", account.balance); + console.log("------ account.totalMP: ", account.totalMP); + console.log("--- epoch totalSupply (after): ", iEpoch.totalSupply); uint256 userEpochReward = Math.mulDiv(userSupply, iEpoch.epochReward, iEpoch.totalSupply); + console.log("--- userEpochReward: ", userEpochReward); userReward += userEpochReward; iEpoch.epochReward -= userEpochReward; + console.log("--- removing epoch userSupply from epoch totalSupply: ", userSupply); iEpoch.totalSupply -= userSupply; + console.log("--- New epoch epochReward: ", iEpoch.epochReward); + console.log("--- New epoch totalSupply: ", iEpoch.totalSupply); + console.log("\n"); } account.epoch = userEpoch; if (userReward > 0) { @@ -411,6 +442,8 @@ contract StakeManager is Ownable { //bonus for increased lock time mpToMint += _getMPToMint(account.balance + amount, increasedLockTime); } + + console.log("--- Minting MP: ", mpToMint); //update storage totalSupplyMP += mpToMint; account.bonusMP += mpToMint; @@ -432,6 +465,8 @@ contract StakeManager is Ownable { account.totalMP ); + console.log("--------- increasedMP: ", increasedMP); + //update storage account.lastMint = processTime; account.totalMP += increasedMP; @@ -501,4 +536,12 @@ contract StakeManager is Ownable { function epochEnd() public view returns (uint256 _epochEnd) { return epochs[currentEpoch].startTime + EPOCH_SIZE; } + + function getEpoch(uint256 _epoch) public view returns (Epoch memory) { + return epochs[_epoch]; + } + + function getAccount(address _account) public view returns (Account memory) { + return accounts[_account]; + } } diff --git a/contracts/StakeVault.sol b/contracts/StakeVault.sol index 8588d9e..b3e2431 100644 --- a/contracts/StakeVault.sol +++ b/contracts/StakeVault.sol @@ -14,15 +14,55 @@ import { StakeManager } from "./StakeManager.sol"; contract StakeVault is Ownable { error StakeVault__MigrationNotAvailable(); - error StakeVault__StakingFailed(); + error StakeVault__InDepositCooldown(); - error StakeVault__UnstakingFailed(); + error StakeVault__InWithdrawCooldown(); + + error StakeVault__DepositFailed(); + + error StakeVault__WithdrawFailed(); + + error StakeVault__InsufficientFunds(); + + error StakeVault__InvalidLockTime(); + + event Deposited(uint256 amount); + + event Withdrawn(uint256 amount); + + event Staked(uint256 _amount, uint256 time); StakeManager private stakeManager; ERC20 public immutable STAKED_TOKEN; - event Staked(address from, address to, uint256 _amount, uint256 time); + uint256 public balance; + + uint256 public depositCooldownUntil; + + uint256 public withdrawCooldownUntil; + + modifier whenNotInDepositCooldown() { + if (block.timestamp <= depositCooldownUntil) { + revert StakeVault__InDepositCooldown(); + } + _; + } + + modifier whenNotInWithdrawCooldown() { + if (block.timestamp <= withdrawCooldownUntil) { + revert StakeVault__InWithdrawCooldown(); + } + _; + } + + modifier onlySufficientBalance(uint256 _amount) { + uint256 availableFunds = _unstakedBalance(); + if (_amount > availableFunds) { + revert StakeVault__InsufficientFunds(); + } + _; + } constructor(address _owner, ERC20 _stakedToken, StakeManager _stakeManager) { _transferOwnership(_owner); @@ -30,26 +70,55 @@ contract StakeVault is Ownable { stakeManager = _stakeManager; } - function stake(uint256 _amount, uint256 _time) external onlyOwner { - bool success = STAKED_TOKEN.transferFrom(msg.sender, address(this), _amount); + function deposit(uint256 _amount) external onlyOwner whenNotInDepositCooldown { + depositCooldownUntil = block.timestamp + stakeManager.DEPOSIT_COOLDOWN_PERIOD(); + _deposit(msg.sender, _amount); + } + + function withdraw(uint256 _amount) public onlyOwner whenNotInWithdrawCooldown onlySufficientBalance(_amount) { + balance -= _amount; + bool success = STAKED_TOKEN.transfer(msg.sender, _amount); if (!success) { - revert StakeVault__StakingFailed(); + revert StakeVault__WithdrawFailed(); } - stakeManager.stake(_amount, _time); + emit Withdrawn(_amount); + } + + function stake( + uint256 _amount, + uint256 _time + ) + public + onlyOwner + whenNotInDepositCooldown + onlySufficientBalance(_amount) + { + _stake(_amount, _time); + } - emit Staked(msg.sender, address(this), _amount, _time); + function depositAndStake(uint256 _amount, uint256 _time) external onlyOwner whenNotInDepositCooldown { + uint256 stakedBalance = _stakedBalance(); + if (stakedBalance == 0 && _time == 0) { + // we expect `depositAndStake` to be called either with a lock time, + // or when there's already funds staked (because it's possible to top up stake without locking) + revert StakeVault__InvalidLockTime(); + } + _deposit(msg.sender, _amount); + _stake(_amount, _time); } function lock(uint256 _time) external onlyOwner { stakeManager.lock(_time); } - function unstake(uint256 _amount) external onlyOwner { + function unstake(uint256 _amount) external onlyOwner whenNotInWithdrawCooldown { + withdrawCooldownUntil = block.timestamp + stakeManager.WITHDRAW_COOLDOWN_PERIOD(); stakeManager.unstake(_amount); - bool success = STAKED_TOKEN.transfer(msg.sender, _amount); - if (!success) { - revert StakeVault__UnstakingFailed(); - } + } + + function unstakeAndWithdraw(uint256 _amount) external onlyOwner { + stakeManager.unstake(_amount); + withdraw(_amount); } function leave() external onlyOwner { @@ -69,4 +138,28 @@ contract StakeVault is Ownable { function stakedToken() external view returns (ERC20) { return STAKED_TOKEN; } + + function _deposit(address _from, uint256 _amount) internal { + balance += _amount; + bool success = STAKED_TOKEN.transferFrom(_from, address(this), _amount); + if (!success) { + revert StakeVault__DepositFailed(); + } + emit Deposited(_amount); + } + + function _stake(uint256 _amount, uint256 _time) internal { + stakeManager.stake(_amount, _time); + emit Staked(_amount, _time); + } + + function _unstakedBalance() internal view returns (uint256) { + (, uint256 stakedBalance,,,,,) = stakeManager.accounts(address(this)); + return balance - stakedBalance; + } + + function _stakedBalance() internal view returns (uint256) { + (, uint256 stakedBalance,,,,,) = stakeManager.accounts(address(this)); + return stakedBalance; + } } diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index 681ae16..8de94e2 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -76,7 +76,14 @@ contract StakeManagerTest is Test { userVault = _createTestVault(owner); vm.startPrank(owner); ERC20(stakeToken).approve(address(userVault), mintAmount); - userVault.stake(amount, lockTime); + + if (lockTime > 0) { + userVault.depositAndStake(amount, lockTime); + } else { + userVault.deposit(amount); + vm.warp(userVault.depositCooldownUntil() + 1); + userVault.stake(amount, lockTime); + } vm.stopPrank(); } } @@ -97,11 +104,11 @@ contract StakeTest is StakeManagerTest { uint256 lockTime = stakeManager.MIN_LOCKUP_PERIOD() - 1; vm.expectRevert(StakeManager.StakeManager__InvalidLockTime.selector); - userVault.stake(100, lockTime); + userVault.depositAndStake(100, lockTime); lockTime = stakeManager.MAX_LOCKUP_PERIOD() + 1; vm.expectRevert(StakeManager.StakeManager__InvalidLockTime.selector); - userVault.stake(100, lockTime); + userVault.depositAndStake(100, lockTime); } function test_StakeWithoutLockUpTimeMintsMultiplierPoints() public { @@ -129,7 +136,7 @@ contract StakeTest is StakeManagerTest { StakeVault userVault = _createStakingAccount(testUser, stakeAmount, lockToIncrease, mintAmount); vm.prank(testUser); - userVault.stake(stakeAmount2, 0); + userVault.depositAndStake(stakeAmount2, 0); (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2, "account balance"); @@ -138,7 +145,7 @@ contract StakeTest is StakeManagerTest { vm.warp(stakeManager.epochEnd()); vm.prank(testUser); - userVault.stake(stakeAmount3, 0); + userVault.depositAndStake(stakeAmount3, 0); (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount3, "account balance 2"); @@ -154,9 +161,9 @@ contract StakeTest is StakeManagerTest { _createStakingAccount(testUser2, stakeAmount, stakeManager.MIN_LOCKUP_PERIOD(), mintAmount); vm.prank(testUser); - userVault.stake(stakeAmount2, 0); + userVault.depositAndStake(stakeAmount2, 0); vm.prank(testUser2); - userVault2.stake(stakeAmount2, 0); + userVault2.depositAndStake(stakeAmount2, 0); (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2, "account balance"); @@ -168,9 +175,9 @@ contract StakeTest is StakeManagerTest { vm.warp(stakeManager.epochEnd()); vm.prank(testUser); - userVault.stake(stakeAmount2, 0); + userVault.depositAndStake(stakeAmount2, 0); vm.prank(testUser2); - userVault2.stake(stakeAmount2, 0); + userVault2.depositAndStake(stakeAmount2, 0); (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount2, "account balance 2"); @@ -187,9 +194,9 @@ contract StakeTest is StakeManagerTest { StakeVault userVault = _createStakingAccount(testUser, stakeAmount, 0, mintAmount); StakeVault userVault2 = _createStakingAccount(testUser2, stakeAmount, lockToIncrease, mintAmount); vm.prank(testUser); - userVault.stake(0, lockToIncrease); + userVault.depositAndStake(0, lockToIncrease); vm.prank(testUser2); - userVault2.stake(0, lockToIncrease); + userVault2.depositAndStake(0, lockToIncrease); (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "account balance"); @@ -201,9 +208,9 @@ contract StakeTest is StakeManagerTest { vm.warp(stakeManager.epochEnd()); vm.prank(testUser); - userVault.stake(0, lockToIncrease); + userVault.depositAndStake(0, lockToIncrease); vm.prank(testUser2); - userVault2.stake(0, lockToIncrease); + userVault2.depositAndStake(0, lockToIncrease); (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount, "account balance 2"); @@ -222,9 +229,9 @@ contract StakeTest is StakeManagerTest { StakeVault userVault2 = _createStakingAccount(testUser2, stakeAmount, lockToIncrease, mintAmount); vm.prank(testUser); - userVault.stake(stakeAmount2, lockToIncrease); + userVault.depositAndStake(stakeAmount2, lockToIncrease); vm.prank(testUser2); - userVault2.stake(stakeAmount2, lockToIncrease); + userVault2.depositAndStake(stakeAmount2, lockToIncrease); (, uint256 balance,, uint256 totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2, "account balance"); @@ -236,9 +243,9 @@ contract StakeTest is StakeManagerTest { vm.warp(stakeManager.epochEnd()); vm.prank(testUser); - userVault.stake(stakeAmount2, lockToIncrease); + userVault.depositAndStake(stakeAmount2, lockToIncrease); vm.prank(testUser2); - userVault2.stake(stakeAmount2, lockToIncrease); + userVault2.depositAndStake(stakeAmount2, lockToIncrease); (, balance,, totalMP,,,) = stakeManager.accounts(address(userVault)); assertEq(balance, stakeAmount + stakeAmount2 + stakeAmount2, "account balance 2"); @@ -518,57 +525,259 @@ contract ExecuteAccountTest is StakeManagerTest { stakeManager.executeAccount(address(userVault), currentEpoch + 1); } + function test_ExecuteAccountBug() public { + uint256 stakeAmount = 10_000_000; + deal(stakeToken, testUser, stakeAmount); + + // initial assumptions + assertEq(stakeManager.currentEpoch(), 0); + assertEq(stakeManager.pendingReward(), 0); + assertEq(stakeManager.totalSupplyMP(), 0); + assertEq(stakeManager.totalSupplyBalance(), 0); + StakeManager.Epoch memory currentEpoch = stakeManager.getEpoch(0); + assertEq(currentEpoch.startTime, block.timestamp); + assertEq(currentEpoch.epochReward, 0); + assertEq(currentEpoch.totalSupply, 0); + + userVaults.push(_createStakingAccount(testUser, stakeAmount, 0)); + + assertEq(stakeManager.currentEpoch(), 1); + assertEq(stakeManager.pendingReward(), 0); + assertEq(stakeManager.totalSupplyMP(), stakeAmount); + assertEq(stakeManager.totalSupplyBalance(), stakeAmount); + + // epoch `1` hasn't been processed yet, so no expected rewards + currentEpoch = stakeManager.getEpoch(1); + assertEq(currentEpoch.epochReward, 0); + assertEq(currentEpoch.totalSupply, 0); + + // however, account should have balance and MPs + StakeManager.Account memory account = stakeManager.getAccount(address(userVaults[0])); + assertEq(account.balance, stakeAmount); + assertEq(account.bonusMP, stakeAmount); + assertEq(account.totalMP, stakeAmount); + assertEq(account.epoch, 1); + + // -------- ADD REVENUE AND ADVANCE EPOCHS -------- + + // emulate revenue increase + console.log("--- Adding revenue: ", 10 ether); + deal(stakeToken, address(stakeManager), 10 ether); + + // ensure current `epoch` has is complete + vm.warp(stakeManager.epochEnd()); + // calculate account rewards and pending rewwards + stakeManager.executeEpoch(); + + assertEq(stakeManager.currentEpoch(), 2); + + // emulate revenue increase + console.log("--- Adding revenue: ", 10 ether); + deal(stakeToken, address(stakeManager), 20 ether); + + // ensure current `epoch` has is complete + vm.warp(stakeManager.epochEnd()); + // calculate account rewards and pending rewwards + stakeManager.executeEpoch(); + + // account epoch is still at 1 + account = stakeManager.getAccount(address(userVaults[0])); + assertEq(account.balance, stakeAmount); + assertEq(account.bonusMP, stakeAmount); + assertEq(account.totalMP, stakeAmount); + assertEq(account.epoch, 1); + + stakeManager.executeAccount(address(userVaults[0]), 2); + + stakeManager.executeAccount(address(userVaults[0]), 3); + } + function test_ExecuteAccountMintMP() public { uint256 stakeAmount = 10_000_000; deal(stakeToken, testUser, stakeAmount); - userVaults.push(_createStakingAccount(makeAddr("testUser"), stakeAmount, 0)); - userVaults.push(_createStakingAccount(makeAddr("testUser2"), stakeAmount, 0)); - userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, 0)); + // console.log("# NOW", block.timestamp); + // console.log("# START EPOCH", stakeManager.currentEpoch()); + // console.log("# PND_REWARDS", stakeManager.pendingReward()); + // console.log("------- CREATING ACCOUNT 1"); + // userVaults.push(_createStakingAccount(testUser, stakeAmount, 0)); + // (,,, , ,, uint256 epoch) = stakeManager.accounts(address(userVaults[0])); + // stakeManager.executeEpoch(); + // console.log("# NOW", block.timestamp); + // vm.warp(stakeManager.epochEnd()); + // console.log("# EPOCH END", block.timestamp); + // console.log("# START EPOCH", stakeManager.currentEpoch()); + // console.log("# USER 1 EPOCH", epoch); + // console.log("# PND_REWARDS", stakeManager.pendingReward()); + // vm.warp(stakeManager.epochEnd()); + // stakeManager.executeEpoch(); + // console.log("------- CREATING ACCOUNT 2"); + // userVaults.push(_createStakingAccount(testUser2, stakeAmount, 0)); + // (,,, , ,, epoch) = stakeManager.accounts(address(userVaults[1])); + // console.log("# NOW", block.timestamp); + // console.log("# START EPOCH", stakeManager.currentEpoch()); + // console.log("# USER 2 EPOCH", epoch); + // console.log("# PND_REWARDS", stakeManager.pendingReward()); + // userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, 0)); + + // console.log("######### NOW", block.timestamp); + // console.log("# START EPOCH", stakeManager.currentEpoch()); + // console.log("# PND_REWARDS", stakeManager.pendingReward()); + + // initial assumptions + assertEq(stakeManager.currentEpoch(), 0); + assertEq(stakeManager.pendingReward(), 0); + assertEq(stakeManager.totalSupplyMP(), 0); + assertEq(stakeManager.totalSupplyBalance(), 0); + StakeManager.Epoch memory currentEpoch = stakeManager.getEpoch(0); + assertEq(currentEpoch.startTime, block.timestamp); + assertEq(currentEpoch.epochReward, 0); + assertEq(currentEpoch.totalSupply, 0); - console.log("######### NOW", block.timestamp); - console.log("# START EPOCH", stakeManager.currentEpoch()); - console.log("# PND_REWARDS", stakeManager.pendingReward()); + // Create stake vaults and deposit + stake `stakeAmount` + // Keep in mind that this advances `block.timestamp` to get past the + // `depositCooldownUntil` time + userVaults.push(_createStakingAccount(testUser, stakeAmount, 0)); - for (uint256 i = 0; i < 3; i++) { - deal(stakeToken, address(stakeManager), 100 ether); - vm.warp(stakeManager.epochEnd()); - console.log("######### NOW", block.timestamp); - stakeManager.executeEpoch(); - console.log("##### NEW EPOCH", stakeManager.currentEpoch()); - console.log("# PND_REWARDS", stakeManager.pendingReward()); + assertEq(stakeManager.currentEpoch(), 1); + assertEq(stakeManager.pendingReward(), 0); + assertEq(stakeManager.totalSupplyMP(), stakeAmount); + assertEq(stakeManager.totalSupplyBalance(), stakeAmount); - for (uint256 j = 0; j < userVaults.length; j++) { - (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore) = - stakeManager.accounts(address(userVaults[j])); - uint256 rewardsBefore = ERC20(stakeToken).balanceOf(rewardAddress); - console.log("-Vault number", j); - console.log("--=====BEFORE====="); - console.log("---### totalMP :", totalMPBefore); - console.log("---#### lastMint :", lastMintBefore); - console.log("---## user_epoch :", epochBefore); - console.log("---##### rewards :", rewardsBefore); - console.log("--=====AFTER======"); - stakeManager.executeAccount(address(userVaults[j]), epochBefore + 1); - (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch) = stakeManager.accounts(address(userVaults[j])); - uint256 rewards = ERC20(stakeToken).balanceOf(rewardAddress); - console.log("---### deltaTime :", lastMint - lastMintBefore); - console.log("---### totalMP :", totalMP); - console.log("---#### lastMint :", lastMint); - console.log("---## user_epoch :", epoch); - console.log("---##### rewards :", rewards); - console.log("--=======#======="); - console.log("--# TOTAL_SUPPLY", stakeManager.totalSupply()); - console.log("--# PND_REWARDS", stakeManager.pendingReward()); - assertEq(lastMint, lastMintBefore + stakeManager.EPOCH_SIZE(), "must increaase lastMint"); - assertEq(epoch, epochBefore + 1, "must increase epoch"); - assertGt(totalMP, totalMPBefore, "must increase MPs"); - assertGt(rewards, rewardsBefore, "must increase rewards"); - lastMintBefore = lastMint; - epochBefore = epoch; - totalMPBefore = totalMP; - } - } + // epoch `1` hasn't been processed yet, so no expected rewards + currentEpoch = stakeManager.getEpoch(1); + assertEq(currentEpoch.epochReward, 0); + assertEq(currentEpoch.totalSupply, 0); + + // however, account should have balance and MPs + StakeManager.Account memory account = stakeManager.getAccount(address(userVaults[0])); + assertEq(account.balance, stakeAmount); + assertEq(account.bonusMP, stakeAmount); + assertEq(account.totalMP, stakeAmount); + assertEq(account.epoch, 1); + + // emulate revenue increase + console.log("--- Adding revenue: ", 100 ether); + deal(stakeToken, address(stakeManager), 100 ether); + + // ensure current `epoch` has is complete + vm.warp(stakeManager.epochEnd()); + // calculate account rewards and pending rewwards + stakeManager.executeEpoch(); + + assertEq(stakeManager.currentEpoch(), 2); + // previous epoch rewards should be added to `pendingReward` + currentEpoch = stakeManager.getEpoch(1); + assertEq(currentEpoch.epochReward, 100 ether, "epoch reward"); + assertEq(stakeManager.pendingReward(), 100 ether, "pending rewards"); + assertEq(stakeManager.totalSupplyMP(), stakeAmount); + assertEq(stakeManager.totalSupplyBalance(), stakeAmount); + assertEq(currentEpoch.totalSupply, account.balance + account.totalMP); + + // account epoch is still at 1 + account = stakeManager.getAccount(address(userVaults[0])); + assertEq(account.balance, stakeAmount); + assertEq(account.bonusMP, stakeAmount); + assertEq(account.totalMP, stakeAmount); + assertEq(account.epoch, 1); + + stakeManager.executeAccount(address(userVaults[0]), 2); + + assertEq(stakeManager.pendingReward(), 0); + account = stakeManager.getAccount(address(userVaults[0])); + assertEq(account.balance, stakeAmount); + assertEq(account.bonusMP, stakeAmount); + assertGt(account.totalMP, stakeAmount); + assertEq(ERC20(stakeToken).balanceOf(userVaults[0].owner()), 100 ether); + assertEq(account.epoch, 2); + + // second staker comes in + userVaults.push(_createStakingAccount(testUser2, stakeAmount, 0)); + StakeManager.Account memory secondAccount = stakeManager.getAccount(address(userVaults[1])); + + assertEq(stakeManager.currentEpoch(), 3); + assertEq(stakeManager.pendingReward(), 0); + assertEq(secondAccount.epoch, 3); + assertEq(secondAccount.balance, stakeAmount); + assertEq(secondAccount.bonusMP, stakeAmount); + assertEq(secondAccount.totalMP, stakeAmount); + // assertEq(stakeManager.totalSupplyMP(), stakeAmount); + assertEq(stakeManager.totalSupplyBalance(), stakeAmount * 2); + + // emulate revenue increase + deal(stakeToken, address(stakeManager), 100 ether); + vm.warp(stakeManager.epochEnd()); + // calculate account rewards and pending rewwards + // stakeManager.executeEpoch(); + // assertEq(stakeManager.pendingReward(), 100 ether); + stakeManager.executeAccount(address(userVaults[1]), 4); + secondAccount = stakeManager.getAccount(address(userVaults[1])); + assertEq(secondAccount.epoch, 4); + assertEq(secondAccount.balance, stakeAmount); + assertEq(secondAccount.bonusMP, stakeAmount); + assertGt(secondAccount.totalMP, stakeAmount); + assertEq(stakeManager.pendingReward(), 50 ether); + + account = stakeManager.getAccount(address(userVaults[0])); + assertEq(account.epoch, 2); + stakeManager.executeAccount(address(userVaults[0]), 4); + // assertEq(stakeManager.pendingReward(), 50 ether); + // stakeManager.executeAccount(address(userVaults[0]), 4); + assertEq(stakeManager.pendingReward(), 0); + + // ensure current `epoch` has is complete + + // userVaults.push(_createStakingAccount(makeAddr("testUser3"), stakeAmount, 0)); + + // for (uint256 i = 0; i < 3; i++) { + + // for (uint256 i = 0; i < 1; i++) { + // // emulate revenue increase + // deal(stakeToken, address(stakeManager), 100 ether); + // // ensure current `epoch` has is complete + // vm.warp(stakeManager.epochEnd()); + // // calculate account rewards and pending rewwards + // stakeManager.executeEpoch(); + // + // // uint256 currentEpoch = stakeManager.currentEpoch(); + // // (uint256 startTime, uint256 epochReward, uint256 totalSupply) = stakeManager.epochs(currentEpoch); + // // + // // console.log("######### NOW", block.timestamp); + // // console.log("##### NEW EPOCH", stakeManager.currentEpoch()); + // // console.log("# PND_REWARDS", stakeManager.pendingReward()); + // + // for (uint256 j = 0; j < userVaults.length; j++) { + // // (address rewardAddress,,, uint256 totalMPBefore, uint256 lastMintBefore,, uint256 epochBefore) = + // // stakeManager.accounts(address(userVaults[j])); + // // uint256 rewardsBefore = ERC20(stakeToken).balanceOf(rewardAddress); + // // console.log("-Vault number", j); + // // console.log("--=====BEFORE====="); + // // console.log("---### totalMP :", totalMPBefore); + // // console.log("---#### lastMint :", lastMintBefore); + // // console.log("---## user_epoch :", epochBefore); + // // console.log("---##### rewards :", rewardsBefore); + // // console.log("--=====AFTER======"); + // // stakeManager.executeAccount(address(userVaults[j]), epochBefore + 1); + // // (,,, uint256 totalMP, uint256 lastMint,, uint256 epoch) = + // // stakeManager.accounts(address(userVaults[j])); + // // uint256 rewards = ERC20(stakeToken).balanceOf(rewardAddress); + // // console.log("---### deltaTime :", lastMint - lastMintBefore); + // // console.log("---### totalMP :", totalMP); + // // console.log("---#### lastMint :", lastMint); + // // console.log("---## user_epoch :", epoch); + // // console.log("---##### rewards :", rewards); + // // console.log("--=======#======="); + // // console.log("--# TOTAL_SUPPLY", stakeManager.totalSupply()); + // // console.log("--# PND_REWARDS", stakeManager.pendingReward()); + // // assertEq(lastMint, lastMintBefore + stakeManager.EPOCH_SIZE(), "must increaase lastMint"); + // // assertEq(epoch, epochBefore + 1, "must increase epoch"); + // // assertGt(totalMP, totalMPBefore, "must increase MPs"); + // // assertGt(rewards, rewardsBefore, "must increase rewards"); + // // lastMintBefore = lastMint; + // // epochBefore = epoch; + // // totalMPBefore = totalMP; + // } + // } } function test_ShouldNotMintMoreThanCap() public { diff --git a/test/StakeVault.t.sol b/test/StakeVault.t.sol index bd7841b..946ab27 100644 --- a/test/StakeVault.t.sol +++ b/test/StakeVault.t.sol @@ -20,10 +20,14 @@ contract StakeVaultTest is Test { StakeVault internal stakeVault; + StakeVault internal stakeVault2; + address internal deployer; address internal testUser = makeAddr("testUser"); + address internal testUser2 = makeAddr("testUser2"); + address internal stakeToken; function setUp() public virtual { @@ -33,6 +37,12 @@ contract StakeVaultTest is Test { vm.prank(testUser); stakeVault = vaultFactory.createVault(); + + vm.prank(testUser2); + stakeVault2 = vaultFactory.createVault(); + + vm.prank(deployer); + stakeManager.setVault(address(stakeVault).codehash); } } @@ -46,7 +56,211 @@ contract StakedTokenTest is StakeVaultTest { } } +contract DepositTest is StakeVaultTest { + event Deposited(uint256 amount); + + function setUp() public override { + StakeVaultTest.setUp(); + } + + function test_RevertWhen_DepositAndInDepositCooldown() public { + deal(stakeToken, testUser, 1000); + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), 100); + stakeVault.deposit(100); + vm.expectRevert(StakeVault.StakeVault__InDepositCooldown.selector); + stakeVault.deposit(100); + } + + function test_Deposit() public { + uint256 userFunds = 1000; + uint256 depositAmount = 100; + + deal(stakeToken, testUser, userFunds); + deal(stakeToken, testUser2, userFunds); + + // first user + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), depositAmount); + + vm.expectEmit(true, true, true, true); + emit Deposited(depositAmount); + stakeVault.deposit(depositAmount); + + assertEq(ERC20(stakeToken).balanceOf(address(stakeVault)), depositAmount); + assertEq(stakeVault.balance(), depositAmount); + assertEq(stakeVault.depositCooldownUntil(), block.timestamp + stakeManager.DEPOSIT_COOLDOWN_PERIOD()); + // ensure funds haven't reached stake manager yet + assertEq(stakeManager.totalSupply(), 0); + } + + function test_DepositAfterCooldown() public { + uint256 userFunds = 1000; + uint256 depositAmount = 100; + deal(stakeToken, testUser, userFunds); + + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), depositAmount); + + // make first deposit + vm.expectEmit(true, true, true, true); + emit Deposited(depositAmount); + stakeVault.deposit(depositAmount); + + assertEq(ERC20(stakeToken).balanceOf(address(stakeVault)), depositAmount); + assertEq(stakeVault.balance(), depositAmount); + assertEq(stakeVault.depositCooldownUntil(), block.timestamp + stakeManager.DEPOSIT_COOLDOWN_PERIOD()); + assertEq(stakeManager.totalSupply(), 0); + + // wait for deposit cooldown to elapse + vm.warp(stakeVault.depositCooldownUntil() + 1); + + ERC20(stakeToken).approve(address(stakeVault), depositAmount); + + // make second deposit after first deposit has cooled down + vm.expectEmit(true, true, true, true); + emit Deposited(depositAmount); + stakeVault.deposit(depositAmount); + + assertEq(ERC20(stakeToken).balanceOf(address(stakeVault)), depositAmount * 2); + assertEq(stakeVault.balance(), depositAmount * 2); + assertEq(stakeVault.depositCooldownUntil(), block.timestamp + stakeManager.DEPOSIT_COOLDOWN_PERIOD()); + assertEq(stakeManager.totalSupply(), 0); + } +} + +contract WithdrawTest is StakeVaultTest { + event Withdrawn(uint256 amount); + + uint256 internal stakeAmount = 100; + uint256 internal userFunds = 1000; + + function setUp() public override { + StakeVaultTest.setUp(); + + deal(stakeToken, testUser, userFunds); + + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), stakeAmount); + stakeVault.deposit(stakeAmount); + } + + function test_RevertWhen_InWithdrawCooldown() public { + // ensure deposit cooldown has passed + vm.warp(stakeVault.depositCooldownUntil() + 1); + + // stake funds so we can unstake after that (and initialize withdraw cooldown) + stakeVault.stake(stakeAmount, 0); + stakeVault.unstake(stakeAmount); + + assertEq(stakeVault.withdrawCooldownUntil(), block.timestamp + stakeManager.WITHDRAW_COOLDOWN_PERIOD()); + + vm.expectRevert(StakeVault.StakeVault__InWithdrawCooldown.selector); + stakeVault.withdraw(stakeAmount); + } + + function test_RevertWhen_WithdrawInsufficientFunds() public { + vm.startPrank(testUser); + vm.expectRevert(StakeVault.StakeVault__InsufficientFunds.selector); + stakeVault.withdraw(stakeAmount + 1); + } + + function test_Withdraw() public { + vm.startPrank(testUser); + vm.expectEmit(true, true, true, true); + emit Withdrawn(stakeAmount); + stakeVault.withdraw(stakeAmount); + + assertEq(stakeVault.balance(), 0); + assertEq(ERC20(stakeToken).balanceOf(address(stakeVault)), 0); + assertEq(ERC20(stakeToken).balanceOf(testUser), userFunds); + } + + function test_WithdrawLessAmountThanAvailable() public { + uint256 remainingAmount = 50; + vm.startPrank(testUser); + vm.expectEmit(true, true, true, true); + emit Withdrawn(stakeAmount - remainingAmount); + stakeVault.withdraw(stakeAmount - remainingAmount); + + assertEq(stakeVault.balance(), remainingAmount); + assertEq(ERC20(stakeToken).balanceOf(address(stakeVault)), remainingAmount); + assertEq(ERC20(stakeToken).balanceOf(testUser), userFunds - remainingAmount); + + // try to withdraw the remaining amount + emit Withdrawn(remainingAmount); + stakeVault.withdraw(remainingAmount); + + assertEq(stakeVault.balance(), 0); + assertEq(ERC20(stakeToken).balanceOf(address(stakeVault)), 0); + assertEq(ERC20(stakeToken).balanceOf(testUser), userFunds); + } +} + contract StakeTest is StakeVaultTest { + event Staked(uint256 amount, uint256 time); + + uint256 internal userFunds = 1000; + uint256 internal stakeAmount = 100; + + function setUp() public override { + StakeVaultTest.setUp(); + deal(stakeToken, testUser, userFunds); + } + + function test_RevertWhen_StakeAndInDepositCooldown() public { + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), stakeAmount); + stakeVault.deposit(stakeAmount); + + vm.expectRevert(StakeVault.StakeVault__InDepositCooldown.selector); + stakeVault.stake(stakeAmount, 0); + } + + function test_RevertWhen_StakeAndInsufficientFunds() public { + vm.startPrank(testUser); + vm.expectRevert(StakeVault.StakeVault__InsufficientFunds.selector); + stakeVault.stake(stakeAmount + 1, 0); + + // do another one, this time with deposited funds (but too little) + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), stakeAmount); + stakeVault.deposit(stakeAmount); + + // make sure deposit cooldown has passed + vm.warp(stakeVault.depositCooldownUntil() + 1); + + vm.expectRevert(StakeVault.StakeVault__InsufficientFunds.selector); + stakeVault.stake(stakeAmount + 1, 0); + } + + function test_Stake() public { + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), stakeAmount); + stakeVault.deposit(stakeAmount); + assertEq(stakeManager.totalSupply(), 0); + + // make sure deposit cooldown has passed + vm.warp(stakeVault.depositCooldownUntil() + 1); + + vm.expectEmit(true, true, true, true); + emit Staked(stakeAmount, 0); + stakeVault.stake(stakeAmount, 0); + + assertEq(stakeVault.balance(), stakeAmount); + assertEq(ERC20(stakeToken).balanceOf(address(stakeVault)), stakeAmount); + assertEq(ERC20(stakeToken).balanceOf(testUser), userFunds - stakeAmount); + + (, uint256 stakeBalance, uint256 initialMP, uint256 currentMP,,,) = stakeManager.accounts(address(stakeVault)); + + assertEq(stakeBalance, stakeAmount); + assertEq(currentMP, stakeAmount); + assertEq(initialMP, stakeAmount); + assertEq(stakeManager.totalSupply(), stakeAmount + initialMP); + } +} + +contract StakeWithBrokenTokenTest is StakeVaultTest { function setUp() public override { DeployBroken deployment = new DeployBroken(); (vaultFactory, stakeManager, stakeToken) = deployment.run(); @@ -61,7 +275,44 @@ contract StakeTest is StakeVaultTest { vm.startPrank(address(testUser)); ERC20(stakeToken).approve(address(stakeVault), 100); - vm.expectRevert(StakeVault.StakeVault__StakingFailed.selector); - stakeVault.stake(100, 0); + vm.expectRevert(StakeVault.StakeVault__DepositFailed.selector); + stakeVault.deposit(100); + } +} + +contract UnstakeTest is StakeVaultTest { + uint256 internal userFunds = 1000; + uint256 internal stakeAmount = 100; + + function setUp() public override { + StakeVaultTest.setUp(); + deal(stakeToken, testUser, userFunds); + + vm.startPrank(testUser); + ERC20(stakeToken).approve(address(stakeVault), stakeAmount); + stakeVault.deposit(stakeAmount); + + // ensure deposit cooldown has passed + vm.warp(stakeVault.depositCooldownUntil() + 1); + } + + function test_RevertWhen_SenderIsNotOwner() public { + vm.stopPrank(); + vm.prank(deployer); + vm.expectRevert(bytes("Ownable: caller is not the owner")); + stakeVault.unstake(stakeAmount); + } + + function test_RevertWhen_UnstakeAndInWithdrawCooldown() public { + uint256 unstakeAmount = stakeAmount / 2; + + // stake funds so we can unstake after that (and initialize withdraw cooldown) + stakeVault.stake(stakeAmount, 0); + stakeVault.unstake(unstakeAmount); + + assertEq(stakeVault.withdrawCooldownUntil(), block.timestamp + stakeManager.WITHDRAW_COOLDOWN_PERIOD()); + + vm.expectRevert(StakeVault.StakeVault__InWithdrawCooldown.selector); + stakeVault.unstake(unstakeAmount); } }