@juicedollar/jusd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +356 -0
- package/contracts/Equity.sol +457 -0
- package/contracts/JuiceDollar.sol +363 -0
- package/contracts/Leadrate.sol +79 -0
- package/contracts/MintingHubV2/MintingHub.sol +445 -0
- package/contracts/MintingHubV2/Position.sol +810 -0
- package/contracts/MintingHubV2/PositionFactory.sol +69 -0
- package/contracts/MintingHubV2/PositionRoller.sol +159 -0
- package/contracts/MintingHubV2/interface/IMintingHub.sol +26 -0
- package/contracts/MintingHubV2/interface/IPosition.sol +90 -0
- package/contracts/MintingHubV2/interface/IPositionFactory.sol +20 -0
- package/contracts/Savings.sol +141 -0
- package/contracts/SavingsVaultJUSD.sol +140 -0
- package/contracts/StablecoinBridge.sol +109 -0
- package/contracts/StartUSD.sol +16 -0
- package/contracts/gateway/CoinLendingGateway.sol +223 -0
- package/contracts/gateway/FrontendGateway.sol +224 -0
- package/contracts/gateway/MintingHubGateway.sol +87 -0
- package/contracts/gateway/SavingsGateway.sol +51 -0
- package/contracts/gateway/interface/ICoinLendingGateway.sol +73 -0
- package/contracts/gateway/interface/IFrontendGateway.sol +49 -0
- package/contracts/gateway/interface/IMintingHubGateway.sol +12 -0
- package/contracts/impl/ERC3009.sol +171 -0
- package/contracts/interface/IJuiceDollar.sol +54 -0
- package/contracts/interface/ILeadrate.sol +7 -0
- package/contracts/interface/IReserve.sol +9 -0
- package/contracts/interface/ISavingsJUSD.sol +49 -0
- package/contracts/test/FreakToken.sol +25 -0
- package/contracts/test/Math.sol +339 -0
- package/contracts/test/MockEquity.sol +15 -0
- package/contracts/test/PositionExpirationTest.sol +75 -0
- package/contracts/test/PositionRollingTest.sol +65 -0
- package/contracts/test/TestFlashLoan.sol +84 -0
- package/contracts/test/TestFlashLoanGateway.sol +49 -0
- package/contracts/test/TestMathUtil.sol +40 -0
- package/contracts/test/TestToken.sol +45 -0
- package/contracts/test/TestWcBTC.sol +35 -0
- package/contracts/utils/MathUtil.sol +61 -0
- package/dist/index.d.mts +8761 -0
- package/dist/index.d.ts +8761 -0
- package/dist/index.js +11119 -0
- package/dist/index.mjs +11073 -0
- package/exports/abis/MintingHubV2/PositionFactoryV2.ts +90 -0
- package/exports/abis/MintingHubV2/PositionRoller.ts +183 -0
- package/exports/abis/MintingHubV2/PositionV2.ts +999 -0
- package/exports/abis/core/CoinLendingGateway.ts +427 -0
- package/exports/abis/core/Equity.ts +1286 -0
- package/exports/abis/core/FrontendGateway.ts +906 -0
- package/exports/abis/core/JuiceDollar.ts +1366 -0
- package/exports/abis/core/MintingHubGateway.ts +865 -0
- package/exports/abis/core/SavingsGateway.ts +559 -0
- package/exports/abis/core/SavingsVaultJUSD.ts +920 -0
- package/exports/abis/utils/ERC20.ts +310 -0
- package/exports/abis/utils/ERC20PermitLight.ts +520 -0
- package/exports/abis/utils/Leadrate.ts +175 -0
- package/exports/abis/utils/MintingHubV2.ts +682 -0
- package/exports/abis/utils/Ownable.ts +76 -0
- package/exports/abis/utils/Savings.ts +453 -0
- package/exports/abis/utils/StablecoinBridge.ts +209 -0
- package/exports/abis/utils/StartUSD.ts +315 -0
- package/exports/abis/utils/UniswapV3Pool.ts +638 -0
- package/exports/address.config.ts +48 -0
- package/exports/index.ts +28 -0
- package/package.json +87 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
pragma solidity ^0.8.0;
|
|
4
|
+
|
|
5
|
+
import {JuiceDollar} from "./JuiceDollar.sol";
|
|
6
|
+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
7
|
+
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
|
|
8
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
9
|
+
import {ERC3009} from "./impl/ERC3009.sol";
|
|
10
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
11
|
+
import {IReserve} from "./interface/IReserve.sol";
|
|
12
|
+
import {MathUtil} from "./utils/MathUtil.sol";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @title Equity
|
|
16
|
+
* @notice If the JuiceDollar system was a bank, this contract would represent the equity on its balance sheet.
|
|
17
|
+
* Like a corporation, the owners of the equity capital are the shareholders, or in this case the holders
|
|
18
|
+
* of Juice Protocol (JUICE) tokens. Anyone can mint additional JUICE tokens by adding JuiceDollars to the
|
|
19
|
+
* reserve pool. Also, JUICE tokens can be redeemed for JuiceDollars again, with only a one-block delay to
|
|
20
|
+
* prevent flash loan attacks.
|
|
21
|
+
*
|
|
22
|
+
* Furthermore, the JUICE shares come with voting power based on holding duration. Anyone that held at least
|
|
23
|
+
* 2% of the holding-period-weighted reserve pool shares gains veto power and can veto new proposals.
|
|
24
|
+
*/
|
|
25
|
+
contract Equity is ERC20Permit, ERC3009, MathUtil, IReserve, ERC165 {
|
|
26
|
+
/**
|
|
27
|
+
* The VALUATION_FACTOR determines the market cap of the reserve pool shares relative to the equity reserves.
|
|
28
|
+
* The following always holds: Market Cap = Valuation Factor * Equity Reserve = Price * Supply
|
|
29
|
+
*
|
|
30
|
+
* In the absence of fees, profits and losses, the variables grow as follows when JUICE tokens are minted:
|
|
31
|
+
*
|
|
32
|
+
* | Reserve | Market Cap | Price | Supply |
|
|
33
|
+
* | 1_000 | 10_000 | 0.001 | 10_000_000 |
|
|
34
|
+
* | 100_000_000 | 1_000_000_000 | 31.62 | 31_622_777 |
|
|
35
|
+
* | 10_000_000_000_000 |100_000_000_000_000 | 1_000_000 | 100_000_000 |
|
|
36
|
+
*
|
|
37
|
+
* i.e., the supply is proportional to the tenth root of the reserve and the price is proportional to
|
|
38
|
+
* the ninth power of the tenth root (or Reserve^0.9). When profits accumulate or losses materialize,
|
|
39
|
+
* the reserve, market cap, and price adjust according to their respective power laws.
|
|
40
|
+
*/
|
|
41
|
+
uint32 public constant VALUATION_FACTOR = 10;
|
|
42
|
+
|
|
43
|
+
uint256 private constant MINIMUM_EQUITY = 1_000 * ONE_DEC18;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @notice The quorum in basis points. 100 is 1%.
|
|
47
|
+
*/
|
|
48
|
+
uint32 private constant QUORUM = 200;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @notice The number of digits to store the average holding time of share tokens.
|
|
52
|
+
*/
|
|
53
|
+
uint8 private constant TIME_RESOLUTION_BITS = 20;
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
JuiceDollar public immutable JUSD;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @dev To track the total number of votes we need to know the number of votes at the anchor time and when the
|
|
60
|
+
* anchor time was. This is (hopefully) stored in one 256 bit slot, with the anchor time taking 64 Bits and
|
|
61
|
+
* the total vote count 192 Bits. Given the sub-second resolution of 20 Bits, the implicit assumption is
|
|
62
|
+
* that the timestamp can always be stored in 44 Bits (i.e., it does not exceed half a million years). Further,
|
|
63
|
+
* given 18 decimals (about 60 Bits), this implies that the total supply cannot exceed
|
|
64
|
+
* 192 - 60 - 44 - 20 = 68 Bits
|
|
65
|
+
* Here, we are also safe, as 68 Bits would imply more than a trillion outstanding shares. In fact,
|
|
66
|
+
* a limit of about 2**36 shares (that's about 2**96 Bits when taking into account the decimals) is imposed
|
|
67
|
+
* when minting. This means that the maximum supply is billions of shares, which could only be reached in
|
|
68
|
+
* a scenario with hyperinflation, in which case the stablecoin is worthless anyway.
|
|
69
|
+
*/
|
|
70
|
+
uint192 private totalVotesAtAnchor; // Total number of votes at the anchor time
|
|
71
|
+
uint64 private totalVotesAnchorTime; // 44 Bits for the time stamp, 20 Bit sub-second resolution
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @notice Keeping track of who delegated votes to whom.
|
|
75
|
+
* Note that delegation does not mean you cannot vote / veto anymore; it just means that the delegate can
|
|
76
|
+
* benefit from your votes when invoking a veto. Circular delegations are valid but do not help when voting.
|
|
77
|
+
*/
|
|
78
|
+
mapping(address owner => address delegate) public delegates;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @notice A time stamp in the past such that: votes = balance * (time passed since anchor was set).
|
|
82
|
+
*/
|
|
83
|
+
mapping(address owner => uint64 timestamp) private voteAnchor; // 44 bits for time stamp, 20 sub-second resolution
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @notice Block number when an address last received JUICE shares (via mint, transfer, or any inbound path).
|
|
87
|
+
* Used to prevent same-block redemptions and flash loan attacks.
|
|
88
|
+
*/
|
|
89
|
+
mapping(address owner => uint256 blockNumber) public lastInboundBlock;
|
|
90
|
+
|
|
91
|
+
event Delegation(address indexed from, address indexed to); // indicates a delegation
|
|
92
|
+
event Trade(address who, int256 amount, uint256 totPrice, uint256 newprice); // amount pos or neg for mint or redemption
|
|
93
|
+
|
|
94
|
+
error NotQualified();
|
|
95
|
+
error NotMinter();
|
|
96
|
+
error InsufficientEquity();
|
|
97
|
+
error TooManyShares();
|
|
98
|
+
error TotalSupplyExceeded();
|
|
99
|
+
error SameBlockRedemption();
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
JuiceDollar JUSD_
|
|
103
|
+
)
|
|
104
|
+
ERC20Permit("Juice Protocol")
|
|
105
|
+
ERC20("Juice Protocol", "JUICE")
|
|
106
|
+
{
|
|
107
|
+
JUSD = JUSD_;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @notice Prevents same-block redemptions to protect against flash loan and atomic MEV attacks.
|
|
112
|
+
* @param owner The address whose shares are being redeemed
|
|
113
|
+
*/
|
|
114
|
+
modifier notSameBlock(address owner) {
|
|
115
|
+
if (block.number <= lastInboundBlock[owner]) revert SameBlockRedemption();
|
|
116
|
+
_;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @notice Returns the price of one JUICE in JUSD with 18 decimals precision.
|
|
121
|
+
*/
|
|
122
|
+
function price() public view returns (uint256) {
|
|
123
|
+
uint256 equity = JUSD.equity();
|
|
124
|
+
if (equity == 0 || totalSupply() == 0) {
|
|
125
|
+
return 10 ** 14;
|
|
126
|
+
} else {
|
|
127
|
+
return (VALUATION_FACTOR * JUSD.equity() * ONE_DEC18) / totalSupply();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _update(address from, address to, uint256 value) internal override {
|
|
132
|
+
if (value > 0) {
|
|
133
|
+
// No need to adjust the sender's votes. When they send out 10% of their shares, they also lose 10% of
|
|
134
|
+
// their votes, so everything falls nicely into place. Recipient votes should stay the same, but grow
|
|
135
|
+
// faster in the future, requiring an adjustment of the anchor.
|
|
136
|
+
uint256 roundingLoss = _adjustRecipientVoteAnchor(to, value);
|
|
137
|
+
// The total also must be adjusted and kept accurate by taking into account the rounding error.
|
|
138
|
+
_adjustTotalVotes(from, value, roundingLoss);
|
|
139
|
+
|
|
140
|
+
// Flash loan protection: Track when shares are received to prevent same-block redemptions.
|
|
141
|
+
// This covers mints (from == address(0)), transfers, and ERC3009 transferWithAuthorization.
|
|
142
|
+
if (to != address(0)) {
|
|
143
|
+
lastInboundBlock[to] = block.number;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
super._update(from, to, value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @notice Decrease the total votes anchor when tokens lose their voting power due to being moved.
|
|
151
|
+
* @param from sender
|
|
152
|
+
* @param amount amount to be sent
|
|
153
|
+
*/
|
|
154
|
+
function _adjustTotalVotes(address from, uint256 amount, uint256 roundingLoss) internal {
|
|
155
|
+
uint64 time = _anchorTime();
|
|
156
|
+
uint256 lostVotes = from == address(0x0) ? 0 : (time - voteAnchor[from]) * amount;
|
|
157
|
+
totalVotesAtAnchor = uint192(totalVotes() - roundingLoss - lostVotes);
|
|
158
|
+
totalVotesAnchorTime = time;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @notice The vote anchor of the recipient is moved forward such that the number of calculated
|
|
163
|
+
* votes does not change despite the higher balance.
|
|
164
|
+
* @param to receiver address
|
|
165
|
+
* @param amount amount to be received
|
|
166
|
+
* @return the number of votes lost due to rounding errors
|
|
167
|
+
*/
|
|
168
|
+
function _adjustRecipientVoteAnchor(address to, uint256 amount) internal returns (uint256) {
|
|
169
|
+
if (to != address(0x0)) {
|
|
170
|
+
uint256 recipientVotes = votes(to); // for example 21 if 7 shares were held for 3 seconds
|
|
171
|
+
uint256 newbalance = balanceOf(to) + amount; // for example 11 if 4 shares are added
|
|
172
|
+
// new example: anchor is only 21 / 11 = ~1 second in the past
|
|
173
|
+
voteAnchor[to] = uint64(_anchorTime() - recipientVotes / newbalance);
|
|
174
|
+
return recipientVotes % newbalance; // we have lost 21 % 11 = 10 votes
|
|
175
|
+
} else {
|
|
176
|
+
// optimization for burn, vote anchor of null address does not matter
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @notice Time stamp with some additional bits for higher resolution.
|
|
183
|
+
*/
|
|
184
|
+
function _anchorTime() internal view returns (uint64) {
|
|
185
|
+
return uint64(block.timestamp << TIME_RESOLUTION_BITS);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @notice The relative voting power of the address.
|
|
190
|
+
* @return A percentage with 1e18 being 100%
|
|
191
|
+
*/
|
|
192
|
+
function relativeVotes(address holder) external view returns (uint256) {
|
|
193
|
+
return (ONE_DEC18 * votes(holder)) / totalVotes();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @notice The votes of the holder, excluding votes from delegates.
|
|
198
|
+
*/
|
|
199
|
+
function votes(address holder) public view returns (uint256) {
|
|
200
|
+
return balanceOf(holder) * (_anchorTime() - voteAnchor[holder]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @notice How long the holder already held onto their average JUICE in seconds.
|
|
205
|
+
*/
|
|
206
|
+
function holdingDuration(address holder) public view returns (uint256) {
|
|
207
|
+
return (_anchorTime() - voteAnchor[holder]) >> TIME_RESOLUTION_BITS;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @notice Total number of votes in the system.
|
|
212
|
+
*/
|
|
213
|
+
function totalVotes() public view returns (uint256) {
|
|
214
|
+
return totalVotesAtAnchor + totalSupply() * (_anchorTime() - totalVotesAnchorTime);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @notice The number of votes the sender commands when taking the support of the helpers into account.
|
|
219
|
+
* @param sender The address whose total voting power is of interest
|
|
220
|
+
* @param helpers An incrementally sorted list of helpers without duplicates and without the sender.
|
|
221
|
+
* The call fails if the list contains an address that does not delegate to sender.
|
|
222
|
+
* For indirect delegates, i.e. a -> b -> c, both a and b must be included for both to count.
|
|
223
|
+
* @return The total number of votes of sender at the current point in time.
|
|
224
|
+
*/
|
|
225
|
+
function votesDelegated(address sender, address[] calldata helpers) public view returns (uint256) {
|
|
226
|
+
uint256 _votes = votes(sender);
|
|
227
|
+
require(_checkDuplicatesAndSorted(helpers));
|
|
228
|
+
for (uint i = 0; i < helpers.length; i++) {
|
|
229
|
+
address current = helpers[i];
|
|
230
|
+
require(current != sender);
|
|
231
|
+
require(_canVoteFor(sender, current));
|
|
232
|
+
_votes += votes(current);
|
|
233
|
+
}
|
|
234
|
+
return _votes;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function _checkDuplicatesAndSorted(address[] calldata helpers) internal pure returns (bool ok) {
|
|
238
|
+
if (helpers.length <= 1) {
|
|
239
|
+
return true;
|
|
240
|
+
} else {
|
|
241
|
+
address prevAddress = helpers[0];
|
|
242
|
+
for (uint i = 1; i < helpers.length; i++) {
|
|
243
|
+
if (helpers[i] <= prevAddress) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
prevAddress = helpers[i];
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @notice Checks whether the sender address is qualified given a list of helpers that delegated their votes
|
|
254
|
+
* directly or indirectly to the sender. It is the responsibility of the caller to figure out whether
|
|
255
|
+
* helpers are necessary and to identify them by scanning the blockchain for Delegation events.
|
|
256
|
+
*/
|
|
257
|
+
function checkQualified(address sender, address[] calldata helpers) public view override {
|
|
258
|
+
uint256 _votes = votesDelegated(sender, helpers);
|
|
259
|
+
if (_votes * 10_000 < QUORUM * totalVotes()) revert NotQualified();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @notice Increases the voting power of the delegate by your number of votes without taking away any voting power
|
|
264
|
+
* from the sender.
|
|
265
|
+
*/
|
|
266
|
+
function delegateVoteTo(address delegate) external {
|
|
267
|
+
delegates[msg.sender] = delegate;
|
|
268
|
+
emit Delegation(msg.sender, delegate);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function _canVoteFor(address delegate, address owner) internal view returns (bool) {
|
|
272
|
+
if (owner == delegate) {
|
|
273
|
+
return true;
|
|
274
|
+
} else if (owner == address(0x0)) {
|
|
275
|
+
return false;
|
|
276
|
+
} else {
|
|
277
|
+
return _canVoteFor(delegate, delegates[owner]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @notice Since quorum is rather low, it is important to have a way to prevent malicious minority holders
|
|
283
|
+
* from blocking the whole system. This method provides a way for the good guys to team up and destroy
|
|
284
|
+
* the bad guy's votes (at the cost of also reducing their own votes). This mechanism potentially
|
|
285
|
+
* gives full control over the system to whoever has 51% of the votes.
|
|
286
|
+
*
|
|
287
|
+
* Since this is a rather aggressive measure, delegation is not supported. Every holder must call this
|
|
288
|
+
* method on their own.
|
|
289
|
+
* @param targets The target addresses to remove votes from
|
|
290
|
+
* @param votesToDestroy The maximum number of votes the caller is willing to sacrifice
|
|
291
|
+
*/
|
|
292
|
+
function kamikaze(address[] calldata targets, uint256 votesToDestroy) external {
|
|
293
|
+
uint256 budget = _reduceVotes(msg.sender, votesToDestroy);
|
|
294
|
+
uint256 destroyedVotes = 0;
|
|
295
|
+
for (uint256 i = 0; i < targets.length && destroyedVotes < budget; i++) {
|
|
296
|
+
destroyedVotes += _reduceVotes(targets[i], budget - destroyedVotes);
|
|
297
|
+
}
|
|
298
|
+
require(destroyedVotes > 0); // sanity check
|
|
299
|
+
totalVotesAtAnchor = uint192(totalVotes() - destroyedVotes - budget);
|
|
300
|
+
totalVotesAnchorTime = _anchorTime();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _reduceVotes(address target, uint256 amount) internal returns (uint256) {
|
|
304
|
+
uint256 votesBefore = votes(target);
|
|
305
|
+
if (amount >= votesBefore) {
|
|
306
|
+
amount = votesBefore;
|
|
307
|
+
voteAnchor[target] = _anchorTime();
|
|
308
|
+
return votesBefore;
|
|
309
|
+
} else {
|
|
310
|
+
voteAnchor[target] = uint64(_anchorTime() - (votesBefore - amount) / balanceOf(target));
|
|
311
|
+
return votesBefore - votes(target);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @notice Call this method to obtain newly minted pool shares in exchange for JuiceDollars.
|
|
317
|
+
* No allowance required (i.e., it is hard-coded in the JuiceDollar token contract).
|
|
318
|
+
* Make sure to invest at least 10e-12 * market cap to avoid rounding losses.
|
|
319
|
+
*
|
|
320
|
+
* @dev If equity is close to zero or negative, you need to send enough JUSD to bring equity back to 1_000 JUSD.
|
|
321
|
+
*
|
|
322
|
+
* @param amount JuiceDollars to invest
|
|
323
|
+
* @param expectedShares Minimum amount of expected shares for front running protection
|
|
324
|
+
*/
|
|
325
|
+
function invest(uint256 amount, uint256 expectedShares) external returns (uint256) {
|
|
326
|
+
return _invest(_msgSender(), amount, expectedShares);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function investFor(address investor, uint256 amount, uint256 expectedShares) external returns (uint256) {
|
|
330
|
+
if (!JUSD.isMinter(_msgSender())) revert NotMinter();
|
|
331
|
+
return _invest(investor, amount, expectedShares);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function _invest(address investor, uint256 amount, uint256 expectedShares) internal returns (uint256) {
|
|
335
|
+
JUSD.transferFrom(investor, address(this), amount);
|
|
336
|
+
uint256 equity = JUSD.equity();
|
|
337
|
+
if (equity < MINIMUM_EQUITY) revert InsufficientEquity(); // ensures that the initial deposit is at least 1_000 JUSD
|
|
338
|
+
|
|
339
|
+
uint256 shares = _calculateShares(equity <= amount ? 0 : equity - amount, amount);
|
|
340
|
+
require(shares >= expectedShares);
|
|
341
|
+
_mint(investor, shares);
|
|
342
|
+
emit Trade(investor, int(shares), amount, price());
|
|
343
|
+
|
|
344
|
+
// limit the total supply to a reasonable amount to guard against overflows with price and vote calculations
|
|
345
|
+
if(totalSupply() > type(uint96).max) revert TotalSupplyExceeded();
|
|
346
|
+
return shares;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @notice Calculate shares received when investing JuiceDollars
|
|
351
|
+
* @param investment JUSD to be invested
|
|
352
|
+
* @return shares to be received in return
|
|
353
|
+
*/
|
|
354
|
+
function calculateShares(uint256 investment) external view returns (uint256) {
|
|
355
|
+
return _calculateShares(JUSD.equity(), investment);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function _calculateShares(uint256 capitalBefore, uint256 investment) internal view returns (uint256) {
|
|
359
|
+
uint256 totalShares = totalSupply();
|
|
360
|
+
uint256 investmentExFees = (investment * 980) / 1_000; // remove 2% fee
|
|
361
|
+
// Assign 10_000_000 JUICE for the initial deposit, calculate the amount otherwise
|
|
362
|
+
uint256 newTotalShares = (capitalBefore < MINIMUM_EQUITY || totalShares == 0)
|
|
363
|
+
? totalShares + 10_000_000 * ONE_DEC18
|
|
364
|
+
: _mulD18(totalShares, _tenthRoot(_divD18(capitalBefore + investmentExFees, capitalBefore)));
|
|
365
|
+
return newTotalShares - totalShares;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @notice Redeem the given amount of shares owned by the sender and transfer the proceeds to the target.
|
|
370
|
+
* @return The amount of JUSD transferred to the target
|
|
371
|
+
*/
|
|
372
|
+
function redeem(address target, uint256 shares) external notSameBlock(msg.sender) returns (uint256) {
|
|
373
|
+
return _redeemFrom(msg.sender, target, shares);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @notice Like redeem(...), but with an extra parameter to protect against front running.
|
|
378
|
+
* @param expectedProceeds The minimum acceptable redemption proceeds.
|
|
379
|
+
*/
|
|
380
|
+
function redeemExpected(address target, uint256 shares, uint256 expectedProceeds) external notSameBlock(msg.sender) returns (uint256) {
|
|
381
|
+
uint256 proceeds = _redeemFrom(msg.sender, target, shares);
|
|
382
|
+
require(proceeds >= expectedProceeds);
|
|
383
|
+
return proceeds;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @notice Redeem JUICE based on an allowance from the owner to the caller.
|
|
388
|
+
* See also redeemExpected(...).
|
|
389
|
+
*/
|
|
390
|
+
function redeemFrom(
|
|
391
|
+
address owner,
|
|
392
|
+
address target,
|
|
393
|
+
uint256 shares,
|
|
394
|
+
uint256 expectedProceeds
|
|
395
|
+
) external notSameBlock(owner) returns (uint256) {
|
|
396
|
+
_spendAllowance(owner, msg.sender, shares);
|
|
397
|
+
uint256 proceeds = _redeemFrom(owner, target, shares);
|
|
398
|
+
require(proceeds >= expectedProceeds);
|
|
399
|
+
return proceeds;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function _redeemFrom(address owner, address target, uint256 shares) internal returns (uint256) {
|
|
403
|
+
uint256 proceeds = calculateProceeds(shares);
|
|
404
|
+
_burn(owner, shares);
|
|
405
|
+
JUSD.transfer(target, proceeds);
|
|
406
|
+
emit Trade(owner, -int(shares), proceeds, price());
|
|
407
|
+
return proceeds;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @notice Calculate JUSD received when depositing shares
|
|
412
|
+
* @param shares number of shares we want to exchange for JUSD,
|
|
413
|
+
* in dec18 format
|
|
414
|
+
* @return amount of JUSD received for the shares
|
|
415
|
+
*/
|
|
416
|
+
function calculateProceeds(uint256 shares) public view returns (uint256) {
|
|
417
|
+
uint256 totalShares = totalSupply();
|
|
418
|
+
if (shares + ONE_DEC18 >= totalShares) revert TooManyShares(); // make sure there is always at least one share
|
|
419
|
+
uint256 capital = JUSD.equity();
|
|
420
|
+
uint256 reductionAfterFees = (shares * 980) / 1_000; // remove 2% fee
|
|
421
|
+
uint256 newCapital = _mulD18(capital, _power10(_divD18(totalShares - reductionAfterFees, totalShares)));
|
|
422
|
+
return capital - newCapital;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @notice If there is less than 1_000 JUSD in equity left (maybe even negative), the system is at risk
|
|
427
|
+
* and we should allow qualified JUICE holders to restructure the system.
|
|
428
|
+
*
|
|
429
|
+
* Example: there was a devastating loss and equity stands at -1'000'000. Most shareholders have lost hope in the
|
|
430
|
+
* JuiceDollar system except for a group of small JUICE holders who still believe in it and are willing to provide
|
|
431
|
+
* 2'000'000 JUSD to save it. These brave souls are essentially donating 1'000'000 to the minter reserve and it
|
|
432
|
+
* would be wrong to force them to share the other million with the passive JUICE holders. Instead, they will get
|
|
433
|
+
* the possibility to bootstrap the system again owning 100% of all JUICE shares.
|
|
434
|
+
*
|
|
435
|
+
* @param helpers A list of addresses that delegate to the caller in incremental order
|
|
436
|
+
* @param addressesToWipe A list of addresses whose JUICE will be burned to zero
|
|
437
|
+
*/
|
|
438
|
+
function restructureCapTable(address[] calldata helpers, address[] calldata addressesToWipe) external {
|
|
439
|
+
require(JUSD.equity() < MINIMUM_EQUITY);
|
|
440
|
+
checkQualified(msg.sender, helpers);
|
|
441
|
+
for (uint256 i = 0; i < addressesToWipe.length; i++) {
|
|
442
|
+
address current = addressesToWipe[i];
|
|
443
|
+
_burn(current, balanceOf(current));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @dev See {IERC165-supportsInterface}.
|
|
449
|
+
*/
|
|
450
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
451
|
+
return
|
|
452
|
+
interfaceId == type(IERC20).interfaceId ||
|
|
453
|
+
interfaceId == type(ERC20Permit).interfaceId ||
|
|
454
|
+
interfaceId == type(ERC3009).interfaceId ||
|
|
455
|
+
super.supportsInterface(interfaceId);
|
|
456
|
+
}
|
|
457
|
+
}
|