Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce StakeVault and IStakeManager #61

Merged
merged 2 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 30 additions & 22 deletions .gas-report
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,34 @@
| updateRewardIndex | 23374 | 45581 | 39585 | 73785 | 3 |


| src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | |
|------------------------------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1115122 | 5026 | | | | |
| Function Name | min | avg | median | max | # calls |
| MAX_LOCKING_PERIOD | 273 | 273 | 273 | 273 | 22 |
| MAX_MULTIPLIER | 252 | 252 | 252 | 252 | 28 |
| MIN_LOCKING_PERIOD | 273 | 273 | 273 | 273 | 11 |
| MP_RATE_PER_YEAR | 231 | 231 | 231 | 231 | 3 |
| SCALE_FACTOR | 229 | 229 | 229 | 229 | 39 |
| accountedRewards | 351 | 909 | 351 | 2351 | 68 |
| getAccount | 1574 | 1574 | 1574 | 1574 | 65 |
| rewardIndex | 351 | 380 | 351 | 2351 | 68 |
| stake | 168062 | 217022 | 228861 | 249345 | 46 |
| totalMP | 374 | 374 | 374 | 374 | 71 |
| totalMaxMP | 351 | 351 | 351 | 351 | 71 |
| totalStaked | 330 | 330 | 330 | 330 | 71 |
| unstake | 75493 | 103902 | 91566 | 134228 | 13 |
| updateAccountMP | 34610 | 36848 | 37112 | 37112 | 19 |
| updateGlobalState | 29986 | 55567 | 47365 | 80313 | 25 |
| src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | |
|------------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1010922 | 4521 | | | | |
| Function Name | min | avg | median | max | # calls |
| MAX_LOCKUP_PERIOD | 273 | 273 | 273 | 273 | 22 |
| MAX_MULTIPLIER | 274 | 274 | 274 | 274 | 28 |
| MIN_LOCKUP_PERIOD | 230 | 230 | 230 | 230 | 11 |
| MP_RATE_PER_YEAR | 231 | 231 | 231 | 231 | 3 |
| SCALE_FACTOR | 229 | 229 | 229 | 229 | 39 |
| STAKING_TOKEN | 273 | 273 | 273 | 273 | 128 |
| accountedRewards | 373 | 931 | 373 | 2373 | 68 |
| getAccount | 1574 | 1574 | 1574 | 1574 | 65 |
| rewardIndex | 351 | 380 | 351 | 2351 | 68 |
| totalMP | 330 | 330 | 330 | 330 | 71 |
| totalMaxMP | 373 | 373 | 373 | 373 | 71 |
| totalStaked | 352 | 352 | 352 | 352 | 71 |
| updateAccountMP | 34632 | 36870 | 37134 | 37134 | 19 |
| updateGlobalState | 29986 | 55567 | 47365 | 80313 | 25 |


| src/StakeVault.sol:StakeVault contract | | | | | |
|----------------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 857122 | 4070 | | | | |
| Function Name | min | avg | median | max | # calls |
| stake | 194660 | 231720 | 238353 | 258837 | 46 |
| unstake | 81452 | 109772 | 99015 | 141680 | 13 |


| src/XPNFTToken.sol:XPNFTToken contract | | | | | |
Expand Down Expand Up @@ -107,8 +115,8 @@
| Deployment Cost | Deployment Size | | | | |
| 639406 | 3369 | | | | |
| Function Name | min | avg | median | max | # calls |
| approve | 46346 | 46346 | 46346 | 46346 | 165 |
| balanceOf | 561 | 1341 | 561 | 2561 | 292 |
| approve | 46334 | 46343 | 46346 | 46346 | 165 |
| balanceOf | 561 | 1351 | 561 | 2561 | 286 |
| mint | 51284 | 59028 | 51284 | 68384 | 181 |
| transfer | 34390 | 48070 | 51490 | 51490 | 10 |

Expand Down
64 changes: 32 additions & 32 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
IntegrationTest:testStake() (gas: 1376068)
IntegrationTest:testStakeFoo() (gas: 1459151)
NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 92874)
NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 60081)
NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35818)
NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 109345)
NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 50653)
NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35993)
RewardsStreamerTest:testStake() (gas: 869874)
StakeTest:test_StakeMultipleAccounts() (gas: 439303)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 586526)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 743870)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 448640)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 470042)
StakeTest:test_StakeOneAccount() (gas: 268046)
StakeTest:test_StakeOneAccountAndRewards() (gas: 415311)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 473030)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 468097)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 283269)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 283300)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 283411)
UnstakeTest:test_StakeMultipleAccounts() (gas: 439325)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 586548)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 743892)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 448639)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 470064)
UnstakeTest:test_StakeOneAccount() (gas: 268069)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 415289)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 473074)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 468099)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 283314)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 283300)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 283389)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 472770)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 616827)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 938166)
UnstakeTest:test_UnstakeOneAccount() (gas: 446562)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 467764)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 557405)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 490824)
StakeTest:test_StakeMultipleAccounts() (gas: 484173)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 629575)
StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 796295)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 490055)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 511457)
StakeTest:test_StakeOneAccount() (gas: 279745)
StakeTest:test_StakeOneAccountAndRewards() (gas: 425143)
StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 485909)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 480997)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 293513)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 293501)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 293612)
UnstakeTest:test_StakeMultipleAccounts() (gas: 484195)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 629552)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 796317)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 490032)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 511479)
UnstakeTest:test_StakeOneAccount() (gas: 279768)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 425165)
UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 485931)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 480977)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 293558)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 293501)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 293590)
UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 495129)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 672552)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 991588)
UnstakeTest:test_UnstakeOneAccount() (gas: 468592)
UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 483480)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 574902)
UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 505042)
XPNFTTokenTest:testApproveNotAllowed() (gas: 10507)
XPNFTTokenTest:testGetApproved() (gas: 10531)
XPNFTTokenTest:testIsApprovedForAll() (gas: 10705)
Expand Down
21 changes: 6 additions & 15 deletions src/RewardsStreamerMP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ pragma solidity ^0.8.26;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IStakeManager } from "./interfaces/IStakeManager.sol";

// Rewards Streamer with Multiplier Points
contract RewardsStreamerMP is ReentrancyGuard {
contract RewardsStreamerMP is ReentrancyGuard, IStakeManager {
error StakingManager__AmountCannotBeZero();
error StakingManager__TransferFailed();
error StakingManager__InsufficientBalance();
Expand All @@ -19,8 +20,8 @@ contract RewardsStreamerMP is ReentrancyGuard {
uint256 public constant SCALE_FACTOR = 1e18;
uint256 public constant MP_RATE_PER_YEAR = 1e18;

uint256 public constant MIN_LOCKING_PERIOD = 90 days;
uint256 public constant MAX_LOCKING_PERIOD = 4 * 365 days;
uint256 public constant MIN_LOCKUP_PERIOD = 90 days;
uint256 public constant MAX_LOCKUP_PERIOD = 4 * 365 days;
uint256 public constant MAX_MULTIPLIER = 4;

uint256 public totalStaked;
Expand Down Expand Up @@ -52,7 +53,7 @@ contract RewardsStreamerMP is ReentrancyGuard {
revert StakingManager__AmountCannotBeZero();
}

if (lockPeriod != 0 && (lockPeriod < MIN_LOCKING_PERIOD || lockPeriod > MAX_LOCKING_PERIOD)) {
if (lockPeriod != 0 && (lockPeriod < MIN_LOCKUP_PERIOD || lockPeriod > MAX_LOCKUP_PERIOD)) {
revert StakingManager__InvalidLockingPeriod();
}

Expand All @@ -69,11 +70,6 @@ contract RewardsStreamerMP is ReentrancyGuard {
distributeRewards(msg.sender, accountRewards);
}

bool success = STAKING_TOKEN.transferFrom(msg.sender, address(this), amount);
if (!success) {
revert StakingManager__TransferFailed();
}

account.stakedBalance += amount;
totalStaked += amount;

Expand All @@ -82,7 +78,7 @@ contract RewardsStreamerMP is ReentrancyGuard {
uint256 bonusMP = 0;

if (lockPeriod != 0) {
uint256 lockMultiplier = (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR) / MAX_LOCKING_PERIOD;
uint256 lockMultiplier = (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR) / MAX_LOCKUP_PERIOD;
bonusMP = amount * lockMultiplier / SCALE_FACTOR;
account.lockUntil = block.timestamp + lockPeriod;
} else {
Expand Down Expand Up @@ -132,11 +128,6 @@ contract RewardsStreamerMP is ReentrancyGuard {
totalMaxMP -= maxMPToReduce;
totalStaked -= amount;

bool success = STAKING_TOKEN.transfer(msg.sender, amount);
if (!success) {
revert StakingManager__TransferFailed();
}

account.accountRewardIndex = rewardIndex;
}

Expand Down
158 changes: 158 additions & 0 deletions src/StakeVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IStakeManager } from "./interfaces/IStakeManager.sol";

/**
* @title StakeVault
* @author Ricardo Guilherme Schmidt <ricardo3@status.im>
* @notice A contract to secure user stakes and manage staking with IStakeManager.
* @dev This contract is owned by the user and allows staking, unstaking, and withdrawing tokens.
*/
contract StakeVault is Ownable {
error StakeVault__NoEnoughAvailableBalance();
error StakeVault__InvalidDestinationAddress();
error StakeVault__UpdateNotAvailable();
error StakeVault__StakingFailed();
error StakeVault__UnstakingFailed();

//STAKING_TOKEN must be kept as an immutable, otherwise, StakeManager would accept StakeVaults with any token
//if is needed that STAKING_TOKEN to be a variable, StakeManager should be changed to check codehash and
//StakeVault(msg.sender).STAKING_TOKEN()
IERC20 public immutable STAKING_TOKEN;
IStakeManager private stakeManager;

/**
* @dev Emitted when tokens are staked.
* @param from The address from which tokens are transferred.
* @param to The address receiving the staked tokens (this contract).
* @param amount The amount of tokens staked.
* @param time The time period for which tokens are staked.
*/
event Staked(address indexed from, address indexed to, uint256 amount, uint256 time);

modifier validDestination(address _destination) {
if (_destination == address(0)) {
revert StakeVault__InvalidDestinationAddress();
}
_;
}

/**
* @notice Initializes the contract with the owner, staked token, and stake manager.
* @param _owner The address of the owner.
* @param _stakeManager The address of the StakeManager contract.
*/
constructor(address _owner, IStakeManager _stakeManager) Ownable(_owner) {
STAKING_TOKEN = _stakeManager.STAKING_TOKEN();
stakeManager = _stakeManager;
}

/**
* @notice Stake tokens for a specified time.
* @param _amount The amount of tokens to stake.
* @param _seconds The time period to stake for.
*/
function stake(uint256 _amount, uint256 _seconds) external onlyOwner {
_stake(_amount, _seconds, msg.sender);
}

/**
* @notice Stake tokens from a specified address for a specified time.
* @param _amount The amount of tokens to stake.
* @param _seconds The time period to stake for.
* @param _from The address from which tokens will be transferred.
*/
function stake(uint256 _amount, uint256 _seconds, address _from) external onlyOwner {
_stake(_amount, _seconds, _from);
}

/**
* @notice Unstake a specified amount of tokens and send to the owner.
* @param _amount The amount of tokens to unstake.
*/
function unstake(uint256 _amount) external onlyOwner {
_unstake(_amount, msg.sender);
}

/**
* @notice Unstake a specified amount of tokens and send to a destination address.
* @param _amount The amount of tokens to unstake.
* @param _destination The address to receive the unstaked tokens.
*/
function unstake(uint256 _amount, address _destination) external onlyOwner validDestination(_destination) {
_unstake(_amount, _destination);
}

/**
* @notice Withdraw tokens from the contract.
* @param _token The IERC20 token to withdraw.
* @param _amount The amount of tokens to withdraw.
*/
function withdraw(IERC20 _token, uint256 _amount) external onlyOwner {
_withdraw(_token, _amount, msg.sender);
}

/**
* @notice Withdraw tokens from the contract to a destination address.
* @param _token The IERC20 token to withdraw.
* @param _amount The amount of tokens to withdraw.
* @param _destination The address to receive the tokens.
*/
function withdraw(
IERC20 _token,
uint256 _amount,
address _destination
)
external
onlyOwner
validDestination(_destination)
{
_withdraw(_token, _amount, _destination);
}

/**
* @notice Returns the available amount of a token that can be withdrawn.
* @param _token The IERC20 token to check.
* @return The amount of token available for withdrawal.
*/
function availableWithdraw(IERC20 _token) external view returns (uint256) {
if (_token == STAKING_TOKEN) {
return STAKING_TOKEN.balanceOf(address(this)) - amountStaked();
}
return _token.balanceOf(address(this));
}

function _stake(uint256 _amount, uint256 _seconds, address _source) internal {
bool success = STAKING_TOKEN.transferFrom(_source, address(this), _amount);
if (!success) {
revert StakeVault__StakingFailed();
}

stakeManager.stake(_amount, _seconds);

emit Staked(_source, address(this), _amount, _seconds);
}

function _unstake(uint256 _amount, address _destination) internal {
stakeManager.unstake(_amount);
bool success = STAKING_TOKEN.transfer(_destination, _amount);
if (!success) {
revert StakeVault__UnstakingFailed();
}
}

function _withdraw(IERC20 _token, uint256 _amount, address _destination) internal {
if (_token == STAKING_TOKEN && STAKING_TOKEN.balanceOf(address(this)) - amountStaked() < _amount) {
revert StakeVault__NoEnoughAvailableBalance();
}
_token.transfer(_destination, _amount);
}

function amountStaked() public view returns (uint256) {
return stakeManager.getStakedBalance(address(this));
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@3esmit In this vault implementation I remove the functions to withdraw ETH.

The reason being is that this contract doesn't have any payable functions or fallback/receive implementations, so it can't receive any ETH anyways.

Loading
Loading