399 lines
14 KiB
Solidity
399 lines
14 KiB
Solidity
/*
|
|
|
|
Copyright 2020 DODO ZOO.
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
*/
|
|
pragma solidity 0.6.9;
|
|
pragma experimental ABIEncoderV2;
|
|
|
|
import {IERC20} from "../intf/IERC20.sol";
|
|
import {SafeMath} from "../lib/SafeMath.sol";
|
|
import {DecimalMath} from "../lib/DecimalMath.sol";
|
|
import {InitializableOwnable} from "../lib/InitializableOwnable.sol";
|
|
import {SafeERC20} from "../lib/SafeERC20.sol";
|
|
import {IDODOApproveProxy} from "../SmartRoute/DODOApproveProxy.sol";
|
|
|
|
interface IGovernance {
|
|
function getLockedvDODO(address account) external view returns (uint256);
|
|
}
|
|
|
|
interface IDODOCirculationHelper {
|
|
// Locked vDOOD not counted in circulation
|
|
function getCirculation() external view returns (uint256);
|
|
|
|
function getDodoWithdrawFeeRatio() external view returns (uint256);
|
|
}
|
|
|
|
contract vDODOToken is InitializableOwnable {
|
|
using SafeMath for uint256;
|
|
|
|
// ============ Storage(ERC20) ============
|
|
|
|
string public name = "vDODO Token";
|
|
string public symbol = "vDODO";
|
|
uint8 public decimals = 18;
|
|
mapping(address => mapping(address => uint256)) internal _ALLOWED_;
|
|
|
|
// ============ Storage ============
|
|
|
|
address public immutable _DODO_TOKEN_;
|
|
address public immutable _DODO_APPROVE_PROXY_;
|
|
address public immutable _DODO_TEAM_;
|
|
address public _DOOD_GOV_;
|
|
address public _DODO_CIRCULATION_HELPER_;
|
|
|
|
bool public _CAN_TRANSFER_;
|
|
|
|
// staking reward parameters
|
|
uint256 public _DODO_PER_BLOCK_;
|
|
uint256 public constant _SUPERIOR_RATIO_ = 10**17; // 0.1
|
|
uint256 public constant _DODO_RATIO_ = 100; // 100
|
|
uint256 public _DODO_FEE_BURN_RATIO_;
|
|
|
|
// accounting
|
|
uint112 public alpha = 10**18; // 1
|
|
uint112 public _TOTAL_BLOCK_DISTRIBUTION_;
|
|
uint32 public _LAST_REWARD_BLOCK_;
|
|
|
|
uint256 public _TOTAL_BLOCK_REWARD_;
|
|
uint256 public _TOTAL_STAKING_POWER_;
|
|
mapping(address => UserInfo) public userInfo;
|
|
|
|
struct UserInfo {
|
|
uint128 stakingPower;
|
|
uint128 superiorSP;
|
|
address superior;
|
|
uint256 credit;
|
|
}
|
|
|
|
// ============ Events ============
|
|
|
|
event MintVDODO(address user, address superior, uint256 mintDODO);
|
|
event RedeemVDODO(address user, uint256 receiveDODO, uint256 burnDODO, uint256 feeDODO);
|
|
event SetCantransfer(bool allowed);
|
|
|
|
event ChangePerReward(uint256 dodoPerBlock);
|
|
event UpdateDODOFeeBurnRatio(uint256 dodoFeeBurnRatio);
|
|
|
|
event Transfer(address indexed from, address indexed to, uint256 amount);
|
|
event Approval(address indexed owner, address indexed spender, uint256 amount);
|
|
|
|
// ============ Modifiers ============
|
|
|
|
modifier canTransfer() {
|
|
require(_CAN_TRANSFER_, "vDODOToken: not allowed transfer");
|
|
_;
|
|
}
|
|
|
|
modifier balanceEnough(address account, uint256 amount) {
|
|
require(availableBalanceOf(account) >= amount, "vDODOToken: available amount not enough");
|
|
_;
|
|
}
|
|
|
|
// ============ Constructor ============
|
|
|
|
constructor(
|
|
address dodoGov,
|
|
address dodoToken,
|
|
address dodoApproveProxy,
|
|
address dodoTeam
|
|
) public {
|
|
_DOOD_GOV_ = dodoGov;
|
|
_DODO_TOKEN_ = dodoToken;
|
|
_DODO_APPROVE_PROXY_ = dodoApproveProxy;
|
|
_DODO_TEAM_ = dodoTeam;
|
|
}
|
|
|
|
// ============ Ownable Functions ============`
|
|
|
|
function setCantransfer(bool allowed) public onlyOwner {
|
|
_CAN_TRANSFER_ = allowed;
|
|
emit SetCantransfer(allowed);
|
|
}
|
|
|
|
function changePerReward(uint256 dodoPerBlock) public onlyOwner {
|
|
_updateAlpha();
|
|
_DODO_PER_BLOCK_ = dodoPerBlock;
|
|
emit ChangePerReward(dodoPerBlock);
|
|
}
|
|
|
|
function updateDODOFeeBurnRatio(uint256 dodoFeeBurnRatio) public onlyOwner {
|
|
_DODO_FEE_BURN_RATIO_ = dodoFeeBurnRatio;
|
|
emit UpdateDODOFeeBurnRatio(_DODO_FEE_BURN_RATIO_);
|
|
}
|
|
|
|
function updateDODOCirculationHelper(address helper) public onlyOwner {
|
|
_DODO_CIRCULATION_HELPER_ = helper;
|
|
}
|
|
|
|
function updateGovernance(address governance) public onlyOwner {
|
|
_DOOD_GOV_ = governance;
|
|
}
|
|
|
|
function emergencyWithdraw() public onlyOwner {
|
|
uint256 dodoBalance = IERC20(_DODO_TOKEN_).balanceOf(address(this));
|
|
IERC20(_DODO_TOKEN_).transfer(_OWNER_, dodoBalance);
|
|
}
|
|
|
|
// ============ Mint & Redeem & Donate ============
|
|
|
|
function mint(uint256 dodoAmount, address superiorAddress) public {
|
|
require(
|
|
superiorAddress != address(0) && superiorAddress != msg.sender,
|
|
"vDODOToken: Superior INVALID"
|
|
);
|
|
require(dodoAmount > 0, "vDODOToken: must mint greater than 0");
|
|
|
|
UserInfo storage user = userInfo[msg.sender];
|
|
|
|
if (user.superior == address(0)) {
|
|
require(
|
|
superiorAddress == _DODO_TEAM_ || userInfo[superiorAddress].superior != address(0),
|
|
"vDODOToken: INVALID_SUPERIOR_ADDRESS"
|
|
);
|
|
user.superior = superiorAddress;
|
|
}
|
|
|
|
_updateAlpha();
|
|
|
|
IDODOApproveProxy(_DODO_APPROVE_PROXY_).claimTokens(
|
|
_DODO_TOKEN_,
|
|
msg.sender,
|
|
address(this),
|
|
dodoAmount
|
|
);
|
|
|
|
uint256 newStakingPower = DecimalMath.divFloor(dodoAmount, alpha);
|
|
|
|
_mint(user, newStakingPower);
|
|
|
|
emit MintVDODO(msg.sender, superiorAddress, dodoAmount);
|
|
}
|
|
|
|
function redeem(uint256 vdodoAmount, bool all) public balanceEnough(msg.sender, vdodoAmount) {
|
|
_updateAlpha();
|
|
UserInfo storage user = userInfo[msg.sender];
|
|
|
|
uint256 dodoAmount;
|
|
uint256 stakingPower;
|
|
|
|
if (all) {
|
|
stakingPower = uint256(user.stakingPower).sub(DecimalMath.divFloor(user.credit, alpha));
|
|
dodoAmount = DecimalMath.mulFloor(stakingPower, alpha);
|
|
} else {
|
|
dodoAmount = vdodoAmount.mul(_DODO_RATIO_);
|
|
stakingPower = DecimalMath.divFloor(dodoAmount, alpha);
|
|
}
|
|
|
|
_redeem(user, stakingPower);
|
|
|
|
(uint256 dodoReceive, uint256 burnDodoAmount, uint256 withdrawFeeDodoAmount) =
|
|
getWithdrawResult(dodoAmount);
|
|
IERC20(_DODO_TOKEN_).transfer(msg.sender, dodoReceive);
|
|
if (burnDodoAmount > 0) {
|
|
IERC20(_DODO_TOKEN_).transfer(address(0), burnDodoAmount);
|
|
}
|
|
if (withdrawFeeDodoAmount > 0) {
|
|
alpha = uint112(
|
|
uint256(alpha).add(
|
|
DecimalMath.divFloor(withdrawFeeDodoAmount, _TOTAL_STAKING_POWER_)
|
|
)
|
|
);
|
|
}
|
|
|
|
emit RedeemVDODO(msg.sender, dodoReceive, burnDodoAmount, withdrawFeeDodoAmount);
|
|
}
|
|
|
|
function donate(uint256 dodoAmount) public {
|
|
IDODOApproveProxy(_DODO_APPROVE_PROXY_).claimTokens(
|
|
_DODO_TOKEN_,
|
|
msg.sender,
|
|
address(this),
|
|
dodoAmount
|
|
);
|
|
alpha = uint112(
|
|
uint256(alpha).add(DecimalMath.divFloor(dodoAmount, _TOTAL_STAKING_POWER_))
|
|
);
|
|
}
|
|
|
|
function preDepositedBlockReward(uint256 dodoAmount) public {
|
|
IDODOApproveProxy(_DODO_APPROVE_PROXY_).claimTokens(
|
|
_DODO_TOKEN_,
|
|
msg.sender,
|
|
address(this),
|
|
dodoAmount
|
|
);
|
|
_TOTAL_BLOCK_REWARD_ = _TOTAL_BLOCK_REWARD_.add(dodoAmount);
|
|
}
|
|
|
|
// ============ ERC20 Functions ============
|
|
|
|
function totalSupply() public view returns (uint256 vDODOSupply) {
|
|
uint256 totalDODO = IERC20(_DODO_TOKEN_).balanceOf(address(this));
|
|
(,uint256 curDistribution) = getLatestAlpha();
|
|
uint256 actualDODO = totalDODO.sub(_TOTAL_BLOCK_REWARD_.sub(curDistribution.add(_TOTAL_BLOCK_DISTRIBUTION_)));
|
|
vDODOSupply = actualDODO / _DODO_RATIO_;
|
|
}
|
|
|
|
function balanceOf(address account) public view returns (uint256 vDODOAmount) {
|
|
vDODOAmount = dodoBalanceOf(account) / _DODO_RATIO_;
|
|
}
|
|
|
|
function transfer(address to, uint256 vDODOAmount) public returns (bool) {
|
|
_updateAlpha();
|
|
_transfer(msg.sender, to, vDODOAmount);
|
|
return true;
|
|
}
|
|
|
|
function approve(address spender, uint256 vDODOAmount) canTransfer public returns (bool) {
|
|
_ALLOWED_[msg.sender][spender] = vDODOAmount;
|
|
emit Approval(msg.sender, spender, vDODOAmount);
|
|
return true;
|
|
}
|
|
|
|
function transferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 vDODOAmount
|
|
) public returns (bool) {
|
|
require(vDODOAmount <= _ALLOWED_[from][msg.sender], "ALLOWANCE_NOT_ENOUGH");
|
|
_updateAlpha();
|
|
_transfer(from, to, vDODOAmount);
|
|
_ALLOWED_[from][msg.sender] = _ALLOWED_[from][msg.sender].sub(vDODOAmount);
|
|
return true;
|
|
}
|
|
|
|
function allowance(address owner, address spender) public view returns (uint256) {
|
|
return _ALLOWED_[owner][spender];
|
|
}
|
|
|
|
// ============ Helper Functions ============
|
|
|
|
function getLatestAlpha() public view returns (uint256 newAlpha, uint256 curDistribution) {
|
|
curDistribution = _DODO_PER_BLOCK_ * (block.number - _LAST_REWARD_BLOCK_);
|
|
if (_TOTAL_STAKING_POWER_ > 0) {
|
|
newAlpha = uint256(alpha).add(DecimalMath.divFloor(curDistribution, _TOTAL_STAKING_POWER_));
|
|
} else {
|
|
newAlpha = alpha;
|
|
}
|
|
}
|
|
|
|
function availableBalanceOf(address account) public view returns (uint256 vDODOAmount) {
|
|
if (_DOOD_GOV_ == address(0)) {
|
|
vDODOAmount = balanceOf(account);
|
|
} else {
|
|
uint256 lockedvDODOAmount = IGovernance(_DOOD_GOV_).getLockedvDODO(account);
|
|
vDODOAmount = balanceOf(account).sub(lockedvDODOAmount);
|
|
}
|
|
}
|
|
|
|
function dodoBalanceOf(address account) public view returns (uint256 dodoAmount) {
|
|
UserInfo memory user = userInfo[account];
|
|
(uint256 newAlpha,) = getLatestAlpha();
|
|
uint256 nominalDodo = DecimalMath.mulFloor(uint256(user.stakingPower), newAlpha);
|
|
if(nominalDodo > user.credit) {
|
|
dodoAmount = nominalDodo - user.credit;
|
|
}else {
|
|
dodoAmount = 0;
|
|
}
|
|
}
|
|
|
|
function getWithdrawResult(uint256 dodoAmount)
|
|
public
|
|
view
|
|
returns (
|
|
uint256 dodoReceive,
|
|
uint256 burnDodoAmount,
|
|
uint256 withdrawFeeDodoAmount
|
|
)
|
|
{
|
|
uint256 feeRatio =
|
|
IDODOCirculationHelper(_DODO_CIRCULATION_HELPER_).getDodoWithdrawFeeRatio();
|
|
|
|
withdrawFeeDodoAmount = DecimalMath.mulFloor(dodoAmount, feeRatio);
|
|
dodoReceive = dodoAmount.sub(withdrawFeeDodoAmount);
|
|
|
|
burnDodoAmount = DecimalMath.mulFloor(withdrawFeeDodoAmount, _DODO_FEE_BURN_RATIO_);
|
|
withdrawFeeDodoAmount = withdrawFeeDodoAmount.sub(burnDodoAmount);
|
|
}
|
|
|
|
function getDODOWithdrawFeeRatio() public view returns (uint256 feeRatio) {
|
|
feeRatio = IDODOCirculationHelper(_DODO_CIRCULATION_HELPER_).getDodoWithdrawFeeRatio();
|
|
}
|
|
|
|
function getSuperior(address account) public view returns (address superior) {
|
|
return userInfo[account].superior;
|
|
}
|
|
|
|
// ============ Internal Functions ============
|
|
|
|
function _updateAlpha() internal {
|
|
(uint256 newAlpha, uint256 curDistribution) = getLatestAlpha();
|
|
uint256 newTotalDistribution = curDistribution.add(_TOTAL_BLOCK_DISTRIBUTION_);
|
|
require(newAlpha <= uint112(-1) && newTotalDistribution <= uint112(-1), "OVERFLOW");
|
|
alpha = uint112(newAlpha);
|
|
_TOTAL_BLOCK_DISTRIBUTION_ = uint112(newTotalDistribution);
|
|
_LAST_REWARD_BLOCK_ = uint32(block.number);
|
|
}
|
|
|
|
function _mint(UserInfo storage to, uint256 stakingPower) internal {
|
|
require(stakingPower <= uint128(-1), "OVERFLOW");
|
|
UserInfo storage superior = userInfo[to.superior];
|
|
uint256 superiorIncreSP = DecimalMath.mulFloor(stakingPower, _SUPERIOR_RATIO_);
|
|
uint256 superiorIncreCredit = DecimalMath.mulFloor(superiorIncreSP, alpha);
|
|
|
|
to.stakingPower = uint128(uint256(to.stakingPower).add(stakingPower));
|
|
to.superiorSP = uint128(uint256(to.superiorSP).add(superiorIncreSP));
|
|
|
|
superior.stakingPower = uint128(uint256(superior.stakingPower).add(superiorIncreSP));
|
|
superior.credit = uint128(uint256(superior.credit).add(superiorIncreCredit));
|
|
|
|
_TOTAL_STAKING_POWER_ = _TOTAL_STAKING_POWER_.add(stakingPower).add(superiorIncreSP);
|
|
}
|
|
|
|
function _redeem(UserInfo storage from, uint256 stakingPower) internal {
|
|
from.stakingPower = uint128(uint256(from.stakingPower).sub(stakingPower));
|
|
|
|
// superior decrease sp = min(stakingPower*0.1, from.superiorSP)
|
|
uint256 superiorDecreSP = DecimalMath.mulFloor(stakingPower, _SUPERIOR_RATIO_);
|
|
superiorDecreSP = from.superiorSP <= superiorDecreSP ? from.superiorSP : superiorDecreSP;
|
|
from.superiorSP = uint128(uint256(from.superiorSP).sub(superiorDecreSP));
|
|
|
|
UserInfo storage superior = userInfo[from.superior];
|
|
uint256 creditSP = DecimalMath.divFloor(superior.credit, alpha);
|
|
|
|
if (superiorDecreSP >= creditSP) {
|
|
superior.credit = 0;
|
|
superior.stakingPower = uint128(uint256(superior.stakingPower).sub(creditSP));
|
|
} else {
|
|
superior.credit = uint128(
|
|
uint256(superior.credit).sub(DecimalMath.mulFloor(superiorDecreSP, alpha))
|
|
);
|
|
superior.stakingPower = uint128(uint256(superior.stakingPower).sub(superiorDecreSP));
|
|
}
|
|
|
|
_TOTAL_STAKING_POWER_ = _TOTAL_STAKING_POWER_.sub(stakingPower).sub(superiorDecreSP);
|
|
}
|
|
|
|
function _transfer(
|
|
address from,
|
|
address to,
|
|
uint256 vDODOAmount
|
|
) internal canTransfer balanceEnough(from, vDODOAmount) {
|
|
require(from != address(0), "transfer from the zero address");
|
|
require(to != address(0), "transfer to the zero address");
|
|
require(from != to, "transfer from same with to");
|
|
|
|
uint256 stakingPower = DecimalMath.divFloor(vDODOAmount * _DODO_RATIO_, alpha);
|
|
|
|
UserInfo storage fromUser = userInfo[from];
|
|
UserInfo storage toUser = userInfo[to];
|
|
|
|
_redeem(fromUser, stakingPower);
|
|
_mint(toUser, stakingPower);
|
|
|
|
emit Transfer(from, to, vDODOAmount);
|
|
}
|
|
}
|