@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,69 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {Position} from "./Position.sol";
5
+ import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
6
+
7
+ contract PositionFactory {
8
+ /**
9
+ * Create a completely new position in a newly deployed contract.
10
+ * Must be called through the minting hub to be recognized as a valid position.
11
+ */
12
+ function createNewPosition(
13
+ address _owner,
14
+ address _jusd,
15
+ address _collateral,
16
+ uint256 _minCollateral,
17
+ uint256 _initialLimit,
18
+ uint40 _initPeriod,
19
+ uint40 _duration,
20
+ uint40 _challengePeriod,
21
+ uint24 _riskPremiumPPM,
22
+ uint256 _liqPrice,
23
+ uint24 _reserve
24
+ ) external returns (address) {
25
+ return
26
+ address(
27
+ new Position(
28
+ _owner,
29
+ msg.sender,
30
+ _jusd,
31
+ _collateral,
32
+ _minCollateral,
33
+ _initialLimit,
34
+ _initPeriod,
35
+ _duration,
36
+ _challengePeriod,
37
+ _riskPremiumPPM,
38
+ _liqPrice,
39
+ _reserve
40
+ )
41
+ );
42
+ }
43
+
44
+ /**
45
+ * @notice Clone an existing position. This can be a clone of another clone,
46
+ * or an original position.
47
+ * @param _parent address of the position we want to clone
48
+ * @return address of the newly created clone position
49
+ */
50
+ function clonePosition(address _parent) external returns (address) {
51
+ Position parent = Position(_parent);
52
+ parent.assertCloneable();
53
+ Position clone = Position(_createClone(parent.original()));
54
+ return address(clone);
55
+ }
56
+
57
+ // github.com/optionality/clone-factory/blob/32782f82dfc5a00d103a7e61a17a5dedbd1e8e9d/contracts/CloneFactory.sol
58
+ function _createClone(address target) internal returns (address result) {
59
+ bytes20 targetBytes = bytes20(target);
60
+ assembly {
61
+ let clone := mload(0x40)
62
+ mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
63
+ mstore(add(clone, 0x14), targetBytes)
64
+ mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
65
+ result := create(0, clone, 0x37)
66
+ }
67
+ require(result != address(0), "ERC1167: create failed");
68
+ }
69
+ }
@@ -0,0 +1,159 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IJuiceDollar} from "../interface/IJuiceDollar.sol";
5
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
+ import {IMintingHubGateway} from "../gateway/interface/IMintingHubGateway.sol";
8
+ import {IMintingHub} from "./interface/IMintingHub.sol";
9
+ import {IPosition} from "./interface/IPosition.sol";
10
+ import {IReserve} from "../interface/IReserve.sol";
11
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
12
+
13
+ /**
14
+ * @title PositionRoller
15
+ *
16
+ * Helper to roll over a debt from an old position to a new one.
17
+ * Both positions should have the same collateral. Otherwise, it does not make much sense.
18
+ */
19
+ contract PositionRoller {
20
+ IJuiceDollar private jusd;
21
+
22
+ error NotOwner(address pos);
23
+ error NotPosition(address pos);
24
+ error Log(uint256, uint256, uint256);
25
+
26
+ event Roll(address source, uint256 collWithdraw, uint256 repay, address target, uint256 collDeposit, uint256 mint);
27
+
28
+ constructor(address jusd_) {
29
+ jusd = IJuiceDollar(jusd_);
30
+ }
31
+
32
+ /**
33
+ * Convenience method to roll an old position into a new one.
34
+ *
35
+ * Pre-condition: an allowance for the roller to spend the collateral asset on behalf of the caller,
36
+ * i.e., one should set collateral.approve(roller, collateral.balanceOf(sourcePosition)).
37
+ *
38
+ * The following is assumed:
39
+ * - If the limit of the target position permits, the user wants to roll everything.
40
+ * - The user does not want to add additional collateral, but excess collateral is returned.
41
+ * - If not enough can be minted in the new position, it is acceptable for the roller to use JUSD from the msg.sender.
42
+ */
43
+ function rollFully(IPosition source, IPosition target) external {
44
+ rollFullyWithExpiration(source, target, target.expiration());
45
+ }
46
+
47
+ /**
48
+ * Like rollFully, but with a custom expiration date for the new position.
49
+ */
50
+ function rollFullyWithExpiration(IPosition source, IPosition target, uint40 expiration) public {
51
+ require(source.collateral() == target.collateral());
52
+ uint256 principal = source.principal();
53
+ uint256 interest = source.getInterest();
54
+ uint256 usableMint = source.getUsableMint(principal) + interest; // Roll interest into principal
55
+ uint256 mintAmount = target.getMintAmount(usableMint);
56
+ uint256 collateralToWithdraw = IERC20(source.collateral()).balanceOf(address(source));
57
+ uint256 targetPrice = target.price();
58
+ uint256 depositAmount = (mintAmount * 10 ** 18 + targetPrice - 1) / targetPrice; // round up
59
+ if (depositAmount > collateralToWithdraw) {
60
+ // If we need more collateral than available from the old position, we opt for taking
61
+ // the missing funds from the caller instead of requiring additional collateral.
62
+ depositAmount = collateralToWithdraw;
63
+ mintAmount = (depositAmount * target.price()) / 10 ** 18; // round down, rest will be taken from caller
64
+ }
65
+
66
+ roll(source, principal + interest, collateralToWithdraw, target, mintAmount, depositAmount, expiration);
67
+ }
68
+
69
+ /**
70
+ * Rolls the source position into the target position using a flash loan.
71
+ * Both the source and the target position must recognize this roller.
72
+ * It is the responsibility of the caller to ensure that both positions are valid contracts.
73
+ *
74
+ * @param source The source position, must be owned by the msg.sender.
75
+ * @param repay The amount of principal to repay from the source position using a flash loan, freeing up some or all collateral .
76
+ * @param collWithdraw Collateral to move from the source position to the msg.sender.
77
+ * @param target The target position. If not owned by msg.sender or if it does not have the desired expiration,
78
+ * it is cloned to create a position owned by the msg.sender.
79
+ * @param mint The amount to be minted from the target position using collateral from msg.sender.
80
+ * @param collDeposit The amount of collateral to be sent from msg.sender to the target position.
81
+ * @param expiration The desired expiration date for the target position.
82
+ */
83
+ function roll(
84
+ IPosition source,
85
+ uint256 repay,
86
+ uint256 collWithdraw,
87
+ IPosition target,
88
+ uint256 mint,
89
+ uint256 collDeposit,
90
+ uint40 expiration
91
+ ) public valid(source) valid(target) own(source) {
92
+ jusd.mint(address(this), repay); // take a flash loan
93
+ uint256 used = source.repay(repay);
94
+ source.withdrawCollateral(msg.sender, collWithdraw);
95
+ if (mint > 0) {
96
+ IERC20 targetCollateral = IERC20(target.collateral());
97
+ if (Ownable(address(target)).owner() != msg.sender || expiration != target.expiration()) {
98
+ targetCollateral.transferFrom(msg.sender, address(this), collDeposit); // get the new collateral
99
+ targetCollateral.approve(target.hub(), collDeposit); // approve the new collateral and clone:
100
+ target = _cloneTargetPosition(target, source, collDeposit, mint, expiration);
101
+ } else {
102
+ // We can roll into the provided existing position.
103
+ // We do not verify whether the target position was created by the known minting hub in order
104
+ // to allow positions to be rolled into future versions of the minting hub.
105
+ targetCollateral.transferFrom(msg.sender, address(target), collDeposit);
106
+ target.mint(msg.sender, mint);
107
+ }
108
+ }
109
+
110
+ // Transfer remaining flash loan to caller for repayment
111
+ if (repay > used) {
112
+ jusd.transfer(msg.sender, repay - used);
113
+ }
114
+
115
+ jusd.burnFrom(msg.sender, repay); // repay the flash loan
116
+ emit Roll(address(source), collWithdraw, repay, address(target), collDeposit, mint);
117
+ }
118
+
119
+ /**
120
+ * Clones the target position and mints the specified amount using the given collateral.
121
+ */
122
+ function _cloneTargetPosition (
123
+ IPosition target,
124
+ IPosition source,
125
+ uint256 collDeposit,
126
+ uint256 mint,
127
+ uint40 expiration
128
+ ) internal returns (IPosition) {
129
+ if (IERC165(target.hub()).supportsInterface(type(IMintingHubGateway).interfaceId)) {
130
+ bytes32 frontendCode = IMintingHubGateway(target.hub()).GATEWAY().getPositionFrontendCode(
131
+ address(source)
132
+ );
133
+ return IPosition(
134
+ IMintingHubGateway(target.hub()).clone(
135
+ msg.sender,
136
+ address(target),
137
+ collDeposit,
138
+ mint,
139
+ expiration,
140
+ frontendCode // use the same frontend code
141
+ )
142
+ );
143
+ } else {
144
+ return IPosition(
145
+ IMintingHub(target.hub()).clone(msg.sender, address(target), collDeposit, mint, expiration)
146
+ );
147
+ }
148
+ }
149
+
150
+ modifier own(IPosition pos) {
151
+ if (Ownable(address(pos)).owner() != msg.sender) revert NotOwner(address(pos));
152
+ _;
153
+ }
154
+
155
+ modifier valid(IPosition pos) {
156
+ if (jusd.getPositionParent(address(pos)) == address(0x0)) revert NotPosition(address(pos));
157
+ _;
158
+ }
159
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.10;
3
+
4
+ import {ILeadrate} from "../../interface/ILeadrate.sol";
5
+ import {IPosition} from "./IPosition.sol";
6
+ import {PositionRoller} from "../PositionRoller.sol";
7
+
8
+ interface IMintingHub {
9
+ function RATE() external view returns (ILeadrate);
10
+
11
+ function ROLLER() external view returns (PositionRoller);
12
+
13
+ function challenge(
14
+ address _positionAddr,
15
+ uint256 _collateralAmount,
16
+ uint256 minimumPrice
17
+ ) external returns (uint256);
18
+
19
+ function bid(uint32 _challengeNumber, uint256 size, bool postponeCollateralReturn) external;
20
+
21
+ function returnPostponedCollateral(address collateral, address target) external;
22
+
23
+ function buyExpiredCollateral(IPosition pos, uint256 upToAmount) external returns (uint256);
24
+
25
+ function clone(address owner, address parent, uint256 _initialCollateral, uint256 _initialMint, uint40 expiration) external returns (address);
26
+ }
@@ -0,0 +1,90 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+
6
+ interface IPosition {
7
+ function hub() external view returns (address);
8
+
9
+ function collateral() external view returns (IERC20);
10
+
11
+ function minimumCollateral() external view returns (uint256);
12
+
13
+ function price() external view returns (uint256);
14
+
15
+ function virtualPrice() external view returns (uint256);
16
+
17
+ function challengedAmount() external view returns (uint256);
18
+
19
+ function original() external view returns (address);
20
+
21
+ function expiration() external view returns (uint40);
22
+
23
+ function cooldown() external view returns (uint40);
24
+
25
+ function limit() external view returns (uint256);
26
+
27
+ function challengePeriod() external view returns (uint40);
28
+
29
+ function start() external view returns (uint40);
30
+
31
+ function riskPremiumPPM() external view returns (uint24);
32
+
33
+ function reserveContribution() external view returns (uint24);
34
+
35
+ function principal() external view returns (uint256);
36
+
37
+ function interest() external view returns (uint256);
38
+
39
+ function lastAccrual() external view returns (uint40);
40
+
41
+ function initialize(address parent, uint40 _expiration) external;
42
+
43
+ function assertCloneable() external;
44
+
45
+ function notifyMint(uint256 mint_) external;
46
+
47
+ function notifyRepaid(uint256 repaid_) external;
48
+
49
+ function availableForClones() external view returns (uint256);
50
+
51
+ function availableForMinting() external view returns (uint256);
52
+
53
+ function deny(address[] calldata helpers, string calldata message) external;
54
+
55
+ function getUsableMint(uint256 totalMint) external view returns (uint256);
56
+
57
+ function getMintAmount(uint256 usableMint) external view returns (uint256);
58
+
59
+ function adjust(uint256 newMinted, uint256 newCollateral, uint256 newPrice) external;
60
+
61
+ function adjustPrice(uint256 newPrice) external;
62
+
63
+ function mint(address target, uint256 amount) external;
64
+
65
+ function getDebt() external view returns (uint256);
66
+
67
+ function getInterest() external view returns (uint256);
68
+
69
+ function repay(uint256 amount) external returns (uint256);
70
+
71
+ function repayFull() external returns (uint256);
72
+
73
+ function forceSale(address buyer, uint256 colAmount, uint256 proceeds) external;
74
+
75
+ function withdraw(address token, address target, uint256 amount) external;
76
+
77
+ function withdrawCollateral(address target, uint256 amount) external;
78
+
79
+ function transferChallengedCollateral(address target, uint256 amount) external;
80
+
81
+ function challengeData() external view returns (uint256 liqPrice, uint40 phase);
82
+
83
+ function notifyChallengeStarted(uint256 size, uint256 _price) external;
84
+
85
+ function notifyChallengeAverted(uint256 size) external;
86
+
87
+ function notifyChallengeSucceeded(
88
+ uint256 _size
89
+ ) external returns (address, uint256, uint256, uint256, uint32);
90
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ interface IPositionFactory {
5
+ function createNewPosition(
6
+ address _owner,
7
+ address _jusd,
8
+ address _collateral,
9
+ uint256 _minCollateral,
10
+ uint256 _initialLimit,
11
+ uint40 _initPeriod,
12
+ uint40 _duration,
13
+ uint40 _challengePeriod,
14
+ uint24 _riskPremiumPPM,
15
+ uint256 _liqPrice,
16
+ uint24 _reserve
17
+ ) external returns (address);
18
+
19
+ function clonePosition(address _parent) external returns (address);
20
+ }
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {IJuiceDollar} from "./interface/IJuiceDollar.sol";
6
+ import {IReserve} from "./interface/IReserve.sol";
7
+ import {Leadrate} from "./Leadrate.sol";
8
+
9
+ /**
10
+ * @title Savings
11
+ *
12
+ * Module to enable savings based on a Leadrate ("Leitzins") module.
13
+ *
14
+ * As the interest rate changes, the speed at which 'ticks' are accumulated is
15
+ * adjusted. The ticks counter serves as the basis for calculating the interest
16
+ * due for the individual accounts.
17
+ */
18
+ contract Savings is Leadrate {
19
+ IERC20 public immutable jusd;
20
+
21
+ mapping(address => Account) public savings;
22
+
23
+ struct Account {
24
+ uint192 saved;
25
+ uint64 ticks;
26
+ }
27
+
28
+ event Saved(address indexed account, uint192 amount);
29
+ event InterestCollected(address indexed account, uint256 interest);
30
+ event Withdrawn(address indexed account, uint192 amount);
31
+
32
+ // The module is considered disabled if the interest is zero or about to become zero within three days.
33
+ error ModuleDisabled();
34
+
35
+ constructor(IJuiceDollar jusd_, uint24 initialRatePPM) Leadrate(IReserve(jusd_.reserve()), initialRatePPM) {
36
+ jusd = IERC20(jusd_);
37
+ }
38
+
39
+ /**
40
+ * Shortcut for refreshBalance(msg.sender)
41
+ */
42
+ function refreshMyBalance() public returns (uint192) {
43
+ return refreshBalance(msg.sender);
44
+ }
45
+
46
+ /**
47
+ * Collects the accrued interest and adds it to the account.
48
+ *
49
+ * It can be beneficial to do so every now and then in order to start collecting
50
+ * interest on the accrued interest.
51
+ */
52
+ function refreshBalance(address owner) public returns (uint192) {
53
+ return refresh(owner).saved;
54
+ }
55
+
56
+ function refresh(address accountOwner) virtual internal returns (Account storage) {
57
+ Account storage account = savings[accountOwner];
58
+ uint64 ticks = currentTicks();
59
+ if (ticks > account.ticks) {
60
+ uint192 earnedInterest = calculateInterest(account, ticks);
61
+ if (earnedInterest > 0) {
62
+ // collect interest as you go and trigger accounting event
63
+ (IJuiceDollar(address(jusd))).distributeProfits(address(this), earnedInterest);
64
+ account.saved += earnedInterest;
65
+ emit InterestCollected(accountOwner, earnedInterest);
66
+ }
67
+ account.ticks = ticks;
68
+ }
69
+ return account;
70
+ }
71
+
72
+ function accruedInterest(address accountOwner) public view returns (uint192) {
73
+ return accruedInterest(accountOwner, block.timestamp);
74
+ }
75
+
76
+ function accruedInterest(address accountOwner, uint256 timestamp) public view returns (uint192) {
77
+ Account memory account = savings[accountOwner];
78
+ return calculateInterest(account, ticks(timestamp));
79
+ }
80
+
81
+ function calculateInterest(Account memory account, uint64 ticks) public view returns (uint192) {
82
+ if (ticks <= account.ticks || account.ticks == 0) {
83
+ return 0;
84
+ } else {
85
+ uint192 earnedInterest = uint192((uint256(ticks - account.ticks) * account.saved) / 1_000_000 / 365 days);
86
+ uint256 equity = IJuiceDollar(address(jusd)).equity();
87
+ if (earnedInterest > equity) {
88
+ return uint192(equity); // safe conversion as equity is smaller than uint192 earnedInterest
89
+ } else {
90
+ return earnedInterest;
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Save 'amount'.
97
+ */
98
+ function save(uint192 amount) public {
99
+ save(msg.sender, amount);
100
+ }
101
+
102
+ function adjust(uint192 targetAmount) public {
103
+ Account storage balance = refresh(msg.sender);
104
+ if (balance.saved < targetAmount) {
105
+ save(targetAmount - balance.saved);
106
+ } else if (balance.saved > targetAmount) {
107
+ withdraw(msg.sender, balance.saved - targetAmount);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Send 'amount' to the account of the provided owner.
113
+ */
114
+ function save(address owner, uint192 amount) public {
115
+ if (currentRatePPM == 0) revert ModuleDisabled();
116
+ if (nextRatePPM == 0 && (nextChange <= block.timestamp)) revert ModuleDisabled();
117
+ Account storage balance = refresh(owner);
118
+ jusd.transferFrom(msg.sender, address(this), amount);
119
+ assert(balance.ticks >= currentTicks()); // @dev: should not differ, since there is no shift of interests
120
+ balance.saved += amount;
121
+ emit Saved(owner, amount);
122
+ }
123
+
124
+ /**
125
+ * Withdraw up to 'amount' to the target address.
126
+ * When trying to withdraw more than available, all that is available is withdrawn.
127
+ * Returns the actually transferred amount.
128
+ */
129
+ function withdraw(address target, uint192 amount) public returns (uint256) {
130
+ Account storage account = refresh(msg.sender);
131
+ if (amount >= account.saved) {
132
+ amount = account.saved;
133
+ delete savings[msg.sender];
134
+ } else {
135
+ account.saved -= amount;
136
+ }
137
+ jusd.transfer(target, amount);
138
+ emit Withdrawn(msg.sender, amount);
139
+ return amount;
140
+ }
141
+ }
@@ -0,0 +1,140 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
5
+ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
6
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7
+ import {ERC4626, ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
8
+
9
+ import {ISavingsJUSD} from "./interface/ISavingsJUSD.sol";
10
+
11
+ /**
12
+ * @title SavingsVaultJUSD
13
+ * @notice ERC-4626-compatible vault adapter for the JUSD Savings module.
14
+ * This vault tracks interest-bearing deposits using a custom price-based mechanism,
15
+ * where share value increases over time as interest accrues.
16
+ *
17
+ * @dev The vault mitigates dilution and price manipulation attacks on empty vaults
18
+ * (a known vulnerability in ERC-4626) by using an explicit price model that starts at 1e18,
19
+ * instead of relying on the default totalAssets / totalSupply ratio when supply is zero.
20
+ *
21
+ * Interest is recognized through a manual `_accrueInterest()` call, which updates the internal
22
+ * price based on newly accrued interest.
23
+ */
24
+ contract SavingsVaultJUSD is ERC4626 {
25
+ using Math for uint256;
26
+ using SafeCast for uint256;
27
+
28
+ ISavingsJUSD public immutable SAVINGS;
29
+ uint256 public totalClaimed;
30
+
31
+ event InterestClaimed(uint256 interest, uint256 totalClaimed);
32
+
33
+ constructor(
34
+ IERC20 _coin,
35
+ ISavingsJUSD _savings,
36
+ string memory _name,
37
+ string memory _symbol
38
+ ) ERC4626(_coin) ERC20(_name, _symbol) {
39
+ SAVINGS = _savings;
40
+ // Approve the savings contract to transfer tokens from this vault
41
+ SafeERC20.forceApprove(_coin, address(_savings), type(uint256).max);
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------------------
45
+
46
+ /// @notice Returns the current savings account state for this contract
47
+ /// @dev Uses the external `savings` contract to fetch the account details
48
+ function info() public view returns (ISavingsJUSD.Account memory) {
49
+ return SAVINGS.savings(address(this));
50
+ }
51
+
52
+ /// @notice Returns the current price per share of the contract
53
+ /// @dev If no shares exist, it defaults to 1 ether (implying 1:1 value)
54
+ function price() public view returns (uint256) {
55
+ uint256 totalShares = totalSupply();
56
+ if (totalShares == 0) return 1 ether;
57
+ return (totalAssets() * 1 ether) / totalShares;
58
+ }
59
+
60
+ /// @notice Calculates the accrued interest for this contract
61
+ function _interest() internal view returns (uint256) {
62
+ return SAVINGS.accruedInterest(address(this));
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------------------
66
+ // Override functions of ERC4626
67
+
68
+ function totalAssets() public view override returns (uint256) {
69
+ return SAVINGS.savings(address(this)).saved + _interest();
70
+ }
71
+
72
+ function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
73
+ return assets.mulDiv(1 ether, price(), rounding);
74
+ }
75
+
76
+ function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
77
+ return shares.mulDiv(price(), 1 ether, rounding);
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------------------
81
+
82
+ function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override {
83
+ _accrueInterest();
84
+
85
+ // If _asset is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
86
+ // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
87
+ // calls the vault, which is assumed not malicious.
88
+ //
89
+ // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
90
+ // assets are transferred and before the shares are minted, which is a valid state.
91
+ // slither-disable-next-line reentrancy-no-eth
92
+ SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets);
93
+
94
+ SAVINGS.save(assets.toUint192());
95
+
96
+ _mint(receiver, shares);
97
+
98
+ emit Deposit(caller, receiver, assets, shares);
99
+ }
100
+
101
+ function _withdraw(
102
+ address caller,
103
+ address receiver,
104
+ address owner,
105
+ uint256 assets,
106
+ uint256 shares
107
+ ) internal virtual override {
108
+ _accrueInterest();
109
+
110
+ if (caller != owner) {
111
+ _spendAllowance(owner, caller, shares);
112
+ }
113
+
114
+ // If _asset is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
115
+ // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
116
+ // calls the vault, which is assumed not malicious.
117
+ //
118
+ // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
119
+ // shares are burned and after the assets are transferred, which is a valid state.
120
+ _burn(owner, shares);
121
+
122
+ SAVINGS.withdraw(receiver, assets.toUint192());
123
+
124
+ emit Withdraw(caller, receiver, owner, assets, shares);
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------------------
128
+
129
+ /// @notice Internal function to accrue and record interest if available
130
+ /// @dev Retrieves net interest via `_interest()`
131
+ /// @dev If there is interest and shares exist, adds it to `totalClaimed` and emits an event
132
+ function _accrueInterest() internal {
133
+ uint256 interest = _interest();
134
+
135
+ if (interest > 0 && totalSupply() > 0) {
136
+ totalClaimed += interest;
137
+ emit InterestClaimed(interest, totalClaimed);
138
+ }
139
+ }
140
+ }