@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,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {IJuiceDollar} from "./interface/IJuiceDollar.sol";
|
|
5
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
6
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
7
|
+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @title Stablecoin Bridge
|
|
11
|
+
* @notice A minting contract for another USD stablecoin ('source stablecoin') that we trust.
|
|
12
|
+
*/
|
|
13
|
+
contract StablecoinBridge {
|
|
14
|
+
using SafeERC20 for IERC20;
|
|
15
|
+
|
|
16
|
+
IERC20 public immutable usd; // the source stablecoin
|
|
17
|
+
IJuiceDollar public immutable JUSD; // the JUSD
|
|
18
|
+
uint8 private immutable usdDecimals;
|
|
19
|
+
uint8 private immutable JUSDDecimals;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @notice The time horizon after which this bridge expires and needs to be replaced by a new contract.
|
|
23
|
+
*/
|
|
24
|
+
uint256 public immutable horizon;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @notice The maximum amount of outstanding converted source stablecoins.
|
|
28
|
+
*/
|
|
29
|
+
uint256 public immutable limit;
|
|
30
|
+
uint256 public minted;
|
|
31
|
+
|
|
32
|
+
error Limit(uint256 amount, uint256 limit);
|
|
33
|
+
error Expired(uint256 time, uint256 expiration);
|
|
34
|
+
error UnsupportedToken(address token);
|
|
35
|
+
|
|
36
|
+
constructor(address other, address JUSDAddress, uint256 limit_, uint256 weeks_) {
|
|
37
|
+
usd = IERC20(other);
|
|
38
|
+
JUSD = IJuiceDollar(JUSDAddress);
|
|
39
|
+
usdDecimals = IERC20Metadata(other).decimals();
|
|
40
|
+
JUSDDecimals = IERC20Metadata(JUSDAddress).decimals();
|
|
41
|
+
horizon = block.timestamp + weeks_ * 1 weeks;
|
|
42
|
+
limit = limit_;
|
|
43
|
+
minted = 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @notice Convenience method for mint(msg.sender, amount)
|
|
48
|
+
*/
|
|
49
|
+
function mint(uint256 amount) external {
|
|
50
|
+
mintTo(msg.sender, amount);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @notice Mint the target amount of JUSD, taking the equal amount of source coins from the sender.
|
|
55
|
+
* @dev This only works if an allowance for the source coins has been set and the caller has enough of them.
|
|
56
|
+
* @param amount The amount of the source stablecoin to bridge (convert).
|
|
57
|
+
*/
|
|
58
|
+
function mintTo(address target, uint256 amount) public {
|
|
59
|
+
usd.safeTransferFrom(msg.sender, address(this), amount);
|
|
60
|
+
|
|
61
|
+
uint256 targetAmount = _convertAmount(amount, usdDecimals, JUSDDecimals);
|
|
62
|
+
_mint(target, targetAmount);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _mint(address target, uint256 amount) internal {
|
|
66
|
+
if (block.timestamp > horizon) revert Expired(block.timestamp, horizon);
|
|
67
|
+
JUSD.mint(target, amount);
|
|
68
|
+
minted += amount;
|
|
69
|
+
if (minted > limit) revert Limit(amount, limit);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @notice Convenience method for burnAndSend(msg.sender, amount)
|
|
74
|
+
* @param amount The amount of JUSD to burn.
|
|
75
|
+
*/
|
|
76
|
+
function burn(uint256 amount) external {
|
|
77
|
+
_burn(msg.sender, msg.sender, amount);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @notice Burn the indicated amount of JUSD and send the same number of source coins to the caller.
|
|
82
|
+
*/
|
|
83
|
+
function burnAndSend(address target, uint256 amount) external {
|
|
84
|
+
_burn(msg.sender, target, amount);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _burn(address JUSDHolder, address target, uint256 amount) internal {
|
|
88
|
+
uint256 sourceAmount = _convertAmount(amount, JUSDDecimals, usdDecimals);
|
|
89
|
+
JUSD.burnFrom(JUSDHolder, amount);
|
|
90
|
+
usd.safeTransfer(target, sourceAmount);
|
|
91
|
+
minted -= amount;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @notice Converts an amount between two tokens with different decimal places.
|
|
96
|
+
* @param amount The amount to convert.
|
|
97
|
+
* @param fromDecimals The decimal places of the source token.
|
|
98
|
+
* @param toDecimals The decimal places of the target token.
|
|
99
|
+
*/
|
|
100
|
+
function _convertAmount(uint256 amount, uint8 fromDecimals, uint8 toDecimals) internal pure returns (uint256) {
|
|
101
|
+
if (fromDecimals < toDecimals) {
|
|
102
|
+
return amount * 10**(toDecimals - fromDecimals);
|
|
103
|
+
} else if (fromDecimals > toDecimals) {
|
|
104
|
+
return amount / 10**(fromDecimals - toDecimals);
|
|
105
|
+
} else {
|
|
106
|
+
return amount;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title StartUSD
|
|
8
|
+
* @notice A minimal genesis stablecoin used to bootstrap the JuiceDollar protocol.
|
|
9
|
+
* @dev Mints 10,000 SUSD to the deployer. Used to initialize the protocol with initial
|
|
10
|
+
* JUSD supply through a StablecoinBridge, which then creates the initial JUICE tokens.
|
|
11
|
+
*/
|
|
12
|
+
contract StartUSD is ERC20 {
|
|
13
|
+
constructor() ERC20("StartUSD", "SUSD") {
|
|
14
|
+
_mint(msg.sender, 10_000 * 10 ** 18);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.10;
|
|
3
|
+
|
|
4
|
+
import {IMintingHubGateway} from "./interface/IMintingHubGateway.sol";
|
|
5
|
+
import {ICoinLendingGateway} from "./interface/ICoinLendingGateway.sol";
|
|
6
|
+
import {IPosition} from "../MintingHubV2/interface/IPosition.sol";
|
|
7
|
+
import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
|
|
8
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
9
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
10
|
+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
11
|
+
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
|
|
12
|
+
|
|
13
|
+
interface IWrappedCBTC is IERC20 {
|
|
14
|
+
function deposit() external payable;
|
|
15
|
+
function withdraw(uint256 wad) external;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @title Coin Lending Gateway
|
|
20
|
+
* @notice An improved gateway that enables true single-transaction native coin lending with custom liquidation prices
|
|
21
|
+
* @dev This version handles the ownership transfer timing issue to allow price adjustments in the same transaction
|
|
22
|
+
*/
|
|
23
|
+
contract CoinLendingGateway is ICoinLendingGateway, Ownable, ReentrancyGuard, Pausable {
|
|
24
|
+
IMintingHubGateway public immutable MINTING_HUB;
|
|
25
|
+
IWrappedCBTC public immutable WCBTC;
|
|
26
|
+
IJuiceDollar public immutable JUSD;
|
|
27
|
+
|
|
28
|
+
error InsufficientCoin();
|
|
29
|
+
error InvalidPosition();
|
|
30
|
+
error TransferFailed();
|
|
31
|
+
error PriceAdjustmentFailed();
|
|
32
|
+
error DirectCBTCNotAccepted();
|
|
33
|
+
|
|
34
|
+
event CoinRescued(address indexed to, uint256 amount);
|
|
35
|
+
event TokenRescued(address indexed token, address indexed to, uint256 amount);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @notice Initializes the Coin Lending Gateway
|
|
39
|
+
* @param _mintingHub The address of the MintingHubGateway contract
|
|
40
|
+
* @param _wcbtc The address of the Wrapped cBTC (WcBTC) token contract
|
|
41
|
+
* @param _jusd The address of the JuiceDollar contract
|
|
42
|
+
*/
|
|
43
|
+
constructor(address _mintingHub, address _wcbtc, address _jusd) Ownable(_msgSender()) {
|
|
44
|
+
MINTING_HUB = IMintingHubGateway(_mintingHub);
|
|
45
|
+
WCBTC = IWrappedCBTC(_wcbtc);
|
|
46
|
+
JUSD = IJuiceDollar(_jusd);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @notice Creates a lending position using native cBTC in a single transaction
|
|
51
|
+
* @dev This improved version uses a two-step clone process to handle ownership and price adjustment correctly
|
|
52
|
+
* @param parent The parent position to clone from
|
|
53
|
+
* @param initialMint The amount of JUSD to mint
|
|
54
|
+
* @param expiration The expiration timestamp for the position
|
|
55
|
+
* @param frontendCode The frontend referral code
|
|
56
|
+
* @param liquidationPrice The desired liquidation price (0 to skip adjustment)
|
|
57
|
+
* @return position The address of the newly created position
|
|
58
|
+
*/
|
|
59
|
+
function lendWithCoin(
|
|
60
|
+
address parent,
|
|
61
|
+
uint256 initialMint,
|
|
62
|
+
uint40 expiration,
|
|
63
|
+
bytes32 frontendCode,
|
|
64
|
+
uint256 liquidationPrice
|
|
65
|
+
) external payable nonReentrant whenNotPaused returns (address position) {
|
|
66
|
+
if (msg.value == 0) revert InsufficientCoin();
|
|
67
|
+
|
|
68
|
+
return _lendWithCoin(
|
|
69
|
+
_msgSender(),
|
|
70
|
+
parent,
|
|
71
|
+
initialMint,
|
|
72
|
+
expiration,
|
|
73
|
+
frontendCode,
|
|
74
|
+
liquidationPrice
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @notice Creates a lending position for another owner using native cBTC
|
|
80
|
+
* @dev Same as lendWithCoin but allows specifying a different owner
|
|
81
|
+
* @param owner The address that will own the position
|
|
82
|
+
* @param parent The parent position to clone from
|
|
83
|
+
* @param initialMint The amount of JUSD to mint
|
|
84
|
+
* @param expiration The expiration timestamp for the position
|
|
85
|
+
* @param frontendCode The frontend referral code
|
|
86
|
+
* @param liquidationPrice The desired liquidation price (0 to skip adjustment)
|
|
87
|
+
* @return position The address of the newly created position
|
|
88
|
+
*/
|
|
89
|
+
function lendWithCoinFor(
|
|
90
|
+
address owner,
|
|
91
|
+
address parent,
|
|
92
|
+
uint256 initialMint,
|
|
93
|
+
uint40 expiration,
|
|
94
|
+
bytes32 frontendCode,
|
|
95
|
+
uint256 liquidationPrice
|
|
96
|
+
) external payable nonReentrant whenNotPaused returns (address position) {
|
|
97
|
+
if (msg.value == 0) revert InsufficientCoin();
|
|
98
|
+
if (owner == address(0)) revert InvalidPosition();
|
|
99
|
+
|
|
100
|
+
return _lendWithCoin(
|
|
101
|
+
owner,
|
|
102
|
+
parent,
|
|
103
|
+
initialMint,
|
|
104
|
+
expiration,
|
|
105
|
+
frontendCode,
|
|
106
|
+
liquidationPrice
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @dev Internal function containing the core lending logic
|
|
112
|
+
* @param owner The address that will own the position
|
|
113
|
+
* @param parent The parent position to clone from
|
|
114
|
+
* @param initialMint The amount of JUSD to mint
|
|
115
|
+
* @param expiration The expiration timestamp for the position
|
|
116
|
+
* @param frontendCode The frontend referral code
|
|
117
|
+
* @param liquidationPrice The desired liquidation price (0 to skip adjustment)
|
|
118
|
+
* @return position The address of the newly created position
|
|
119
|
+
*/
|
|
120
|
+
function _lendWithCoin(
|
|
121
|
+
address owner,
|
|
122
|
+
address parent,
|
|
123
|
+
uint256 initialMint,
|
|
124
|
+
uint40 expiration,
|
|
125
|
+
bytes32 frontendCode,
|
|
126
|
+
uint256 liquidationPrice
|
|
127
|
+
) internal returns (address position) {
|
|
128
|
+
WCBTC.deposit{value: msg.value}();
|
|
129
|
+
|
|
130
|
+
WCBTC.approve(address(MINTING_HUB), msg.value);
|
|
131
|
+
|
|
132
|
+
// This contract must be initial owner to call adjustPrice before transferring ownership
|
|
133
|
+
position = MINTING_HUB.clone(
|
|
134
|
+
address(this), // temporary owner (this contract)
|
|
135
|
+
parent, // parent position
|
|
136
|
+
msg.value, // collateral amount
|
|
137
|
+
initialMint, // mint amount
|
|
138
|
+
expiration,
|
|
139
|
+
frontendCode
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (position == address(0)) revert InvalidPosition();
|
|
143
|
+
|
|
144
|
+
if (liquidationPrice > 0) {
|
|
145
|
+
uint256 currentPrice = IPosition(position).price();
|
|
146
|
+
|
|
147
|
+
if (liquidationPrice != currentPrice) {
|
|
148
|
+
try IPosition(position).adjustPrice(liquidationPrice) {
|
|
149
|
+
// Price adjustment succeeded
|
|
150
|
+
} catch {
|
|
151
|
+
revert PriceAdjustmentFailed();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
uint256 jusdBalance = JUSD.balanceOf(address(this));
|
|
157
|
+
if (jusdBalance > 0) {
|
|
158
|
+
JUSD.transfer(owner, jusdBalance);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
Ownable(position).transferOwnership(owner);
|
|
162
|
+
|
|
163
|
+
emit PositionCreatedWithCoin(
|
|
164
|
+
owner,
|
|
165
|
+
position,
|
|
166
|
+
msg.value,
|
|
167
|
+
initialMint,
|
|
168
|
+
liquidationPrice
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return position;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @notice Rescue function to withdraw accidentally sent native cBTC
|
|
176
|
+
* @dev Only owner can call this function
|
|
177
|
+
*/
|
|
178
|
+
function rescueCoin() external onlyOwner {
|
|
179
|
+
uint256 balance = address(this).balance;
|
|
180
|
+
if (balance > 0) {
|
|
181
|
+
(bool success, ) = owner().call{value: balance}("");
|
|
182
|
+
if (!success) revert TransferFailed();
|
|
183
|
+
emit CoinRescued(owner(), balance);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @notice Rescue function to withdraw accidentally sent tokens
|
|
189
|
+
* @dev Only owner can call this function
|
|
190
|
+
* @param token The address of the token to rescue
|
|
191
|
+
* @param to The address to send the tokens to
|
|
192
|
+
* @param amount The amount of tokens to rescue
|
|
193
|
+
*/
|
|
194
|
+
function rescueToken(address token, address to, uint256 amount) external onlyOwner {
|
|
195
|
+
if (to == address(0)) revert TransferFailed();
|
|
196
|
+
bool success = IERC20(token).transfer(to, amount);
|
|
197
|
+
if (!success) revert TransferFailed();
|
|
198
|
+
emit TokenRescued(token, to, amount);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @notice Pause the contract (only owner)
|
|
203
|
+
* @dev Prevents lendWithCoin functions from being called
|
|
204
|
+
*/
|
|
205
|
+
function pause() external onlyOwner {
|
|
206
|
+
_pause();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @notice Unpause the contract (only owner)
|
|
211
|
+
* @dev Re-enables lendWithCoin functions
|
|
212
|
+
*/
|
|
213
|
+
function unpause() external onlyOwner {
|
|
214
|
+
_unpause();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @dev Reject direct cBTC transfers to prevent stuck funds
|
|
219
|
+
*/
|
|
220
|
+
receive() external payable {
|
|
221
|
+
revert DirectCBTCNotAccepted();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {Equity} from "../Equity.sol";
|
|
5
|
+
import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
|
|
6
|
+
import {SavingsGateway} from "./SavingsGateway.sol";
|
|
7
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
8
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
9
|
+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
10
|
+
import {IFrontendGateway} from "./interface/IFrontendGateway.sol";
|
|
11
|
+
import {IMintingHubGateway} from "./interface/IMintingHubGateway.sol";
|
|
12
|
+
|
|
13
|
+
contract FrontendGateway is IFrontendGateway, Context, Ownable {
|
|
14
|
+
IERC20 public immutable JUSD;
|
|
15
|
+
Equity public immutable EQUITY;
|
|
16
|
+
|
|
17
|
+
// solhint-disable-next-line var-name-mixedcase
|
|
18
|
+
IMintingHubGateway public MINTING_HUB;
|
|
19
|
+
|
|
20
|
+
// solhint-disable-next-line var-name-mixedcase
|
|
21
|
+
SavingsGateway public SAVINGS;
|
|
22
|
+
|
|
23
|
+
uint24 public feeRate; // Fee rate in PPM (parts per million), for example 10'000 = 1%
|
|
24
|
+
uint24 public savingsFeeRate; // Fee rate of savings in PPM (parts per million), for example 10 = 1%
|
|
25
|
+
uint24 public mintingFeeRate; // Reward rate of newly minted positions in PPM (parts per million), for example 10 = 1%
|
|
26
|
+
uint24 public nextFeeRate;
|
|
27
|
+
uint24 public nextSavingsFeeRate;
|
|
28
|
+
uint24 public nextMintingFeeRate;
|
|
29
|
+
uint256 public changeTimeLock;
|
|
30
|
+
|
|
31
|
+
mapping(bytes32 => FrontendCode) public frontendCodes;
|
|
32
|
+
mapping(address => bytes32) public referredPositions;
|
|
33
|
+
mapping(address => bytes32) public lastUsedFrontendCode;
|
|
34
|
+
|
|
35
|
+
modifier frontendCodeOwnerOnly(bytes32 frontendCode) {
|
|
36
|
+
if (frontendCodes[frontendCode].owner != _msgSender()) revert NotFrontendCodeOwner();
|
|
37
|
+
_;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
modifier onlyGatewayService(address service) {
|
|
41
|
+
if (_msgSender() != address(service)) revert NotGatewayService();
|
|
42
|
+
_;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
constructor(address jusd_) Ownable(_msgSender()) {
|
|
46
|
+
JUSD = IERC20(jusd_);
|
|
47
|
+
EQUITY = Equity(address(IJuiceDollar(jusd_).reserve()));
|
|
48
|
+
feeRate = 10_000; // 10_000/1_000_000 = 1% fee
|
|
49
|
+
savingsFeeRate = 50_000; // 50_000/1_000_000 = 5% fee of the of the savings interest
|
|
50
|
+
mintingFeeRate = 50_000; // 50_000/1_000_000 = 5% fee of the of the interest paid by the position owner
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @notice Call this a wrapper method to obtain newly minted pool shares in exchange for
|
|
55
|
+
* JuiceDollars and reward frontend providers with a small commission.
|
|
56
|
+
* No allowance required (i.e., it is hard-coded in the JuiceDollar token contract).
|
|
57
|
+
* Make sure to invest at least 10e-12 * market cap to avoid rounding losses.
|
|
58
|
+
*
|
|
59
|
+
* @dev If equity is close to zero or negative, you need to send enough JUSD to bring equity back to 1_000 JUSD.
|
|
60
|
+
*
|
|
61
|
+
* @param amount JuiceDollars to invest
|
|
62
|
+
* @param expectedShares Minimum amount of expected shares for front running protection
|
|
63
|
+
* @param frontendCode Code of the used frontend or referrer
|
|
64
|
+
*/
|
|
65
|
+
function invest(uint256 amount, uint256 expectedShares, bytes32 frontendCode) external returns (uint256) {
|
|
66
|
+
uint256 actualShares = EQUITY.investFor(_msgSender(), amount, expectedShares);
|
|
67
|
+
|
|
68
|
+
uint256 reward = updateFrontendAccount(frontendCode, amount);
|
|
69
|
+
emit InvestRewardAdded(frontendCode, _msgSender(), amount, reward);
|
|
70
|
+
return actualShares;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function redeem(
|
|
74
|
+
address target,
|
|
75
|
+
uint256 shares,
|
|
76
|
+
uint256 expectedProceeds,
|
|
77
|
+
bytes32 frontendCode
|
|
78
|
+
) external returns (uint256) {
|
|
79
|
+
uint256 actualProceeds = EQUITY.redeemFrom(_msgSender(), target, shares, expectedProceeds);
|
|
80
|
+
|
|
81
|
+
uint256 reward = updateFrontendAccount(frontendCode, actualProceeds);
|
|
82
|
+
emit RedeemRewardAdded(frontendCode, _msgSender(), actualProceeds, reward);
|
|
83
|
+
return actualProceeds;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
///////////////////
|
|
87
|
+
// Accounting Logic
|
|
88
|
+
///////////////////
|
|
89
|
+
|
|
90
|
+
function updateFrontendAccount(bytes32 frontendCode, uint256 amount) internal returns (uint256) {
|
|
91
|
+
if (frontendCode == bytes32(0)) return 0;
|
|
92
|
+
lastUsedFrontendCode[_msgSender()] = frontendCode;
|
|
93
|
+
uint256 reward = (amount * feeRate) / 1_000_000;
|
|
94
|
+
frontendCodes[frontendCode].balance += reward;
|
|
95
|
+
return reward;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updateSavingCode(
|
|
99
|
+
address savingsOwner,
|
|
100
|
+
bytes32 frontendCode
|
|
101
|
+
) external onlyGatewayService(address(SAVINGS)) {
|
|
102
|
+
if (frontendCode == bytes32(0)) return;
|
|
103
|
+
lastUsedFrontendCode[savingsOwner] = frontendCode;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function updateSavingRewards(address saver, uint256 interest) external onlyGatewayService(address(SAVINGS)) {
|
|
107
|
+
bytes32 frontendCode = lastUsedFrontendCode[saver];
|
|
108
|
+
if (frontendCode == bytes32(0)) return;
|
|
109
|
+
|
|
110
|
+
uint256 reward = (interest * savingsFeeRate) / 1_000_000;
|
|
111
|
+
frontendCodes[frontendCode].balance += reward;
|
|
112
|
+
|
|
113
|
+
emit SavingsRewardAdded(frontendCode, saver, interest, reward);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function registerPosition(
|
|
117
|
+
address position,
|
|
118
|
+
bytes32 frontendCode
|
|
119
|
+
) external onlyGatewayService(address(MINTING_HUB)) {
|
|
120
|
+
if (frontendCode == bytes32(0)) return;
|
|
121
|
+
|
|
122
|
+
referredPositions[position] = frontendCode;
|
|
123
|
+
emit NewPositionRegistered(position, frontendCode);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function updatePositionRewards(address position, uint256 amount) external onlyGatewayService(address(MINTING_HUB)) {
|
|
127
|
+
bytes32 frontendCode = referredPositions[position];
|
|
128
|
+
if (frontendCode == bytes32(0)) return;
|
|
129
|
+
|
|
130
|
+
uint256 reward = (amount * mintingFeeRate) / 1_000_000;
|
|
131
|
+
frontendCodes[frontendCode].balance += reward;
|
|
132
|
+
|
|
133
|
+
emit PositionRewardAdded(frontendCode, position, amount, reward);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getPositionFrontendCode(address position) external view returns (bytes32) {
|
|
137
|
+
return referredPositions[position];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
//////////////////////
|
|
141
|
+
// Frontend Code Logic
|
|
142
|
+
//////////////////////
|
|
143
|
+
|
|
144
|
+
function registerFrontendCode(bytes32 frontendCode) external returns (bool) {
|
|
145
|
+
if (frontendCodes[frontendCode].owner != address(0) || frontendCode == bytes32(0))
|
|
146
|
+
revert FrontendCodeAlreadyExists();
|
|
147
|
+
frontendCodes[frontendCode].owner = _msgSender();
|
|
148
|
+
emit FrontendCodeRegistered(_msgSender(), frontendCode);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function transferFrontendCode(
|
|
153
|
+
bytes32 frontendCode,
|
|
154
|
+
address to
|
|
155
|
+
) external frontendCodeOwnerOnly(frontendCode) returns (bool) {
|
|
156
|
+
frontendCodes[frontendCode].owner = to;
|
|
157
|
+
emit FrontendCodeTransferred(_msgSender(), to, frontendCode);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function withdrawRewards(bytes32 frontendCode) external frontendCodeOwnerOnly(frontendCode) returns (uint256) {
|
|
162
|
+
return _withdrawRewardsTo(frontendCode, _msgSender());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function withdrawRewardsTo(
|
|
166
|
+
bytes32 frontendCode,
|
|
167
|
+
address to
|
|
168
|
+
) external frontendCodeOwnerOnly(frontendCode) returns (uint256) {
|
|
169
|
+
return _withdrawRewardsTo(frontendCode, to);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _withdrawRewardsTo(bytes32 frontendCode, address to) internal returns (uint256) {
|
|
173
|
+
uint256 amount = frontendCodes[frontendCode].balance;
|
|
174
|
+
|
|
175
|
+
if (IJuiceDollar(address(JUSD)).equity() < amount) revert EquityTooLow();
|
|
176
|
+
|
|
177
|
+
frontendCodes[frontendCode].balance = 0;
|
|
178
|
+
IJuiceDollar(address(JUSD)).distributeProfits(to, amount);
|
|
179
|
+
emit FrontendCodeRewardsWithdrawn(to, amount, frontendCode);
|
|
180
|
+
return amount;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @notice Proposes new referral rates that will available to be executed after seven days.
|
|
185
|
+
* To cancel a proposal, just overwrite it with a new one proposing the current rate.
|
|
186
|
+
*/
|
|
187
|
+
function proposeChanges(
|
|
188
|
+
uint24 newFeeRatePPM_,
|
|
189
|
+
uint24 newSavingsFeeRatePPM_,
|
|
190
|
+
uint24 newMintingFeeRatePPM_,
|
|
191
|
+
address[] calldata helpers
|
|
192
|
+
) external {
|
|
193
|
+
if (newFeeRatePPM_ > 20_000 || newSavingsFeeRatePPM_ > 1_000_000 || newMintingFeeRatePPM_ > 1_000_000)
|
|
194
|
+
revert ProposedChangesToHigh();
|
|
195
|
+
EQUITY.checkQualified(_msgSender(), helpers);
|
|
196
|
+
nextFeeRate = newFeeRatePPM_;
|
|
197
|
+
nextSavingsFeeRate = newSavingsFeeRatePPM_;
|
|
198
|
+
nextMintingFeeRate = newMintingFeeRatePPM_;
|
|
199
|
+
changeTimeLock = block.timestamp + 7 days;
|
|
200
|
+
emit RateChangesProposed(
|
|
201
|
+
_msgSender(),
|
|
202
|
+
newFeeRatePPM_,
|
|
203
|
+
newSavingsFeeRatePPM_,
|
|
204
|
+
newMintingFeeRatePPM_,
|
|
205
|
+
changeTimeLock
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function executeChanges() external {
|
|
210
|
+
if (nextFeeRate == feeRate && nextSavingsFeeRate == savingsFeeRate && nextMintingFeeRate == mintingFeeRate)
|
|
211
|
+
revert NoOpenChanges();
|
|
212
|
+
if (block.timestamp < changeTimeLock) revert NotDoneWaiting(changeTimeLock);
|
|
213
|
+
feeRate = nextFeeRate;
|
|
214
|
+
savingsFeeRate = nextSavingsFeeRate;
|
|
215
|
+
mintingFeeRate = nextMintingFeeRate;
|
|
216
|
+
emit RateChangesExecuted(_msgSender(), feeRate, savingsFeeRate, mintingFeeRate);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function init(address savings, address mintingHub) external onlyOwner {
|
|
220
|
+
SAVINGS = SavingsGateway(savings);
|
|
221
|
+
MINTING_HUB = IMintingHubGateway(mintingHub);
|
|
222
|
+
renounceOwnership();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.10;
|
|
3
|
+
|
|
4
|
+
import {MintingHub} from "../MintingHubV2/MintingHub.sol";
|
|
5
|
+
import {IFrontendGateway} from "./interface/IFrontendGateway.sol";
|
|
6
|
+
import {IMintingHubGateway} from "./interface/IMintingHubGateway.sol";
|
|
7
|
+
|
|
8
|
+
contract MintingHubGateway is MintingHub, IMintingHubGateway {
|
|
9
|
+
IFrontendGateway public immutable GATEWAY;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
address _jusd,
|
|
13
|
+
address _leadrate,
|
|
14
|
+
address _roller,
|
|
15
|
+
address _factory,
|
|
16
|
+
address _gateway
|
|
17
|
+
) MintingHub(_jusd, _leadrate, _roller, _factory) {
|
|
18
|
+
GATEWAY = IFrontendGateway(_gateway);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function openPosition(
|
|
22
|
+
address _collateralAddress,
|
|
23
|
+
uint256 _minCollateral,
|
|
24
|
+
uint256 _initialCollateral,
|
|
25
|
+
uint256 _mintingMaximum,
|
|
26
|
+
uint40 _initPeriodSeconds,
|
|
27
|
+
uint40 _expirationSeconds,
|
|
28
|
+
uint40 _challengeSeconds,
|
|
29
|
+
uint24 _riskPremium,
|
|
30
|
+
uint256 _liqPrice,
|
|
31
|
+
uint24 _reservePPM,
|
|
32
|
+
bytes32 _frontendCode
|
|
33
|
+
) public returns (address) {
|
|
34
|
+
address position = openPosition(
|
|
35
|
+
_collateralAddress,
|
|
36
|
+
_minCollateral,
|
|
37
|
+
_initialCollateral,
|
|
38
|
+
_mintingMaximum,
|
|
39
|
+
_initPeriodSeconds,
|
|
40
|
+
_expirationSeconds,
|
|
41
|
+
_challengeSeconds,
|
|
42
|
+
_riskPremium,
|
|
43
|
+
_liqPrice,
|
|
44
|
+
_reservePPM
|
|
45
|
+
);
|
|
46
|
+
GATEWAY.registerPosition(position, _frontendCode);
|
|
47
|
+
return position;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clone(
|
|
51
|
+
address parent,
|
|
52
|
+
uint256 _initialCollateral,
|
|
53
|
+
uint256 _initialMint,
|
|
54
|
+
uint40 expiration,
|
|
55
|
+
bytes32 frontendCode
|
|
56
|
+
) public returns (address) {
|
|
57
|
+
return clone(msg.sender, parent, _initialCollateral, _initialMint, expiration, frontendCode);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @notice Clones an existing position and immediately tries to mint the specified amount using the given collateral.
|
|
62
|
+
* @dev This needs an allowance to be set on the collateral contract such that the minting hub can get the collateral.
|
|
63
|
+
*/
|
|
64
|
+
function clone(
|
|
65
|
+
address owner,
|
|
66
|
+
address parent,
|
|
67
|
+
uint256 _initialCollateral,
|
|
68
|
+
uint256 _initialMint,
|
|
69
|
+
uint40 expiration,
|
|
70
|
+
bytes32 frontendCode
|
|
71
|
+
) public returns (address) {
|
|
72
|
+
address position = clone(owner, parent, _initialCollateral, _initialMint, expiration);
|
|
73
|
+
GATEWAY.registerPosition(position, frontendCode);
|
|
74
|
+
return position;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function notifyInterestPaid(uint256 amount) external validPos(msg.sender) {
|
|
78
|
+
GATEWAY.updatePositionRewards(msg.sender, amount);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @dev See {IERC165-supportsInterface}.
|
|
83
|
+
*/
|
|
84
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
85
|
+
return interfaceId == type(IMintingHubGateway).interfaceId || super.supportsInterface(interfaceId);
|
|
86
|
+
}
|
|
87
|
+
}
|