Skip to content

Commit 88cc207

Browse files
committed
Add documentation, refactor, and improve protocol logic
This commit adds extensive NatSpec documentation to all core contracts and interfaces, refactors event and error names for consistency, and improves protocol logic in NostalgicController, NostalgicFactory, NostalgicJumpRateModel, and NostalgicPool. It also introduces .solhint config files, updates README with project details, and adds dead shares minting logic to pools. Test files are updated to match new function names and behaviors.
1 parent 5138921 commit 88cc207

16 files changed

Lines changed: 550 additions & 107 deletions

.solhint.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": "solhint:recommended",
3+
"rules": {
4+
"func-visibility": [
5+
"error",
6+
{
7+
"ignoreConstructors": true
8+
}
9+
],
10+
"max-line-length": [
11+
"warn",
12+
300
13+
],
14+
"import-path-check": "off",
15+
"use-natspec": "off",
16+
"gas-strict-inequalities": "off"
17+
}
18+
}

.solhintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
test/
2+
script/
3+
lib/

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# nostalgia
2+
3+
Decentralized lending protocol inspired by Compound v2.
4+
It allows users to supply and borrow assets through smart contracts, earning interest algorithmically based on market dynamics.
5+
Motivation for this: **"I Don't Understand Anything I Can't Build"**.
6+
7+
![art](./assets/art.gif)
8+
9+
## 🧱 Overview
10+
11+
- **`NostalgicController.sol`** – Core risk management and market control logic.
12+
- **`NostalgicFactory.sol`** – Responsible for deploying and managing new pool instances.
13+
- **`NostalgicJumpRateModel.sol`** – Implements an interest rate model with a jump multiplier, adjusting rates dynamically based on utilization.
14+
- **`NostalgicPool.sol`** – Core pool contract for supplying and borrowing assets.
15+
16+
## 🧪 Testing
17+
18+
The project uses **[Foundry](https://book.getfoundry.sh/)** for testing and development.
19+
Tests are located under the `test/` directory, with mocks and unit tests for each module.
20+
21+
```
22+
╭--------------------------------+-------------------+-------------------+-----------------+-----------------╮
23+
| File | % Lines | % Statements | % Branches | % Funcs |
24+
+============================================================================================================+
25+
| src/NostalgicController.sol | 100.00% (80/80) | 100.00% (86/86) | 100.00% (30/30) | 100.00% (16/16) |
26+
|--------------------------------+-------------------+-------------------+-----------------+-----------------|
27+
| src/NostalgicFactory.sol | 100.00% (23/23) | 100.00% (21/21) | 100.00% (2/2) | 100.00% (5/5) |
28+
|--------------------------------+-------------------+-------------------+-----------------+-----------------|
29+
| src/NostalgicJumpRateModel.sol | 100.00% (12/12) | 100.00% (13/13) | 100.00% (2/2) | 100.00% (2/2) |
30+
|--------------------------------+-------------------+-------------------+-----------------+-----------------|
31+
| src/NostalgicPool.sol | 100.00% (145/145) | 100.00% (145/145) | 100.00% (33/33) | 100.00% (28/28) |
32+
|--------------------------------+-------------------+-------------------+-----------------+-----------------|
33+
| Total | 100.00% (260/260) | 100.00% (265/265) | 100.00% (67/67) | 100.00% (51/51) |
34+
╰--------------------------------+-------------------+-------------------+-----------------+-----------------╯
35+
```
36+
37+
Run the full test suite:
38+
39+
```bash
40+
forge test
41+
```
42+
43+
For more detailed output:
44+
45+
```bash
46+
forge test -vvv
47+
```
48+
49+
## 🧰 Project Structure
50+
51+
```
52+
src/
53+
├─ interfaces/
54+
│ ├─ NostalgicController.sol
55+
│ ├─ NostalgicFactory.sol
56+
│ ├─ NostalgicJumpRateModel.sol
57+
└─ └─ NostalgicPool.sol
58+
```
59+
60+
## ⚙️ Setup
61+
62+
Clone the repository and install dependencies:
63+
64+
```bash
65+
git clone https://github.com/typicalHuman/nostalgia.git
66+
cd nostalgia
67+
forge install
68+
```
69+
70+
Build the contracts:
71+
72+
```bash
73+
forge build
74+
```

assets/art.gif

30.2 MB
Loading

src/NostalgicController.sol

Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ pragma solidity ^0.8.28;
44
import {INostalgicPool} from "./interfaces/INostalgicPool.sol";
55
import {INostalgicController} from "./interfaces/INostalgicController.sol";
66
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
7+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
78
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
8-
import {console} from "forge-std/console.sol";
99

1010
// /$$ /$$ /$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$
1111
// | $$$ | $$ | $$ | $$ |__/ /$$__ $$ | $$ | $$| $$
@@ -18,23 +18,44 @@ import {console} from "forge-std/console.sol";
1818
// /$$ \ $$
1919
// | $$$$$$/
2020
// \______/
21-
//
22-
contract NostalgicController is AccessControl, INostalgicController {
21+
22+
/// @title Nostalgic Controller
23+
/// @notice Manages user interactions with Nostalgic Pools
24+
/// @author typicalHuman
25+
contract NostalgicController is
26+
AccessControl,
27+
ReentrancyGuard,
28+
INostalgicController
29+
{
2330
using Math for uint256;
2431

2532
uint256 internal constant SCALING_FACTOR = 1e18;
2633

34+
/// @notice Liquidation incentive for liquidators
35+
uint256 public constant LIQUIDATION_INCENTIVE = 0.04e18; // 4%
36+
37+
/// @notice Protocol liquidation fee
38+
uint256 public constant PROTOCOL_LIQUIDATION_FEE = 0.01e18; // 1%
39+
40+
/// @notice Address of the Nostalgic factory contract
41+
address public factory;
42+
43+
/// @notice Mapping of existing pool addresses
2744
mapping(INostalgicPool pool => bool exists) public existingPools;
45+
46+
/// @notice Mapping of user addresses to their entered pool addresses
2847
mapping(address user => INostalgicPool[] pools) public userEnteredMarkets;
48+
49+
/// @notice Mapping of user addresses to their interaction status with each pool
2950
mapping(address user => mapping(INostalgicPool pool => bool hasInteracted))
3051
public userMarketEntries;
31-
mapping(address asset => uint256 collateralFactor) public collateralFactors;
3252

33-
address public factory;
34-
35-
uint256 public liquidationIncentive = 0.04e18; // 4%
36-
uint256 public protocolLiquidationFee = 0.01e18; // 1%
53+
/// @notice Mapping of asset addresses to their collateral factors
54+
mapping(address asset => uint256 collateralFactor) public collateralFactors;
3755

56+
/// @notice Constructor for the NostalgicController contract
57+
/// @param initialAdmin The address of the initial admin
58+
/// @param _factory The address of the Nostalgic factory contract
3859
constructor(address initialAdmin, address _factory) {
3960
require(
4061
initialAdmin != address(0) && _factory != address(0),
@@ -44,11 +65,14 @@ contract NostalgicController is AccessControl, INostalgicController {
4465
_grantRole(DEFAULT_ADMIN_ROLE, initialAdmin);
4566
}
4667

47-
modifier onlyOwnerOrFactory() {
48-
_onlyOwnerOrFactory();
68+
/// @notice Modifier to restrict access to the admin or factory
69+
modifier onlyAdminOrFactory() {
70+
_onlyAdminOrFactory();
4971
_;
5072
}
5173

74+
/// @notice Updates the address of the Nostalgic factory contract
75+
/// @param newFactory The address of the new Nostalgic factory contract
5276
function updateFactory(
5377
address newFactory
5478
) public onlyRole(DEFAULT_ADMIN_ROLE) {
@@ -57,29 +81,36 @@ contract NostalgicController is AccessControl, INostalgicController {
5781
emit FactoryUpdate(newFactory);
5882
}
5983

84+
/// @inheritdoc INostalgicController
6085
function isHealthy(address user) public view returns (bool) {
6186
(uint256 totalUsdCollateral, uint256 totalUsdBorrow) = userPosition(
6287
user
6388
);
6489
return totalUsdCollateral >= totalUsdBorrow;
6590
}
6691

92+
/// @inheritdoc INostalgicController
6793
function userPosition(
6894
address user
6995
) public view returns (uint256 totalUsdCollateral, uint256 totalUsdBorrow) {
7096
INostalgicPool[] memory investedPools = userEnteredMarkets[user];
71-
for (uint i = 0; i < investedPools.length; i++) {
72-
uint256 userCollateral = investedPools[i].getTokenValueInUSD(
73-
investedPools[i].userLends(user)
97+
for (uint256 i = 0; i < investedPools.length; ) {
98+
INostalgicPool pool = investedPools[i];
99+
100+
uint256 userCollateral = pool.getTokenValueInUSD(
101+
pool.userLends(user)
74102
);
75-
totalUsdBorrow += investedPools[i].getTokenValueInUSD(
76-
investedPools[i].scaledBorrowPosition(user)
103+
totalUsdBorrow += pool.getTokenValueInUSD(
104+
pool.scaledBorrowPosition(user)
77105
);
78106

79-
totalUsdCollateral += getUsdValue(investedPools[i], userCollateral);
107+
totalUsdCollateral += getUsdValue(pool, userCollateral);
108+
unchecked {
109+
++i;
110+
}
80111
}
81112
}
82-
113+
/// @inheritdoc INostalgicController
83114
function canBorrow(
84115
INostalgicPool pool,
85116
address user,
@@ -95,12 +126,13 @@ contract NostalgicController is AccessControl, INostalgicController {
95126
return totalUsdCollateral > totalUsdBorrow;
96127
}
97128

129+
/// @inheritdoc INostalgicController
98130
function liquidateBorrower(
99131
address borrower,
100132
address liquidator,
101133
uint256 repaidAmount,
102134
INostalgicPool collateralPool
103-
) external {
135+
) external nonReentrant {
104136
require(
105137
existingPools[INostalgicPool(msg.sender)] &&
106138
existingPools[collateralPool],
@@ -113,9 +145,9 @@ contract NostalgicController is AccessControl, INostalgicController {
113145
repaidAmountUsd
114146
);
115147
uint256 liquidatorIncentive = (repaidInCollateralToken *
116-
liquidationIncentive) / SCALING_FACTOR;
148+
LIQUIDATION_INCENTIVE) / SCALING_FACTOR;
117149
uint256 protocolReward = (repaidInCollateralToken *
118-
protocolLiquidationFee) / SCALING_FACTOR;
150+
PROTOCOL_LIQUIDATION_FEE) / SCALING_FACTOR;
119151
uint256 liquidatorReward = liquidatorIncentive +
120152
repaidInCollateralToken;
121153
require(
@@ -131,11 +163,12 @@ contract NostalgicController is AccessControl, INostalgicController {
131163
);
132164
}
133165

166+
/// @inheritdoc INostalgicController
134167
function interactWithPool(address user) external {
135168
require(existingPools[INostalgicPool(msg.sender)], PoolNotFound());
136169
uint256 lends = INostalgicPool(msg.sender).userLends(user);
137170
uint256 borrows = INostalgicPool(msg.sender).userBorrows(user);
138-
bool toAdd = lends + borrows > 0 ? true : false;
171+
bool toAdd = lends + borrows > 0;
139172
if (toAdd) {
140173
if (userMarketEntries[user][INostalgicPool(msg.sender)]) return;
141174
userMarketEntries[user][INostalgicPool(msg.sender)] = true;
@@ -147,83 +180,113 @@ contract NostalgicController is AccessControl, INostalgicController {
147180
_removePool(user, INostalgicPool(msg.sender));
148181
}
149182

150-
function addPool(INostalgicPool pool) external onlyOwnerOrFactory {
183+
/// @notice Adds a new Nostalgic pool
184+
/// @param pool The address of the Nostalgic pool to add
185+
function addPool(INostalgicPool pool) external onlyAdminOrFactory {
151186
existingPools[pool] = true;
152187

153-
emit PoolAdded(pool);
188+
emit PoolAdd(pool);
154189
}
155190

191+
/// @notice Removes an existing Nostalgic pool
192+
/// @param pool The address of the Nostalgic pool to remove
156193
function removePool(
157194
INostalgicPool pool
158195
) external onlyRole(DEFAULT_ADMIN_ROLE) {
159196
require(pool.totalLended() == 0, PoolNotEmpty());
160197
existingPools[pool] = false;
161-
emit PoolRemoved(pool);
198+
emit PoolRemove(pool);
162199
}
200+
201+
/// @notice Adds a new asset to the collateral manager
202+
/// @param asset The address of the asset to add
203+
/// @param collateralFactor The collateral factor for the asset
163204
function addAsset(
164205
address asset,
165206
uint256 collateralFactor
166-
) external onlyOwnerOrFactory {
207+
) external onlyAdminOrFactory {
167208
require(collateralFactors[asset] == 0, AssetAlreadyAdded());
168209
require(
169210
collateralFactor <=
170-
SCALING_FACTOR - protocolLiquidationFee - liquidationIncentive,
211+
SCALING_FACTOR -
212+
PROTOCOL_LIQUIDATION_FEE -
213+
LIQUIDATION_INCENTIVE,
171214
InvalidCollateralFactor()
172215
);
173216
collateralFactors[asset] = collateralFactor;
174217

175-
emit AssetAdded(asset, collateralFactor);
218+
emit AssetAdd(asset, collateralFactor);
176219
}
177220

178-
function update(
221+
/// @notice Updates the collateral factor for an existing asset
222+
/// @param asset The address of the asset to update
223+
/// @param collateralFactor The new collateral factor for the asset
224+
function updateCollateralFactor(
179225
address asset,
180226
uint256 collateralFactor
181227
) external onlyRole(DEFAULT_ADMIN_ROLE) {
182228
require(collateralFactors[asset] != 0, AssetNotAdded());
183229
require(
184230
collateralFactor <=
185-
SCALING_FACTOR - protocolLiquidationFee - liquidationIncentive,
231+
SCALING_FACTOR -
232+
PROTOCOL_LIQUIDATION_FEE -
233+
LIQUIDATION_INCENTIVE,
186234
InvalidCollateralFactor()
187235
);
188236
collateralFactors[asset] = collateralFactor;
189237

190-
emit CollateralFactorUpdated(asset, collateralFactor);
238+
emit CollateralFactorUpdate(asset, collateralFactor);
191239
}
192240

241+
/// @notice Removes an existing asset from the collateral manager
242+
/// @param asset The address of the asset to remove
193243
function removeAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
194244
require(collateralFactors[asset] != 0, AssetNotAdded());
195245

196246
collateralFactors[asset] = 0;
197-
emit AssetRemoved(asset);
247+
emit AssetRemove(asset);
198248
}
199249

250+
/// @notice Gets the USD value of a given amount of an asset in a Nostalgic pool
251+
/// @param pool The Nostalgic pool to get the USD value from
252+
/// @param amount The amount of the asset to convert to USD
200253
function getUsdValue(
201254
INostalgicPool pool,
202255
uint256 amount
203256
) public view returns (uint256) {
204257
return amount.mulDiv(collateralFactors[pool.asset()], SCALING_FACTOR);
205258
}
206259

207-
function _onlyOwnerOrFactory() internal view {
260+
/// @notice Checks if the caller is the admin or the factory
261+
function _onlyAdminOrFactory() internal view {
208262
require(
209263
hasRole(DEFAULT_ADMIN_ROLE, msg.sender) || msg.sender == factory,
210264
InvalidCaller()
211265
);
212266
}
213267

268+
/// @notice Removes a Nostalgic pool from a user's entered markets
269+
/// @param user The address of the user
270+
/// @param pool The Nostalgic pool to remove
214271
function _removePool(address user, INostalgicPool pool) internal {
215-
uint length = userEnteredMarkets[user].length;
216-
for (uint i = 0; i < length; i++) {
272+
uint256 length = userEnteredMarkets[user].length;
273+
for (uint256 i = 0; i < length; ) {
217274
if (userEnteredMarkets[user][i] == pool) {
218275
// Shift elements left
219-
for (uint j = i; j < length - 1; j++) {
276+
for (uint256 j = i; j < length - 1; ) {
220277
userEnteredMarkets[user][j] = userEnteredMarkets[user][
221278
j + 1
222279
];
280+
unchecked {
281+
++j;
282+
}
223283
}
224284
userEnteredMarkets[user].pop(); // remove last
225285
break;
226286
}
287+
unchecked {
288+
++i;
289+
}
227290
}
228291
}
229292
}

0 commit comments

Comments
 (0)