// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "profile-nft-gamification/contracts/PancakeProfile.sol";
import "./interfaces/IIFOV2.sol";
import "pancake-cake-vault/contracts/IFOPool.sol";
import "pancake-cake-vault/contracts/test/CakeToken.sol";
import "pancake-cake-vault/contracts/test/SyrupBar.sol";
import "pancake-cake-vault/contracts/test/MasterChef.sol";
/**
* @title my cake ifo v3 implement
* @author lrqstudy
* @notice
*/
contract MyIFOInitializableV3 is IIFOV2,ReentrancyGuard,Ownerable {
/**
* 使用了openzeppelin中的safemath,将其方法用于uint256中,这个是之前的版本,现在的新的solidity版本不需要
*/
using SafeMath for uint256;
using SafeERC20 for IERC20;
// 定义了一个池子数的常量,公开的和私人的
uint8 public constant NUMBER_POOLS = 2;
// ifo工厂的地址
address public immutable IFO_FACTORY;
uint256 public MAX_BUFFER_BLOCKS;
//用于参与本次ifo 的资金
IERC20 public lpToken;
//用于公开售卖的token地址
IERC20 public offeringToken;
//pancake平台信息,只有注册信息才可以参与ifo
PancakeProfile public pancakeProfile;
IFOPool public ifoPool;
bool public isInitialized;
//ifo 开始区块号
uint256 public startBlock;
//ifo 结束区块号
uint256 public endBlock;
//本次ifo的活动id
uint256 public campaignId;
//参与达到要求后有多少积分
uint256 public numberPoints;
//最低积分要求
uint256 public thresholdPoints;
//总共要售卖的token数量
uint256 public totalTokensOffered;
PoolCharacteristics[NUMBER_POOLS] private _poolInformation;
//定义了一个mapping结构,key是地址,value是状态,描述用户是否已经领取积分
mapping(address => bool) private _hasClaimedPoints;
/**
* @dev 定了一个潜逃的mapping结构, 第一层key是用户地址,value是一个 key为poolid, value为userinfo的结构mapping结构,
用于记录当前用户参与ifo的信息,参与了几个池子。
*/
mapping(address => mapping(uint8 => UserInfo)) private _userInfo;
//定义了一个mapping,key是用户参与地址, value是用户累计参与的金额。
mapping(address => uint256) public userAccumulateDeposits;
/**
* 定义了一个池子的结构
* 包括了六个对象, 募集目标金额,共计提供多少token,每个用户多少险恶,是否有参与费用,总共的募集资金,总共产生多少费用
* @param amountLP
* @param amountOfferingToken
*/
struct PoolCharacteristics {
uint256 raisingAmountPool;
uint256 offeringAmountPool;
uint256 limitPerUserInLP;
bool hasTax;
uint256 totalAmountPool;
uint256 sumTaxesOverflow;
}
/**
* 定义了一个userinfo的结构,存储用户参与了多少资金,是否领取的状态
* @param amountLP
* @param amountOfferingToken
*/
struct UserInfo {
uint256 amountPool;
bool claimedPool;
}
event AdminWithdraw(uint256 amountLP, uint256 amountOfferingToken);
event AdminTokenRecovery(address tokenAddress, uint256 amountTokens);
event Deposit(address indexed user, uint256 amount, uint8 indexed pid);
event Harvest(address indexed user, uint256 offeringAmount,uint256 excessAmount, uint8 indexed pid);
event NewStartAndEndBlocks(uint256 startBlock,uint256 endBlock);
event PointParametersSet(uint256 campaignId, uint256 numberPoints, uint256 thresholdPoints);
event PoolParametersSet(uint256 offeringAmountPool, uint256 raisingAmountPool, uint8 pid);
/**
* 定义了一个修饰符,合约以及代理合约对象不能参与,只有普通的eta地址可以参与
*/
modifier notContract() {
require(!_isContract(msg.sender)," contract not allowed");
require(msg.sender == tx.origin," proxy contract not allowed");
_;
}
/**
* 定义了一个构造函数,指定IFO_Factory对象地址
*/
constructor() public {
IFO_FACTORY = msg.sender;
}
/**
* 定义了一个初始化函数,用于初始化设置当前ifo事件
* @param _lpToken 用于参与的资金token地址
* @param _offeringToken 用于售卖的token地址
* @param _pancakeProfileAddress pancakefile的合约地址
* @param _startBlock 售卖开始的区块编号
* @param _endBlock 售卖结束的区块编号
* @param _maxBufferBlocks 最大缓冲区块数,用于控制ifo的冗余时间
* @param _adminAddress 管理员地址,用于提取当前资金
* @param _ifoPoolAddress ifo池子地址,用于接收
*/
function initialize(
address _lpToken,
address _offeringToken,
address _pancakeProfileAddress,
uint256 _startBlock,
uint256 _endBlock,
uint256 _maxBufferBlocks,
address _adminAddress,
address _ifoPoolAddress
) public {
require(!isInitialized,"operations: Already initialized");
require(msg.sender == IFO_FACTORY, "Operation: only for Factory");
//make this contract initialized
isInitialized = true;
lpToken = IERC20(_lpToken);
offeringToken = IERC20(_offeringToken);
/**
* 这里无法理解,为什么 PancakeProfile创建了对象,怎么就一个参数就可以呢?
* PancakeProfile的构造函数有四个参数。
* @param _amount
* @param _pid
*/
pancakeProfile = PancakeProfile(_pancakeProfileAddress);
ifoPool = IFOPool(_ifoPoolAddress);
startBlock = _startBlock;
endBlock = _endBlock;
MAX_BUFFER_BLOCKS = _maxBufferBlocks;
transferOwnership(_adminAddress);//这个将当前合约转移给管理员
}
/**
* 用于处理用户参与的请求函数
* @param _amount 本次参与金额,
* @param _pid 本次参与的池子编号
*/
function depositPool(uint256 _amount,uint8 _pid) external override nonReentrant notContract {
//用户必须要注册并获得符合规定的pancake账户及头像,从而参与ifo
require(pancakeProfile.getUserStatus(msg.sender),"Deposit: Must have an active profile");
//当前只有两个池子,如果池子id不小于2,说明参数有问题
require(_pid < NUMBER_POOLS," Deposit: Non valid pool id");
//必须是要先设定好融资额度,以及提供的token数量,用户才能参与进来。
require(_poolInformation[_pid].offeringAmount > 0 && _poolInformation[_pid].raisingAmountPool>0,"Deposit: Pool not set");
//只有到了开始的区块号才能参与,用户不能参与
require(block.number > startBlock,"Deposit: Too early");
//过了结束参与的区块号,不能参与
require(block.number< endBlock,"Deposit: Too Late");
//如果当前ifo合约中的对应的售卖余额,大于已经提供的,那么说明设置的ifo 数据有问题。
require(offeringToken.balanceOf(address(this))>= totalTokensOffered,"Deposit: Tokens not deposited properly");
//用户可以参与的额度,用于存入的总金额不得大于该额度
uint256 ifoCredit = ifoPool.getUserCredit(msg.sender);
require(userAccumulateDeposits[msg.sender].add(_amount)<= ifoCredit,"Not enough IFO credit left");
//如果这些校验都通过了,那么将用于提供的cake数量调用转账函数转到当前合约地址中
lpToken.safeTransferFrom(address(msg.sender),address(this),_amount);
//将用户成功参与到金额以及池子信息设置到mapping中,方便后续查看
_userInfo[msg.sender][_pid].amountPool = _userInfo[msg.sender][_pid].amountPool.add(_amount);
//如果每个用户有参与限额,那么用户的参与金额不得大于这个限额
if (_poolInformation[_pid].limitPerUserInLP > 0) {
require(_userInfo[msg.sender][_pid].amountPool <= _poolInformation[_pid].limitPerUserInLPimi,"Deposit: new amount over user limt");
}
//将用户本次参与的金额更新到总的参与池子信息中,方便后期查看
_poolInformation[_pid].totalAmountPool = _poolInformation[_pid].totalAmountPool.add(_amount);
//记录用于累计参与到ifo金额数量。
userAccumulateDeposits[msg.sender] = userAccumulateDeposits[msg.sender].add(_amount);
//发出用户参与ifo的事件。报告参与人,参与金额,参与项目id
emit Deposit(msg.sender,_amount, _pid);
}
/**
* 定义了用户在参与结束后领取ifo的处理请求函数。 ifo结束后,用户点击claim由当前函数进行处理。
* @param _pid 项目id
*/
function harvestPool(uint8 _pid) external override nonReentrant notContract{
//必须结束ifo才能领取
require(block.number > endBlock,"Harvest: Too early");
//项目id必须是0 或者1
require(_pid < NUMBER_POOLS,"Harvest:Non valid pool id");
//只有参与过的人才可以领取
require(_userInfo[msg.sender][_pid].amountPool>0,"Harvest: Did not participate ");
//如果已经领取了,则不能再次领取
require(!_userInfo[msg.sender][_pid].claimedPool,"Harvest: already done");
//调用领取参与积分的函数
_claimPoints(msg.sender);
//将用户对应的项目id的已经领取状态设置为true
_userInfo[msg.sender][_pid].claimedPool = true;
//计算用户已经参与的金额,需要退还的金额,以及用户参与的费用
(
uint256 offeringTokenAmount,
uint256 refundingTokenAmount,
uint256 userTaxOverflow
) = _calculateOfferingAndRefundingAmountPool(msg.sender,_pid);
if(userTaxOverflow > 0){
_poolInformation[_pid].sumtaxedOverflow = _poolInformation[_pid].sumTaxesOverflow.add(userTaxOverflow);
}
//将用户本次获得的token数转给当前用户
if(offeringTokenAmount > 0) {
//疑问: 这个是如何设置用户的领取周期的 TODO
offeringToken.safeTransfer(address(msg.sender),offeringTokenAmount);
}
//将用户超额部分多cake退还给用户
if(refundingTokenAmount>0){
lpToken.safeTransfer(address(msg.sender),refundingTokenAmount);
}
//调用通知事件,告知领取人,提供的金额,以及退换的金额。
emit Harvest(msg.sender, offeringTokenAmount,refundingTokenAmount,_pid);
}
/**
* 供ifo项目方领取募集资金以及剩余的出售token,仅仅当初设置为合约所有人地址才能调用
* @param _lpAmount 领取募集资金额度
* @param _offerAmount 领取剩余出售token数量
*/
function finalWithdraw(uint256 _lpAmount,uint256 _offerAmount) external override onlyOwner {
//领取数必须小于等于当前合约中剩余的cake数
require(_lpAmount <= lpToken.balanceOf(address(this)),"Operations: Not enough LP tokens");
//领取的token数必须小于等于当前合约剩余的token数
require(_offerAmount <= offeringToken.balanceOf(this),"Operations Not enough offering tokens");
if(_lpAmount > 0){
//当前合约中的cake转给项目方
lpToken.safeTransfer(address(msg.sender),_lpAmount);
}
if(_offerAmount >0){
//将当前合约中剩余的未出售token转给项目方
offeringToken.safeTransfer(address(msg.sender),_offerAmount);
}
//发送管理员领取事件
emit AdminWithdraw(_lpAmount,_offerAmount);
}
/**
* 给合约所有者恢复错误转入的token,且必须是除了cake和当前待出售的合约。
* @param _tokenAddress
* @param _tokenAmount
*/
function recoverWrongTokens(address _tokenAddress,uint256 _tokenAmount) external onlyOwner {
//要转移的token不能是cake
require(_tokenAddress != address(lpToken), "can not be LP token");
//要恢复的token也不能是项目方的出售token
require(_tokenAddress != address(offeringToken),"recover: cannot be offering token");
//将当前合约对象强制转换为ERC20对象,从而能调用erc20对象的safeTransfer方法,将当前合约中的当前token转给项目方
IERC20(_tokenAddress).safeTransfer(address(msg.sender),_tokenAmount);
//发布管理员恢复误操作token
emit AdminTokenRecovery(_tokenAddress,_tokenAmount);
}
/**
* 定义了一个设置启动参数的功能,给管理员调用
* @param _offeringAmountPool 待出售token数量
* @param _raisingAmountPool 共计募集资金
* @param _limitPerUserInLP 每个参与人参与限额
* @param _hasTax 是否有费用
* @param _pid 池子id
*/
function setPool(
uint256 _offeringAmountPool,
uint256 _raisingAmountPool,
uint256 _limitPerUserInLP,
bool _hasTax,
uint8 _pid
) external override onlyOwner {
//只有开始的区块之前才可以调用,在开始后不允许调用。
require(block.number < startBlock," Operations: IFO has started");
//池子id必须是指定的几个。
require(_pid < NUMBER_POOLS," Operations: Pool does not exist");
//将传递的参数赋值给poolInformation状态变量
_poolInformation[_pid].offeringAmountPool = _offeringAmountPool;
_poolInformation[_pid].raisingAmountPool = _raisingAmountPool;
_poolInformation[_pid].limitPerUserInLP = _limitPerUserInLP;
_poolInformation[_pid].hasTax = _hasTax;
uint256 tokensDistributedAcrossPools;
for(uint8 i =0; i< NUMBER_POOLS;i++){
// 统计两个池子共提供的token数
tokensDistributedAcrossPools = tokensDistributedAcrossPools.add(_poolInformation[i].offeringAmountPool);
}
totalTokensOffered = tokensDistributedAcrossPools;
//发送池子参数设定的事件
emit PoolParametersSet(_offeringAmountPool,_raisingAmountPool,_pid);
}
/**
* 更新积分参数,只能在区块结束之前调用
* @param _campaignId ifo活动id
* @param _numberPoints 积分数
* @param _thresholdPoints 积分门槛
*/
function updatePointParameters(
uint256 _campaignId,
uint256 _numberPoints,
uint256 _thresholdPoints
) external override onlyOwner {
require(block.number < endBlock,"operation IFO has ended");
numberPoints = _numberPoints;
campaignId = _campaignId;
thresholdPoints = _thresholdPoints;
// 发布积分更新时间
emit PointParametersSet(campaignId,numberPoints,thresholdPoints);
}
/**
* 更新开始结束区块号,从而重新设定开始结束时间。
* @param _startBlock 新的开始区块
* @param _endBlock 新的结束区块
*/
function updateStartAndEndBlocks(uint256 _startBlock,uint256 _endBlock) external onlyOwner {
//新的结束区块只能在当前区块延后 的MAX_BUFFER_BLOCKS 中
require(_endBlock < (block.number + MAX_BUFFER_BLOCKS), "Operations: EndBlock too far");
//当前操作必须要在开始之前操作
require(block.number < startBlock,"Operations: IFO has started");
//新设置的结束区块必须要大于开始区块
require(_startBlock < _endBlock, "Operations: New startBlock must be lower than new endBlock");
//新设置的区块必须要在当前系统区块之后。
require(block.number < _startBlock,"Operations: New startblock must be higher than current block");
startBlock = _startBlock;
endBlock = _endBlock;
//发布新开始结束时间区块事件
emit NewStartAndEndBlocks(_startBlock,_endBlock);
}
/**
* 定义一个访问ifo池子的方法。
* @param _pid
* @return
* @return
* @return
* @return
* @return
* @return
*/
function viewPoolInformation(uint256 _pid) external view override returns (
uint256,
uint256,
uint256,
bool,
uint256,
uint256
){
return (
_poolInformation[_pid].raisingAmountPool,
_poolInformation[_pid].offeringAmountPool,
_poolInformation[_pid].limitPerUserInLP,
_poolInformation[_pid].hasTax,
_poolInformation[_pid].totalAmountPool,
_poolInformation[_pid].sumTaxesOverflow
);
}
/**
* 定义一个通过池子id获得当前池子溢出税,如果没有税,则返回0,如果有税,则返回计算税的结果
* @param _pid
*/
function viewPoolTaxRateOverflow(uint256 _pid) external view override returns (uint256){
if(!_poolInformation[_pid].hasTax){
return 0;
}
return _calculateTaxOverflow(_poolInformation[_pid].totalAmountPool,_poolInformation[_pid].raisingAmountPool);
}
/**
*
* @param _user
* @param _pids
*/
function viewUserAllocationPools(address _user,uint8[] calldata _pids) external view override returns (uint256[] memory){
uint256[] memory allocationPools = new uint256[](_pids.length);
for(uint8 i=0; i< _pids.length;i++){
allocationPools[i] = _getUserAllocationPool(_user,_pids[i]);
}
return allocationPools;
}
/**
* 定义了一个访问用户数据的方法。 返回值有两个数组。
* 第一个是用户参与每个池子对应的金额数,第二个数组是每个用户是否领取了对应的token
* @param _user
* @param _pids
* @return
* @return
*/
function viewUserInfo(address _user, uint8[] calldata _pids) external view override returns(uint256[] memory, bool[] memory){
uint256[] memory amountPools = new uint256[](_pids.length);
bool[] memory statusPools = new bool[](_pids.length);
for(uint8 i = 0; i< NUMBER_POOLS; i++){
amountPools[i] = _userInfo[_user][i].amountPool;
statusPools[i] = _userInfo[_user][i].claimedPool;
}
return (amountPools,statusPools);
}
/**
* 获取并返回用户在每个池子中参与金额以及退款的金额数
*/
function viewUserOfferingAndRefundingAmountsForPools(address _user,uint8[] calldata _pids) external
view override returns (uint256[3][]memory){
uint256[3][] memory amountPools = new uint256[3][](_pids.length);
for(uint8 i =0; i<_pids.length;i++){
uint256 userOfferingAmountPool;
uint256 userRefundingAmountPool;
uint256 userTaxAmountPool;
if(_poolInformation[_pids[i]].raisingAmountPool > 0){
(
userOfferingAmount,
userRefundingAmountPool,
userTaxAmountPool
) = _calculateOfferingAndRefundingAmountsPool(_user,_pids[i]);
}
amountPools[i] = [userOfferingAmountPool,userRefundingAmountPool,userTaxAmountPool];
}
return amountPools;
}
/**
* @notice It calculates the tax overflow given the raisingAmountPool and the totalAmountPool.
* @dev 100,000,000,000 means 0.1 (10%) / 1 means 0.0000000000001 (0.0000001%) / 1,000,000,000,000 means 1 (100%)
* @return It returns the tax percentage
*/
function _calculateTaxOverflow(uint256 _totalAmountPool, uint256 _raisingAmountPool)
internal
pure
returns (uint256)
{
uint256 ratioOverflow = _totalAmountPool.div(_raisingAmountPool);
if (ratioOverflow >= 1500) {
return 500000000; // 0.05%
} else if (ratioOverflow >= 1000) {
return 1000000000; // 0.1%
} else if (ratioOverflow >= 500) {
return 2000000000; // 0.2%
} else if (ratioOverflow >= 250) {
return 2500000000; // 0.25%
} else if (ratioOverflow >= 100) {
return 3000000000; // 0.3%
} else if (ratioOverflow >= 50) {
return 5000000000; // 0.5%
} else {
return 10000000000; // 1%
}
}
/**
* @notice It calculates the offering amount for a user and the number of LP tokens to transfer back.
* @param _user: user address
* @param _pid: pool id
* @return {uint256, uint256, uint256} It returns the offering amount, the refunding amount (in LP tokens),
* and the tax (if any, else 0)
*/
function _calculateOfferingAndRefundingAmountsPool(address _user, uint8 _pid)
internal
view
returns (
uint256,
uint256,
uint256
)
{
uint256 userOfferingAmount;
uint256 userRefundingAmount;
uint256 taxAmount;
if (_poolInformation[_pid].totalAmountPool > _poolInformation[_pid].raisingAmountPool) {
// Calculate allocation for the user
uint256 allocation = _getUserAllocationPool(_user, _pid);
// Calculate the offering amount for the user based on the offeringAmount for the pool
userOfferingAmount = _poolInformation[_pid].offeringAmountPool.mul(allocation).div(1e12);
// Calculate the payAmount
uint256 payAmount = _poolInformation[_pid].raisingAmountPool.mul(allocation).div(1e12);
// Calculate the pre-tax refunding amount
userRefundingAmount = _userInfo[_user][_pid].amountPool.sub(payAmount);
// Retrieve the tax rate
if (_poolInformation[_pid].hasTax) {
uint256 taxOverflow = _calculateTaxOverflow(
_poolInformation[_pid].totalAmountPool,
_poolInformation[_pid].raisingAmountPool
);
// Calculate the final taxAmount
taxAmount = userRefundingAmount.mul(taxOverflow).div(1e12);
// Adjust the refunding amount
userRefundingAmount = userRefundingAmount.sub(taxAmount);
}
} else {
userRefundingAmount = 0;
taxAmount = 0;
// _userInfo[_user] / (raisingAmount / offeringAmount)
userOfferingAmount = _userInfo[_user][_pid].amountPool.mul(_poolInformation[_pid].offeringAmountPool).div(
_poolInformation[_pid].raisingAmountPool
);
}
return (userOfferingAmount, userRefundingAmount, taxAmount);
}
/**
* 定义了一个供内部调用的领取积分函数
* @param _user
*/
function _claimPoints(address _user) internal {
if(_hasClaimedPoints[_user]){
return;//如果当前用户已经领取积分,那么不执行后续命令? 疑问,为什么这里不报错呢,而仅仅是返回。
}
uint256 sumPools;
for(uint8 i=0; i<NUMBER_POOLS;i++){
sumPools = sumPools.add(_userInfo[msg.sender][i].amountPool);//统计用户参与的总的金额数
}
//如果用户参与的总金额数,超过了限额,则满足领取积分条件
if(sumPools > thresholdPoints) {
_hasClaimedPoints = true;//将已经领取积分的状态设置为true
pancakeProfile.increaseUserPoints(msg.sender, numberPoints,campaignId);//调用pancakeProfile合约执行给用户增加积分的函数功能。
}
}
/**
* 获取用户参与量占当前总量的比例
* @param _user
* @param _pid
*/
function _getUserAllocationPool(address _user,uint8 _pid) internal view returns(uint256) {
if(_poolInformation[_pid].totalAmountPool > 0) {
return _userInfo[_user][_pid].amountPool.mul(1e18).div(_poolInformation[_pid]).totalAmountPool.mul(1e6);
}
return 0;
}
/**
* 定义一个判断是否是合约地址的内部方法
* @param _addr
*/
function _isContract(address _addr) internal view returns (bool){
uint256 size;
assembly {
size := extcodesize(_addr)
}
return size >0;
}
}