-
Notifications
You must be signed in to change notification settings - Fork 24
/
ADXLoyaltyPool.sol
270 lines (235 loc) · 9.38 KB
/
ADXLoyaltyPool.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.6.12;
import "./libs/SafeMath.sol";
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);
}
}
interface IChainlinkSimple {
function latestAnswer() external view returns (uint);
}
// NOTE: If this needs to be upgraded, we just deploy a new instance and remove the governance rights
// of the old instance and set rights for the new instance
contract ADXLoyaltyPoolIncentiveController {
IChainlinkSimple public constant ADXUSDOracle = IChainlinkSimple(0xA3eAeC3AB66048E6F3Cf23D81881a3fcd9A3D2ED);
ADXLoyaltyPoolToken public immutable loyaltyPool;
constructor(ADXLoyaltyPoolToken lpt) public {
loyaltyPool = lpt;
}
function adjustIncentive() external {
// Mint the current incurred incentive before changing the rate,
// otherwise new rate would be applied for the entire period since the last mint
loyaltyPool.mintIncentive();
// @TODO if after a certain point of issuance, set to 0
// or if it's after some time
// Reset the rate based on the price from the Chainlink oracle
uint price = ADXUSDOracle.latestAnswer();
require(price > 0, 'INVALID_ANSWER');
if (price < 0.05*10**8) {
loyaltyPool.setIncentive(uint(0.10*10**18));
} else if (price < 0.10*10**18) {
loyaltyPool.setIncentive(uint(0.15*10**18));
} else if (price < 0.20*10**18) {
loyaltyPool.setIncentive(uint(0.25*10**18));
} else if (price < 0.30*10**18) {
loyaltyPool.setIncentive(uint(0.30*10**18));
} else if (price < 0.50*10**18) {
loyaltyPool.setIncentive(uint(0.35*10**18));
} else if (price < 1.00*10**18) {
loyaltyPool.setIncentive(uint(0.40*10**18));
} else if (price < 2.00*10**18) {
loyaltyPool.setIncentive(uint(0.45*10**18));
} else {
loyaltyPool.setIncentive(uint(0.50*10**18));
}
}
}