StableHermesStaking
StableHermesStaking is a contract that allows HERMES deposits and receives stablecoins sent by MoneyMaker's daily
harvests. Users deposit HERMES and receive a share of what has been sent by MoneyMaker based on their participation of
the total deposited HERMES. It is similar to a MasterChef, but we allow for claiming of different reward tokens
(in case at some point we wish to change the stablecoin rewarded).
Every time updateReward(token) is called, We distribute the balance of that tokens as rewards to users that are
currently staking inside this contract, and they can claim it using withdraw(0)
StableHermesStaking.sol
contract StableHermesStaking is Ownable
using SafeMath for uint256;
using SafeERC20 for IERC20;
struct UserInfo {
uint256 amount;
mapping(IERC20 => uint256) rewardDebt;
}
We do some fancy math here. Basically, any point in time, the amount of HERMESs entitled to a user but is pending to be distributed is:
pending reward = (user.amount * accRewardPerShare) - user.rewardDebt[token]
Whenever a user deposits or withdraws HERMES. Here's what happens:
- accRewardPerShare (and
lastRewardBalance) gets updated - User receives the pending reward sent to his/her address
- User's
amountgets updated - User's
rewardDebt[token]gets updated
IERC20 public hermes;
uint256 internalHermesBalance;
Internal balance of HERMES, this gets updated on user deposits / withdrawals this allows to reward users with HERMES.
IERC20[] public rewardTokens;
Array of tokens that users can claim
mapping(IERC20 => bool) public isRewardToken;
mapping(IERC20 => uint256) public lastRewardBalance;
Last reward balance of token
address public feeCollector;
uint256 public depositFeePercent;
The deposit fee, scaled to PRECISION
mapping(IERC20 => uint256) public accRewardPerShare;
Accumulated token rewards per share, scaled to PRECISION. See above
uint256 public PRECISION;
PRECISION of accRewardPerShare
mapping(address => UserInfo) private userInfo;
Info of each user that stakes HERMES
Events
event Deposit(address indexed user, uint256 amount, uint256 fee);
Emitted when a user deposits HERMES
event DepositFeeChanged(uint256 newFee, uint256 oldFee);
Emitted when owner changes the deposit fee percentage
event Withdraw(address indexed user, uint256 amount);
Emitted when a user withdraws HERMES
event ClaimReward(address indexed user, address indexed rewardToken, uint256 amount);
Emitted when a user claims reward
event EmergencyWithdraw(address indexed user, uint256 amount);
Emitted when a user emergency withdraws its HERMES
event RewardTokenAdded(address token);
Emitted when owner adds a token to the reward tokens list
event RewardTokenRemoved(address token);
Emitted when owner removes a token from the reward tokens list
Setup Functions
constructor(
IERC20 _rewardToken,
IERC20 _hermes,
address _feeCollector,
uint256 _depositFeePercent
) {
require(_feeCollector != address(0), "StableHermesStaking: fee collector can't be address 0");
require(_depositFeePercent <= 5e17, "StableHermesStaking: max deposit fee can't be greater than 50%");
hermes = _hermes;
depositFeePercent = _depositFeePercent;
feeCollector = _feeCollector;
isRewardToken[_rewardToken] = true;
rewardTokens.push(_rewardToken);
PRECISION = 1e24;
}
Initialize a new StableHermesStaking contract
This contract needs to receive an ERC20 _rewardToken in order to distribute them (with MoneyMaker in our case)
@param _rewardToken The address of the ERC20 reward token
@param _hermes The address of the HERMES token
@param _depositFeePercent The deposit fee percent, scalled to 1e18, e.g. 3% is 3e16
Externally Accessible Functions
deposit
function deposit(uint256 _amount) external {
UserInfo storage user = userInfo[msg.sender];
uint256 _fee = (_amount * depositFeePercent) / 1e18;
uint256 _amountMinusFee = _amount.sub(_fee);
uint256 _previousAmount = user.amount;
uint256 _newAmount = user.amount.add(_amountMinusFee);
user.amount = _newAmount;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
IERC20 _token = rewardTokens[i];
updateReward(_token);
if (_previousAmount != 0) {
uint256 _pending = _previousAmount.mul(accRewardPerShare[_token]).div(PRECISION).sub(
user.rewardDebt[_token]
);
if (_pending != 0) {
safeTokenTransfer(_token, msg.sender, _pending);
emit ClaimReward(msg.sender, address(_token), _pending);
}
}
user.rewardDebt[_token] = _newAmount.mul(accRewardPerShare[_token]).div(PRECISION);
}
internalHermesBalance = internalHermesBalance.add(_amountMinusFee);
hermes.safeTransferFrom(msg.sender, feeCollector, _fee);
hermes.safeTransferFrom(msg.sender, address(this), _amountMinusFee);
emit Deposit(msg.sender, _amountMinusFee, _fee);
}
Deposit HERMES for reward token allocation @param _amount The amount of HERMES to deposit
getUserInfo
function getUserInfo(address _user, IERC20 _rewardToken) external view returns (uint256, uint256) {
UserInfo storage user = userInfo[_user];
return (user.amount, user.rewardDebt[_rewardToken]);
}
Get user info
@param _user The address of the user
@param _rewardToken The address of the reward token
@return The amount of HERMES user has deposited
@return The reward debt for the chosen token
rewardTokensLength
function rewardTokensLength() external view returns (uint256) {
return rewardTokens.length;
}
Get the number of reward tokens
@return The length of the array
addRewardToken
function addRewardToken(IERC20 _rewardToken) external onlyOwner {
require(
!isRewardToken[_rewardToken] && address(_rewardToken) != address(0),
"StableHermesStaking: token can't be added"
);
require(rewardTokens.length < 25, "StableHermesStaking: list of token too big");
rewardTokens.push(_rewardToken);
isRewardToken[_rewardToken] = true;
updateReward(_rewardToken);
emit RewardTokenAdded(address(_rewardToken));
}
Add a reward token @param _rewardToken The address of the reward token
removeRewardToken
function removeRewardToken(IERC20 _rewardToken) external onlyOwner {
require(isRewardToken[_rewardToken], "StableHermesStaking: token can't be removed");
updateReward(_rewardToken);
isRewardToken[_rewardToken] = false;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
if (rewardTokens[i] == _rewardToken) {
rewardTokens[i] = rewardTokens[_len - 1];
rewardTokens.pop();
break;
}
}
emit RewardTokenRemoved(address(_rewardToken));
}
Remove a reward token
@param _rewardToken The address of the reward token
setDepositFeePercent
function setDepositFeePercent(uint256 _depositFeePercent) external onlyOwner {
require(_depositFeePercent <= 5e17, "StableHermesStaking: deposit fee can't be greater than 50%");
uint256 oldFee = depositFeePercent;
depositFeePercent = _depositFeePercent;
emit DepositFeeChanged(_depositFeePercent, oldFee);
}
Set the deposit fee percent
@param _depositFeePercent The new deposit fee percent
pendingReward
function pendingReward(address _user, IERC20 _token) external view returns (uint256) {
require(isRewardToken[_token], "StableHermesStaking: wrong reward token");
UserInfo storage user = userInfo[_user];
uint256 _totalHermes = internalHermesBalance;
uint256 _accRewardTokenPerShare = accRewardPerShare[_token];
uint256 _currRewardBalance = _token.balanceOf(address(this));
uint256 _rewardBalance = _token == hermes ? _currRewardBalance.sub(internalHermesBalance) : _currRewardBalance;
if (_rewardBalance != lastRewardBalance[_token] && _totalHermes != 0) {
uint256 _accruedReward = _rewardBalance.sub(lastRewardBalance[_token]);
_accRewardTokenPerShare = _accRewardTokenPerShare.add(_accruedReward.mul(PRECISION).div(_totalHermes));
}
return user.amount.mul(_accRewardTokenPerShare).div(PRECISION).sub(user.rewardDebt[_token]);
}
View function to see pending reward token on frontend
@param _user The address of the user
@param _token The address of the token
@return _user's pending reward token
withdraw
function withdraw(uint256 _amount) external {
UserInfo storage user = userInfo[msg.sender];
uint256 _previousAmount = user.amount;
require(_amount <= _previousAmount, "StableHermesStaking: withdraw amount exceeds balance");
uint256 _newAmount = user.amount.sub(_amount);
user.amount = _newAmount;
uint256 _len = rewardTokens.length;
if (_previousAmount != 0) {
for (uint256 i; i < _len; i++) {
IERC20 _token = rewardTokens[i];
updateReward(_token);
uint256 _pending = _previousAmount.mul(accRewardPerShare[_token]).div(PRECISION).sub(
user.rewardDebt[_token]
);
user.rewardDebt[_token] = _newAmount.mul(accRewardPerShare[_token]).div(PRECISION);
if (_pending != 0) {
safeTokenTransfer(_token, msg.sender, _pending);
emit ClaimReward(msg.sender, address(_token), _pending);
}
}
}
internalHermesBalance = internalHermesBalance.sub(_amount);
hermes.safeTransfer(msg.sender, _amount);
emit Withdraw(msg.sender, _amount);
}
Withdraw HERMES and harvest the rewards @param _amount The amount of HERMES to withdraw
emergencyWithdraw
function emergencyWithdraw() external {
UserInfo storage user = userInfo[msg.sender];
uint256 _amount = user.amount;
user.amount = 0;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
IERC20 _token = rewardTokens[i];
user.rewardDebt[_token] = 0;
}
hermes.safeTransfer(msg.sender, _amount);
emit EmergencyWithdraw(msg.sender, _amount);
}
Withdraw without caring about rewards. EMERGENCY ONLY
updateReward
function updateReward(IERC20 _token) public {
require(isRewardToken[_token], "StableHermesStaking: wrong reward token");
uint256 _currRewardBalance = _token.balanceOf(address(this));
uint256 _rewardBalance = _token == hermes ? _currRewardBalance.sub(internalHermesBalance) : _currRewardBalance;
uint256 _totalHermes = internalHermesBalance;
// Did StableHermesStaking receive any token
if (_rewardBalance == lastRewardBalance[_token] || _totalHermes == 0) {
return;
}
uint256 _accruedReward = _rewardBalance.sub(lastRewardBalance[_token]);
accRewardPerShare[_token] = accRewardPerShare[_token].add(_accruedReward.mul(PRECISION).div(_totalHermes));
lastRewardBalance[_token] = _rewardBalance;
}
Update reward variables @param _token The address of the reward token @dev Needs to be called before any deposit or withdrawal
Internal functions
safeTokenTransfer
function safeTokenTransfer(
IERC20 _token,
address _to,
uint256 _amount
) internal {
uint256 _currRewardBalance = _token.balanceOf(address(this));
uint256 _rewardBalance = _token == hermes ? _currRewardBalance.sub(internalHermesBalance) : _currRewardBalance;
if (_amount > _rewardBalance) {
lastRewardBalance[_token] = lastRewardBalance[_token].sub(_rewardBalance);
_token.safeTransfer(_to, _rewardBalance);
} else {
lastRewardBalance[_token] = lastRewardBalance[_token].sub(_amount);
_token.safeTransfer(_to, _amount);
}
}
Safe token transfer function, just in case if rounding error
causes pool to not have enough reward tokens
@param _token The address of then token to transfer
@param _to The address that will receive _amount rewardToken
@param _amount The amount to send to _to