@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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -0
  3. package/contracts/Equity.sol +457 -0
  4. package/contracts/JuiceDollar.sol +363 -0
  5. package/contracts/Leadrate.sol +79 -0
  6. package/contracts/MintingHubV2/MintingHub.sol +445 -0
  7. package/contracts/MintingHubV2/Position.sol +810 -0
  8. package/contracts/MintingHubV2/PositionFactory.sol +69 -0
  9. package/contracts/MintingHubV2/PositionRoller.sol +159 -0
  10. package/contracts/MintingHubV2/interface/IMintingHub.sol +26 -0
  11. package/contracts/MintingHubV2/interface/IPosition.sol +90 -0
  12. package/contracts/MintingHubV2/interface/IPositionFactory.sol +20 -0
  13. package/contracts/Savings.sol +141 -0
  14. package/contracts/SavingsVaultJUSD.sol +140 -0
  15. package/contracts/StablecoinBridge.sol +109 -0
  16. package/contracts/StartUSD.sol +16 -0
  17. package/contracts/gateway/CoinLendingGateway.sol +223 -0
  18. package/contracts/gateway/FrontendGateway.sol +224 -0
  19. package/contracts/gateway/MintingHubGateway.sol +87 -0
  20. package/contracts/gateway/SavingsGateway.sol +51 -0
  21. package/contracts/gateway/interface/ICoinLendingGateway.sol +73 -0
  22. package/contracts/gateway/interface/IFrontendGateway.sol +49 -0
  23. package/contracts/gateway/interface/IMintingHubGateway.sol +12 -0
  24. package/contracts/impl/ERC3009.sol +171 -0
  25. package/contracts/interface/IJuiceDollar.sol +54 -0
  26. package/contracts/interface/ILeadrate.sol +7 -0
  27. package/contracts/interface/IReserve.sol +9 -0
  28. package/contracts/interface/ISavingsJUSD.sol +49 -0
  29. package/contracts/test/FreakToken.sol +25 -0
  30. package/contracts/test/Math.sol +339 -0
  31. package/contracts/test/MockEquity.sol +15 -0
  32. package/contracts/test/PositionExpirationTest.sol +75 -0
  33. package/contracts/test/PositionRollingTest.sol +65 -0
  34. package/contracts/test/TestFlashLoan.sol +84 -0
  35. package/contracts/test/TestFlashLoanGateway.sol +49 -0
  36. package/contracts/test/TestMathUtil.sol +40 -0
  37. package/contracts/test/TestToken.sol +45 -0
  38. package/contracts/test/TestWcBTC.sol +35 -0
  39. package/contracts/utils/MathUtil.sol +61 -0
  40. package/dist/index.d.mts +8761 -0
  41. package/dist/index.d.ts +8761 -0
  42. package/dist/index.js +11119 -0
  43. package/dist/index.mjs +11073 -0
  44. package/exports/abis/MintingHubV2/PositionFactoryV2.ts +90 -0
  45. package/exports/abis/MintingHubV2/PositionRoller.ts +183 -0
  46. package/exports/abis/MintingHubV2/PositionV2.ts +999 -0
  47. package/exports/abis/core/CoinLendingGateway.ts +427 -0
  48. package/exports/abis/core/Equity.ts +1286 -0
  49. package/exports/abis/core/FrontendGateway.ts +906 -0
  50. package/exports/abis/core/JuiceDollar.ts +1366 -0
  51. package/exports/abis/core/MintingHubGateway.ts +865 -0
  52. package/exports/abis/core/SavingsGateway.ts +559 -0
  53. package/exports/abis/core/SavingsVaultJUSD.ts +920 -0
  54. package/exports/abis/utils/ERC20.ts +310 -0
  55. package/exports/abis/utils/ERC20PermitLight.ts +520 -0
  56. package/exports/abis/utils/Leadrate.ts +175 -0
  57. package/exports/abis/utils/MintingHubV2.ts +682 -0
  58. package/exports/abis/utils/Ownable.ts +76 -0
  59. package/exports/abis/utils/Savings.ts +453 -0
  60. package/exports/abis/utils/StablecoinBridge.ts +209 -0
  61. package/exports/abis/utils/StartUSD.ts +315 -0
  62. package/exports/abis/utils/UniswapV3Pool.ts +638 -0
  63. package/exports/address.config.ts +48 -0
  64. package/exports/index.ts +28 -0
  65. 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
+ }