// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.6.12;
library SafeMath {
function mul(uint a, uint b) internal pure returns (uint) {
uint c = a * b;
require(a == 0 || c / a == b);
return c;
}
function div(uint a, uint b) internal pure returns (uint) {
require(b > 0);
uint c = a / b;
require(a == b * c + a % b);
return c;
}
function sub(uint a, uint b) internal pure returns (uint) {
require(b <= a);
return a - b;
}
function add(uint a, uint b) internal pure returns (uint) {
uint c = a + b;
require(c >= a);
return c;
}
function max64(uint64 a, uint64 b) internal pure returns (uint64) {
return a >= b ? a : b;
}
function min64(uint64 a, uint64 b) internal pure returns (uint64) {
return a < b ? a : b;
}
function max256(uint a, uint b) internal pure returns (uint) {
return a >= b ? a : b;
}
function min256(uint a, uint b) internal pure returns (uint) {
return a < b ? a : b;
}
}
interface ISupplyController {
function mint(address token, address owner, uint amount) external;
}
interface IADXToken {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address spender) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);
function supplyController() external view returns (ISupplyController);
}
contract ADXLoyaltyPoolToken {
using SafeMath for uint;
// ERC20 stuff
// Constants
string public constant name = "AdEx Loyalty";
uint8 public constant decimals = 18;
string public symbol = "ADX-LOYALTY";
// Mutable variables
uint public totalSupply;
mapping(address => uint) balances;
mapping(address => mapping(address => uint)) allowed;
// EIP 2612
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;
// ERC20 events
event Approval(address indexed owner, address indexed spender, uint amount);
event Transfer(address indexed from, address indexed to, uint amount);
function balanceOf(address owner) external view returns (uint balance) {
return balances[owner];
}
function transfer(address to, uint amount) external returns (bool success) {
require(to != address(this), 'BAD_ADDRESS');
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint amount) external returns (bool success) {
balances[from] = balances[from].sub(amount);
allowed[from][msg.sender] = allowed[from][msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
emit Transfer(from, to, amount);
return true;
}
function approve(address spender, uint amount) external returns (bool success) {
allowed[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function allowance(address owner, address spender) external view returns (uint remaining) {
return allowed[owner][spender];
}
// EIP 2612
function permit(address owner, address spender, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'DEADLINE_EXPIRED');
bytes32 digest = keccak256(abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonces[owner]++, deadline))
));
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'INVALID_SIGNATURE');
allowed[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
// Inner
function innerMint(address owner, uint amount) internal {
totalSupply = totalSupply.add(amount);
balances[owner] = balances[owner].add(amount);
// Because of https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#transfer-1
emit Transfer(address(0), owner, amount);
}
function innerBurn(address owner, uint amount) internal {
totalSupply = totalSupply.sub(amount);
balances[owner] = balances[owner].sub(amount);
emit Transfer(owner, address(0), amount);
}
// Pool functionality
event LogSetGovernance(address indexed addr, bool hasGovt, uint time);
event LogSetIncentive(uint incentive, uint time);
IADXToken public ADXToken;
uint public incentivePerTokenPerAnnum;
uint public lastMintTime;
uint public maxTotalADX;
mapping (address => bool) public governance;
constructor(IADXToken token, uint incentive, uint cap) public {
ADXToken = token;
incentivePerTokenPerAnnum = incentive;
maxTotalADX = cap;
governance[msg.sender] = true;
lastMintTime = block.timestamp;
// EIP 2612
uint chainId;
assembly {
chainId := chainid()
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
emit LogSetGovernance(msg.sender, true, block.timestamp);
emit LogSetIncentive(incentive, block.timestamp);
}
// Governance functions
function setGovernance(address addr, bool hasGovt) external {
require(governance[msg.sender], 'NOT_GOVERNANCE');
governance[addr] = hasGovt;
emit LogSetGovernance(addr, hasGovt, block.timestamp);
}
// This doesn't trigger a mint because otherwise we risk of being unable to setIncentive to 0
// if minting is impossible
// It's the better tradeoff to make - and the issue of front-running mintIncnetive with setIncentive(0) can
// be solved by timelocking the governance
function setIncentive(uint newIncentive) external {
require(governance[msg.sender], 'NOT_GOVERNANCE');
incentivePerTokenPerAnnum = newIncentive;
lastMintTime = block.timestamp;
emit LogSetIncentive(newIncentive, block.timestamp);
}
function setSymbol(string calldata newSymbol) external {
require(governance[msg.sender], 'NOT_GOVERNANCE');
symbol = newSymbol;
}
function setMaxTotalADX(uint newMaxTotalADX) external {
require(governance[msg.sender], 'NOT_GOVERNANCE');
maxTotalADX = newMaxTotalADX;
}
// Pool stuff
// There are a few notable items in how minting works
// 1) if ADX is sent to the LoyaltyPool in-between mints, it will calculate the incentive as if this amount
// has been there the whole time since the last mint
// 2) Compounding is happening when mint is called, so essentially when entities enter/leave/trigger it manually
function toMint() external view returns (uint) {
if (block.timestamp <= lastMintTime) return 0;
uint totalADX = ADXToken.balanceOf(address(this));
return (block.timestamp - lastMintTime)
.mul(totalADX)
.mul(incentivePerTokenPerAnnum)
.div(365 days * 10e17);
}
function shareValue() external view returns (uint) {
if (totalSupply == 0) return 0;
return ADXToken.balanceOf(address(this))
.add(this.toMint())
.mul(10e17)
.div(totalSupply);
}
function mintIncentive() public {
if (incentivePerTokenPerAnnum == 0) return;
uint amountToMint = this.toMint();
if (amountToMint == 0) return;
lastMintTime = block.timestamp;
ADXToken.supplyController().mint(address(ADXToken), address(this), amountToMint);
}
function enter(uint256 amount) external {
// Please note that minting has to be in the beginning so that we take it into account
// when using ADXToken.balanceOf()
// Minting makes an external call but it's to a trusted contract (ADXToken)
mintIncentive();
uint totalADX = ADXToken.balanceOf(address(this));
require(totalADX.add(amount) <= maxTotalADX, 'REACHED_MAX_TOTAL_ADX');
// The totalADX == 0 check here should be redudnant; the only way to get totalSupply to a nonzero val is by adding ADX
if (totalSupply == 0 || totalADX == 0) {
innerMint(msg.sender, amount);
} else {
uint256 newShares = amount.mul(totalSupply).div(totalADX);
innerMint(msg.sender, newShares);
}
require(ADXToken.transferFrom(msg.sender, address(this), amount));
}
function leaveInner(uint256 shares) internal {
uint256 totalADX = ADXToken.balanceOf(address(this));
uint256 adxAmount = shares.mul(totalADX).div(totalSupply);
innerBurn(msg.sender, shares);
require(ADXToken.transfer(msg.sender, adxAmount));
}
function leave(uint256 shares) external {
mintIncentive();
leaveInner(shares);
}
// Guarantees ADX can be taken out even if minting is failing
function emergencyLeave(uint256 shares) external {
leaveInner(shares);
}
}
{
"compilationTarget": {
"ADXLoyaltyPoolToken.sol": "ADXLoyaltyPoolToken"
},
"evmVersion": "istanbul",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"contract IADXToken","name":"token","type":"address"},{"internalType":"uint256","name":"incentive","type":"uint256"},{"internalType":"uint256","name":"cap","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"bool","name":"hasGovt","type":"bool"},{"indexed":false,"internalType":"uint256","name":"time","type":"uint256"}],"name":"LogSetGovernance","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"incentive","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"time","type":"uint256"}],"name":"LogSetIncentive","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"ADXToken","outputs":[{"internalType":"contract IADXToken","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PERMIT_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"remaining","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"emergencyLeave","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"enter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"governance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"incentivePerTokenPerAnnum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastMintTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"leave","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"maxTotalADX","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintIncentive","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"bool","name":"hasGovt","type":"bool"}],"name":"setGovernance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newIncentive","type":"uint256"}],"name":"setIncentive","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newMaxTotalADX","type":"uint256"}],"name":"setMaxTotalADX","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newSymbol","type":"string"}],"name":"setSymbol","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"shareValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"toMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]