// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/math/SafeMathUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "./IStarNode.sol"; interface INFTLogic { function starMeta(uint256 _tokenId) view external returns (uint8, uint256, uint256, uint256); } // import "@nomiclabs/buidler/console.sol"; interface IMigratorChef { function migrate(IERC20Upgradeable token) external returns (IERC20Upgradeable); } // MasterChef is the master of Star. He can make Star and he is a fair guy. // // Note that it's ownable and the owner wields tremendous power. The ownership // will be transferred to a governance smart contract once STAR is sufficiently // distributed and the community can show to govern itself. // // Have fun reading it. Hopefully it's bug-free. God bless. contract MasterChef is Initializable, OwnableUpgradeable { using SafeMathUpgradeable for uint256; using SafeERC20Upgradeable for IERC20Upgradeable; // Info of each user. struct UserInfo { uint256 amount; // How many LP tokens the user has provided. uint256 rewardDebt; // Reward debt. See explanation below. uint256 lastDeposit; uint256 nftAmount; uint256 nftRewardDebt; uint256 nftLastDeposit; // // We do some fancy math here. Basically, any point in time, the amount of STARs // entitled to a user but is pending to be distributed is: // // pending reward = (user.amount * pool.accStarPerShare) - user.rewardDebt // // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: // 1. The pool's `accStarPerShare` (and `lastRewardBlock`) gets updated. // 2. User receives the pending reward sent to his/her address. // 3. User's `amount` gets updated. // 4. User's `rewardDebt` gets updated. } // Info of each pool. struct PoolInfo { IERC20Upgradeable lpToken; // Address of LP token contract. uint256 allocPoint; // How many allocation points assigned to this pool. STARs to distribute per block. uint256 lastRewardBlock; // Last block number that STARs distribution occurs. uint256 accStarPerShare; // Accumulated STARs per share, times 1e12. See below. uint256 extraAmount; // Extra amount of token. users from node or NFT. uint256 fee; } // The STAR TOKEN! IERC20Upgradeable public starToken; // Star node. IStarNode public starNode; // Dev address. address public bonusAddr; // Star NFT. IERC721Upgradeable public starNFT; // NFT logic INFTLogic public nftLogic; // STAR tokens created per block. uint256 public starPerBlock; // Bonus muliplier for early star makers. uint256 public BONUS_MULTIPLIER; // The migrator contract. It has a lot of power. Can only be set through governance (owner). IMigratorChef public migrator; // Info of each pool. PoolInfo[] public poolInfo; // Info of each user that stakes LP tokens. mapping (uint256 => mapping (address => UserInfo)) public userInfo; // Total allocation poitns. Must be the sum of all allocation points in all pools. uint256 public totalAllocPoint; // The block number when STAR mining starts. uint256 public startBlock; // Node user mapping (address => bool) public isNodeUser; // mapping (address => mapping(uint256 => bool)) public userNFT; mapping (uint256 => address) public NFTOwner; mapping (address => uint256[]) public userNFTs; event Deposit(address indexed user, uint256 indexed pid, uint256 amount, bool isNodeUser); event Withdraw(address indexed user, uint256 indexed pid, uint256 amount, bool isNodeUser); event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount, bool isNodeUser); function initialize(address _starToken, address _bonus, address _node, uint256 _starPerBlock, uint256 _startBlock) public initializer { __farm_init(_starToken, _bonus, _node, _starPerBlock, _startBlock); } function __farm_init(address _starToken, address _bonus, address _node, uint256 _starPerBlock, uint256 _startBlock) internal initializer { __Context_init_unchained(); __Ownable_init_unchained(); __farm_init_unchained(_starToken, _bonus, _node, _starPerBlock, _startBlock); } function __farm_init_unchained(address _starToken, address _bonus, address _node, uint256 _starPerBlock, uint256 _startBlock) internal initializer { starToken = IERC20Upgradeable(_starToken); bonusAddr = _bonus; starNode = IStarNode(_node); starPerBlock = _starPerBlock; startBlock = _startBlock; // staking pool poolInfo.push(PoolInfo({ lpToken: IERC20Upgradeable(_starToken), allocPoint: 1000, lastRewardBlock: startBlock, accStarPerShare: 0, extraAmount: 0, fee: 0 })); totalAllocPoint = 1000; } function updateMultiplier(uint256 multiplierNumber) public onlyOwner { BONUS_MULTIPLIER = multiplierNumber; } function poolLength() external view returns (uint256) { return poolInfo.length; } // Add a new lp to the pool. Can only be called by the owner. // XXX DO NOT add the same LP token more than once. Rewards will be messed up if you do. function add(uint256 _allocPoint, IERC20Upgradeable _lpToken, uint256 _fee, bool _withUpdate) public onlyOwner { if (_withUpdate) { massUpdatePools(); } uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; totalAllocPoint = totalAllocPoint.add(_allocPoint); poolInfo.push(PoolInfo({ lpToken: _lpToken, allocPoint: _allocPoint, lastRewardBlock: lastRewardBlock, accStarPerShare: 0, extraAmount: 0, fee: _fee })); updateStakingPool(); } // Update the given pool's STAR allocation point. Can only be called by the owner. function set(uint256 _pid, uint256 _allocPoint, uint256 _fee, bool _withUpdate) public onlyOwner { if (_withUpdate) { massUpdatePools(); } totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(_allocPoint); uint256 prevAllocPoint = poolInfo[_pid].allocPoint; poolInfo[_pid].allocPoint = _allocPoint; poolInfo[_pid].fee = _fee; if (prevAllocPoint != _allocPoint) { updateStakingPool(); } } function updateStakingPool() internal { uint256 length = poolInfo.length; uint256 points = 0; for (uint256 pid = 1; pid < length; ++pid) { points = points.add(poolInfo[pid].allocPoint); } if (points != 0) { points = points.div(3); totalAllocPoint = totalAllocPoint.sub(poolInfo[0].allocPoint).add(points); poolInfo[0].allocPoint = points; } } // Set the migrator contract. Can only be called by the owner. function setMigrator(IMigratorChef _migrator) public onlyOwner { migrator = _migrator; } // Migrate lp token to another lp contract. Can be called by anyone. We trust that migrator contract is good. function migrate(uint256 _pid) public { require(address(migrator) != address(0), "migrate: no migrator"); PoolInfo storage pool = poolInfo[_pid]; IERC20Upgradeable lpToken = pool.lpToken; uint256 bal = lpToken.balanceOf(address(this)); lpToken.safeApprove(address(migrator), bal); IERC20Upgradeable newLpToken = migrator.migrate(lpToken); require(bal == newLpToken.balanceOf(address(this)), "migrate: bad"); pool.lpToken = newLpToken; } // Return reward multiplier over the given _from to _to block. function getMultiplier(uint256 _from, uint256 _to) public view returns (uint256) { return _to.sub(_from).mul(BONUS_MULTIPLIER); } // View function to see pending STARs on frontend. function pendingStar(uint256 _pid, address _user) external view returns (uint256) { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][_user]; uint256 accStarPerShare = pool.accStarPerShare; uint256 lpSupply = pool.lpToken.balanceOf(address(this)).add(pool.extraAmount); if (block.number > pool.lastRewardBlock && lpSupply != 0) { uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number); uint256 starReward = multiplier.mul(starPerBlock).mul(pool.allocPoint).div(totalAllocPoint); accStarPerShare = accStarPerShare.add(starReward.mul(1e12).div(lpSupply)); } (uint256 _selfGain, ) = starNode.nodeGain(); uint256 _useramount = user.amount.sub(user.nftAmount); uint256 _amountGain = _useramount.add(_useramount.mul(_selfGain).div(100)); // return user.amount.mul(accStarPerShare).div(1e12).sub(user.rewardDebt); return _amountGain.mul(accStarPerShare).div(1e12).sub(user.rewardDebt).add(user.nftRewardDebt); } // Update reward variables for all pools. Be careful of gas spending! function massUpdatePools() public { uint256 length = poolInfo.length; for (uint256 pid = 0; pid < length; ++pid) { updatePool(pid); } } // Update reward variables of the given pool to be up-to-date. function updatePool(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; if (block.number <= pool.lastRewardBlock) { return; } uint256 lpSupply = pool.lpToken.balanceOf(address(this)).add(pool.extraAmount); if (lpSupply == 0) { pool.lastRewardBlock = block.number; return; } uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number); uint256 starReward = multiplier.mul(starPerBlock).mul(pool.allocPoint).div(totalAllocPoint); starToken.safeTransfer(bonusAddr, starReward.div(10)); pool.accStarPerShare = pool.accStarPerShare.add(starReward.mul(1e12).div(lpSupply)); pool.lastRewardBlock = block.number; } // Deposit LP tokens to MasterChef for STAR allocation. function deposit(uint256 _pid, uint256 _amount) public { //require (_pid != 0, 'withdraw STAR by unstaking'); //if (_pid == 0) require(userNFTs[_msgSender()].length == 0, "nft user"); PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][_msgSender()]; updatePool(_pid); (uint256 _selfGain, uint256 _parentGain) = starNode.nodeGain(); uint256 _useramount = user.amount.sub(user.nftAmount); uint256 _amountGain = _useramount.add(_useramount.mul(_selfGain).div(100)); if (_amountGain > 0) { uint256 pending = _amountGain.mul(pool.accStarPerShare).div(1e12).sub(user.rewardDebt).add(user.nftRewardDebt); if(pending > 0) { if (user.lastDeposit > block.timestamp.sub(604800)) { pending = pending.mul(90).div(100); starToken.safeTransfer(bonusAddr, pending.mul(10).div(100)); } starToken.safeTransfer(_msgSender(), pending); starNode.settleNode(_msgSender(), user.amount); } } if (_amount > 0) { pool.lpToken.safeTransferFrom(_msgSender(), address(this), _amount); user.amount = user.amount.add(_amount); uint256 _extraAmount = _amount.mul(_selfGain.add(_parentGain)).div(100); pool.extraAmount = pool.extraAmount.add(_extraAmount); } _amountGain = user.amount.add(user.amount.mul(_selfGain).div(100)); user.rewardDebt = _amountGain.mul(pool.accStarPerShare).div(1e12); emit Deposit(_msgSender(), _pid, _amount, isNodeUser[_msgSender()]); } function testdeposit(uint256 _pid, uint256 _amount) public { //require (_pid != 0, 'withdraw STAR by unstaking'); //if (_pid == 0) require(userNFTs[_msgSender()].length == 0, "nft user"); PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][_msgSender()]; updatePool(_pid); (uint256 _selfGain, uint256 _parentGain) = starNode.nodeGain(); uint256 _useramount = user.amount.sub(user.nftAmount); uint256 _amountGain = _useramount.add(_useramount.mul(_selfGain).div(100)); if (_amountGain > 0) { uint256 pending = _amountGain.mul(pool.accStarPerShare).div(1e12).add(user.nftRewardDebt).sub(user.rewardDebt); user.amount = user.amount.sub(_amount); } } // Withdraw LP tokens from MasterChef. function withdraw(uint256 _pid, uint256 _amount) public { //require (_pid != 0, 'withdraw STAR by unstaking'); //if (_pid == 0) require(userNFTs[_msgSender()].length == 0, "nft user"); PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][_msgSender()]; uint256 _useramount = user.amount.sub(user.nftAmount); require(_useramount >= _amount, "withdraw: amount error"); updatePool(_pid); (uint256 _selfGain, uint256 _parentGain) = starNode.nodeGain(); uint256 _amountGain = _useramount.add(_useramount.mul(_selfGain).div(100)); uint256 pending = _amountGain.mul(pool.accStarPerShare).div(1e12).add(user.nftRewardDebt).sub(user.rewardDebt); if(pending > 0) { if (user.lastDeposit > block.timestamp.sub(604800)) { pending = pending.mul(90).div(100); starToken.safeTransfer(bonusAddr, pending.mul(10).div(100)); } starToken.safeTransfer(_msgSender(), pending); starNode.settleNode(_msgSender(), _useramount); } if(_amount > 0) { user.amount = user.amount.sub(_amount); uint256 _extraAmount = _amount.mul(_selfGain.add(_parentGain)).div(100); pool.extraAmount = pool.extraAmount.sub(_extraAmount); pool.lpToken.safeTransfer(_msgSender(), _amount); } _amountGain = user.amount.add(user.amount.mul(_selfGain).div(100)); user.rewardDebt = _amountGain.mul(pool.accStarPerShare).div(1e12).add(user.nftRewardDebt); emit Withdraw(_msgSender(), _pid, _amount, isNodeUser[_msgSender()]); } // Stake Star NFT to MasterChef function enterStakingNFT(uint256 _tokenId) public { PoolInfo storage pool = poolInfo[0]; UserInfo storage user = userInfo[0][_msgSender()]; require(starNFT.ownerOf(_tokenId) == _msgSender(), "error NFT user"); //require(userNFTs[_msgSender()].length > 0, "star token user"); updatePool(0); (uint256 _selfGain, uint256 _parentGain) = starNode.nodeGain(); uint256 _amountGain = user.amount.add(user.amount.mul(_selfGain).div(100)); uint256 _nftAmountGain = user.nftAmount.add(user.nftAmount.mul(_selfGain).div(100)); if (_nftAmountGain > 0) { uint256 pending = _nftAmountGain.mul(pool.accStarPerShare).div(1e12).sub(user.nftRewardDebt); if(pending > 0) { if (user.nftLastDeposit > block.timestamp.sub(604800)) { pending = pending.mul(90).div(100); starToken.safeTransfer(bonusAddr, pending.mul(10).div(100)); } starToken.safeTransfer(_msgSender(), pending); starNode.settleNode(_msgSender(), user.nftAmount); } } if (_tokenId > 0) { starNFT.transferFrom(_msgSender(), address(this), _tokenId); userNFTs[_msgSender()].push(_tokenId); (, , uint256 _price, uint256 _multi) = nftLogic.starMeta(_tokenId); uint256 _amount = _price.mul(_multi).div(100); uint256 _extraAmount = _amount.add(_amount.mul(_selfGain.add(_parentGain)).div(100)); pool.extraAmount = pool.extraAmount.add(_extraAmount); user.amount = user.amount.add(_amount); user.nftAmount = user.nftAmount.add(_amount); _amountGain = user.amount.add(user.amount.mul(_selfGain).div(100)); _nftAmountGain = user.nftAmount.add(user.nftAmount.mul(_selfGain).div(100)); } user.rewardDebt = _amountGain.mul(pool.accStarPerShare).div(1e12); user.nftRewardDebt = _nftAmountGain.mul(pool.accStarPerShare).div(1e12); //user.rewardDebt = user.rewardDebt.add(user.nftRewardDebt); emit Deposit(_msgSender(), 0, _tokenId, isNodeUser[_msgSender()]); } // Withdraw Star NFT from STAKING. function leaveStakingNFT(uint256 _tokenId) public { //PoolInfo storage pool = poolInfo[0]; UserInfo storage user = userInfo[0][_msgSender()]; require(userNFTs[_msgSender()].length > 0, "no NFT"); updatePool(0); (uint256 _selfGain, uint256 _parentGain) = starNode.nodeGain(); uint256 _amountGain = user.amount.add(user.amount.mul(_selfGain).div(100)); uint256 _nftAmountGain = user.nftAmount.add(user.nftAmount.mul(_selfGain).div(100)); uint256 pending = _nftAmountGain.mul(poolInfo[0].accStarPerShare).div(1e12).sub(user.nftRewardDebt); if(pending > 0) { if (user.nftLastDeposit > block.timestamp.sub(604800)) { pending = pending.mul(90).div(100); starToken.safeTransfer(bonusAddr, pending.mul(10).div(100)); } starToken.safeTransfer(_msgSender(), pending); starNode.settleNode(_msgSender(), user.nftAmount); } if (_tokenId > 0) { uint256[] storage _userNFTs = userNFTs[_msgSender()]; for (uint256 i = 0; i < _userNFTs.length; i ++) { if(_userNFTs[i] == _tokenId) { (, , uint256 _price, uint256 _multi) = nftLogic.starMeta(_tokenId); uint256 _amount = _price.mul(_multi).div(100); if(_amount > 0) { uint256 _self_parentGain = _selfGain.add(_parentGain); uint256 _extraAmount = _amount.add(_amount.mul(_self_parentGain).div(100)); poolInfo[0].extraAmount = poolInfo[0].extraAmount.sub(_extraAmount); user.amount = user.amount.sub(_amount); user.nftAmount = user.nftAmount.sub(_amount); _userNFTs[i] = _userNFTs[_userNFTs.length - 1]; _userNFTs.pop(); } starNFT.transferFrom(address(this), _msgSender(), _tokenId); _amountGain = user.amount.add(user.amount.mul(_selfGain).div(100)); _nftAmountGain = user.nftAmount.add(user.nftAmount.mul(_selfGain).div(100)); user.rewardDebt = _amountGain.mul(poolInfo[0].accStarPerShare).div(1e12); user.nftRewardDebt = _nftAmountGain.mul(poolInfo[0].accStarPerShare).div(1e12); emit Withdraw(_msgSender(), 0, _amount, isNodeUser[_msgSender()]); break; } } } } function getStakingNFTAmount(address _user) view public returns (uint256) { return userNFTs[_user].length; } // Withdraw without caring about rewards. EMERGENCY ONLY. function emergencyWithdraw(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][_msgSender()]; pool.lpToken.safeTransfer(_msgSender(), user.amount); emit EmergencyWithdraw(_msgSender(), _pid, user.amount, isNodeUser[_msgSender()]); user.amount = 0; user.rewardDebt = 0; } function setBonus(address _addr) external onlyOwner { require(address(0) != _addr, "bonus address can not be address 0"); bonusAddr = _addr; } function setStarNFT(address _addr) external onlyOwner { require(address(0) != _addr, "NFT address can not be address 0"); starNFT = IERC721Upgradeable(_addr); } function setNFTLogic(address _addr) external onlyOwner { require(address(0) != _addr, "logic address can not be address 0"); nftLogic = INFTLogic(_addr); } function regNodeUser(address _user) external onlyNode { require(address(0) != _user, ''); isNodeUser[_user] = true; } function setNode(address _node) public onlyOwner { require(address(0) != _node, 'node can not be address 0'); starNode = IStarNode(_node); } modifier onlyNode() { require(_msgSender() == address(starNode), "not node"); _; } }