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

Upgrade sjoe #120

Merged
merged 19 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ contracts/.deps/
.idea
.openzeppelin
.vscode

out
broadcast
forge-cache/*
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
branch = v1.5.5
120 changes: 89 additions & 31 deletions contracts/StableJoeStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol
* harvests. Users deposit JOE and receive a share of what has been sent by MoneyMaker based on their participation of
* the total deposited JOE. 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
* 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)`
*/
contract StableJoeStaking is Initializable, OwnableUpgradeable {
Expand All @@ -40,28 +40,47 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
*/
}

IERC20Upgradeable public joe;
// @dev gap to keep the storage ordering, replace `IERC20Upgradeable public joe;`
uint256[1] private __gap0;

/// @notice The address of the JOE token
IERC20Upgradeable public immutable joe;

/// @dev Internal balance of JOE, this gets updated on user deposits / withdrawals
/// this allows to reward users with JOE
uint256 public internalJoeBalance;

/// @notice Array of tokens that users can claim
IERC20Upgradeable[] public rewardTokens;

/// @notice Mapping to check if a token is a reward token
mapping(IERC20Upgradeable => bool) public isRewardToken;

/// @notice Last reward balance of `token`
mapping(IERC20Upgradeable => uint256) public lastRewardBalance;

// @notice The address where deposit fees will be sent
0x0Louis marked this conversation as resolved.
Show resolved Hide resolved
address public feeCollector;

/// @notice The deposit fee, scaled to `DEPOSIT_FEE_PERCENT_PRECISION`
uint256 public depositFeePercent;
uint88 public depositFeePercent;
/// @notice Reentrancy guard
bool public reentrant;
Copy link
Contributor

Choose a reason for hiding this comment

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

This only work because depositFeePercent is zero atm. That's a bit dangerous imo, imagine we put this upgrade on hold for some reason, then later we update the deposit fee and decide to finally carry on the upgrade while forgetting about this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, but to me it's fine as it would just reset the value to 0. I'll use the right storage for safety, but I don't think it's necessary


// @dev gap to keep the storage ordering, replace `uint256 public depositFeePercent;`
0x0Louis marked this conversation as resolved.
Show resolved Hide resolved
// and `uint256 public DEPOSIT_FEE_PERCENT_PRECISION;`
uint256[2] private __gap1;

/// @notice The precision of `depositFeePercent`
uint256 public DEPOSIT_FEE_PERCENT_PRECISION;
uint256 public constant DEPOSIT_FEE_PERCENT_PRECISION = 1e18;

/// @notice Accumulated `token` rewards per share, scaled to `ACC_REWARD_PER_SHARE_PRECISION`
mapping(IERC20Upgradeable => uint256) public accRewardPerShare;

// @dev gap to keep the storage ordering, replace `uint256 public DEPOSIT_FEE_PERCENT_PRECISION;`
uint256[1] private __gap3;

0x0Louis marked this conversation as resolved.
Show resolved Hide resolved
/// @notice The precision of `accRewardPerShare`
uint256 public ACC_REWARD_PER_SHARE_PRECISION;
uint256 public constant ACC_REWARD_PER_SHARE_PRECISION = 1e24;

/// @dev Info of each user that stakes JOE
mapping(address => UserInfo) private userInfo;
Expand All @@ -87,42 +106,58 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
/// @notice Emitted when owner removes a token from the reward tokens list
event RewardTokenRemoved(address token);

/// @notice Emitted when owner sweeps a token
event TokenSwept(address token, address to, uint256 amount);

/**
* @notice Reentrancy guard
*/
modifier nonReentrant() {
require(!reentrant, "StableJoeStaking: reentrant call");
reentrant = true;
_;
reentrant = false;
}

/**
* @notice Construct a new StableJoeStaking contract
* @param _joe The address of the JOE token
*/
constructor(IERC20Upgradeable _joe) initializer {
require(address(_joe) != address(0), "StableJoeStaking: joe can't be address(0)");

joe = _joe;
}

/**
* @notice Initialize a new StableJoeStaking contract
* @dev 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 _joe The address of the JOE token
* @param _feeCollector The address where deposit fees will be sent
* @param _depositFeePercent The deposit fee percent, scalled to 1e18, e.g. 3% is 3e16
*/
function initialize(
IERC20Upgradeable _rewardToken,
IERC20Upgradeable _joe,
address _feeCollector,
uint256 _depositFeePercent
uint88 _depositFeePercent
) external initializer {
__Ownable_init();
require(address(_rewardToken) != address(0), "StableJoeStaking: reward token can't be address(0)");
require(address(_joe) != address(0), "StableJoeStaking: joe can't be address(0)");
require(_feeCollector != address(0), "StableJoeStaking: fee collector can't be address(0)");
require(_depositFeePercent <= 5e17, "StableJoeStaking: max deposit fee can't be greater than 50%");

joe = _joe;
depositFeePercent = _depositFeePercent;
feeCollector = _feeCollector;

isRewardToken[_rewardToken] = true;
rewardTokens.push(_rewardToken);
DEPOSIT_FEE_PERCENT_PRECISION = 1e18;
ACC_REWARD_PER_SHARE_PRECISION = 1e24;
}

/**
* @notice Deposit JOE for reward token allocation
* @param _amount The amount of JOE to deposit
*/
function deposit(uint256 _amount) external {
function deposit(uint256 _amount) external nonReentrant {
UserInfo storage user = userInfo[_msgSender()];

uint256 _fee = _amount.mul(depositFeePercent).div(DEPOSIT_FEE_PERCENT_PRECISION);
Expand All @@ -135,7 +170,7 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
IERC20Upgradeable _token = rewardTokens[i];
updateReward(_token);
_updateReward(_token);

uint256 _previousRewardDebt = user.rewardDebt[_token];
user.rewardDebt[_token] = _newAmount.mul(accRewardPerShare[_token]).div(ACC_REWARD_PER_SHARE_PRECISION);
Expand All @@ -146,15 +181,17 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
.div(ACC_REWARD_PER_SHARE_PRECISION)
.sub(_previousRewardDebt);
if (_pending != 0) {
safeTokenTransfer(_token, _msgSender(), _pending);
_safeTokenTransfer(_token, _msgSender(), _pending);
emit ClaimReward(_msgSender(), address(_token), _pending);
}
}
}

internalJoeBalance = internalJoeBalance.add(_amountMinusFee);
joe.safeTransferFrom(_msgSender(), feeCollector, _fee);
joe.safeTransferFrom(_msgSender(), address(this), _amountMinusFee);

if (_fee > 0) joe.safeTransferFrom(_msgSender(), feeCollector, _fee);
if (_amountMinusFee > 0) joe.safeTransferFrom(_msgSender(), address(this), _amountMinusFee);

emit Deposit(_msgSender(), _amountMinusFee, _fee);
}

Expand Down Expand Up @@ -188,9 +225,11 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
"StableJoeStaking: token can't be added"
);
require(rewardTokens.length < 25, "StableJoeStaking: list of token too big");
require(accRewardPerShare[_rewardToken] == 0, "StableJoeStaking: reward token can't be re-added");

rewardTokens.push(_rewardToken);
isRewardToken[_rewardToken] = true;
updateReward(_rewardToken);

emit RewardTokenAdded(address(_rewardToken));
}

Expand All @@ -200,7 +239,6 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
*/
function removeRewardToken(IERC20Upgradeable _rewardToken) external onlyOwner {
require(isRewardToken[_rewardToken], "StableJoeStaking: token can't be removed");
updateReward(_rewardToken);
isRewardToken[_rewardToken] = false;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
Expand All @@ -217,7 +255,7 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
* @notice Set the deposit fee percent
* @param _depositFeePercent The new deposit fee percent
*/
function setDepositFeePercent(uint256 _depositFeePercent) external onlyOwner {
function setDepositFeePercent(uint88 _depositFeePercent) external onlyOwner {
require(_depositFeePercent <= 5e17, "StableJoeStaking: deposit fee can't be greater than 50%");
uint256 oldFee = depositFeePercent;
depositFeePercent = _depositFeePercent;
Expand Down Expand Up @@ -253,7 +291,7 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
* @notice Withdraw JOE and harvest the rewards
* @param _amount The amount of JOE to withdraw
*/
function withdraw(uint256 _amount) external {
function withdraw(uint256 _amount) external nonReentrant {
UserInfo storage user = userInfo[_msgSender()];
uint256 _previousAmount = user.amount;
require(_amount <= _previousAmount, "StableJoeStaking: withdraw amount exceeds balance");
Expand All @@ -264,7 +302,7 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
if (_previousAmount != 0) {
for (uint256 i; i < _len; i++) {
IERC20Upgradeable _token = rewardTokens[i];
updateReward(_token);
_updateReward(_token);

uint256 _pending = _previousAmount
.mul(accRewardPerShare[_token])
Expand All @@ -273,7 +311,7 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
user.rewardDebt[_token] = _newAmount.mul(accRewardPerShare[_token]).div(ACC_REWARD_PER_SHARE_PRECISION);

if (_pending != 0) {
safeTokenTransfer(_token, _msgSender(), _pending);
_safeTokenTransfer(_token, _msgSender(), _pending);
emit ClaimReward(_msgSender(), address(_token), _pending);
}
}
Expand All @@ -287,10 +325,13 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
/**
* @notice Withdraw without caring about rewards. EMERGENCY ONLY
*/
function emergencyWithdraw() external {
function emergencyWithdraw() external nonReentrant {
UserInfo storage user = userInfo[_msgSender()];

uint256 _amount = user.amount;

require(_amount > 0, "StableJoeStaking: can't withdraw 0");

user.amount = 0;
uint256 _len = rewardTokens.length;
for (uint256 i; i < _len; i++) {
Expand All @@ -303,11 +344,11 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
}

/**
* @notice Update reward variables
* @dev Update reward variables
* Needs to be called before any deposit or withdrawal
* @param _token The address of the reward token
* @dev Needs to be called before any deposit or withdrawal
*/
function updateReward(IERC20Upgradeable _token) public {
function _updateReward(IERC20Upgradeable _token) internal {
require(isRewardToken[_token], "StableJoeStaking: wrong reward token");

uint256 _totalJoe = internalJoeBalance;
Expand All @@ -329,13 +370,30 @@ contract StableJoeStaking is Initializable, OwnableUpgradeable {
}

/**
* @notice Safe token transfer function, just in case if rounding error
* @notice Sweep token to the `_to` address
* @param _token The address of the token to sweep
* @param _to The address that will receive `_token` balance
*/
function sweep(IERC20Upgradeable _token, address _to) external onlyOwner {
require(!isRewardToken[_token] && address(_token) != address(joe), "StableJoeStaking: token can't be swept");

uint256 _balance = _token.balanceOf(address(this));

require(_balance > 0, "StableJoeStaking: can't sweep 0");

_token.safeTransfer(_to, _balance);

emit TokenSwept(address(_token), _to, _balance);
}

/**
* @dev 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`
*/
function safeTokenTransfer(
function _safeTokenTransfer(
IERC20Upgradeable _token,
address _to,
uint256 _amount
Expand Down
30 changes: 30 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[profile.default]
src = 'contracts'
out = 'out'
libs = ["node_modules", "lib"]
test = 'test/foundry'
cache_path = 'forge-cache'

remappings = [
'@ensdomains/=node_modules/@ensdomains/',
'@openzeppelin/=node_modules/@openzeppelin/',
'@solidity-parser/=node_modules/truffle-flattener/node_modules/@solidity-parser/',
'@uniswap/=node_modules/@uniswap/',
'eth-gas-reporter/=node_modules/eth-gas-reporter/',
'hardhat-deploy/=node_modules/hardhat-deploy/',
'hardhat/=node_modules/hardhat/',
]

[rpc_endpoints]
avalanche = "https://api.avax.network/ext/bc/C/rpc"
fuji = "https://api.avax-test.network/ext/bc/C/rpc"
arbitrum = "https://arb1.arbitrum.io/rpc"
bsc = "https://bscrpc.com"

[etherscan]
arbitrum = { key = "${ARBISCAN_API_KEY}", chain = 42161 }
avalanche = { key = "${SNOWTRACE_API_KEY}", chain = 43114 }
arbitrum_goerli = { key = "${ARBISCAN_API_KEY}", chain = 421613 }
fuji = { key = "${SNOWTRACE_API_KEY}", chain = 43113 }

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 73d44e
Loading