Skip to content

Commit 0b4bdae

Browse files
authored
Isolated public pool + ERC4626 Adapter + Audit fixes 4 (#127)
* Add PublicLiquidityPool and ERC4626Adapter, wip * Get fill amount from calldata in Public pool * Add most of the Public pool tests, wip * Complete Public Liquidity Pool tests * Add ERC4626Adapter tests * Add totalDeposited getter to public pool * Revert back to FeeConfig in public pool * Revert back to parsing calldata This reverts commit 930488f. * Fix typo * Add public pool deploy scripts (#128) * Add deploy scripts * Update scripts * Fix chain id in tests * Audit fixes 4 (#135) * Apply audit fixes 1 * Add various fixes * Remove console.log from tests * Merge main and fix tests * Make all tests run * Last part of fixes * Make public pool maxWithdraw and maxRedeem compliant with the EIP4626 * Fix lint * Improve ERC4626Adapter withdraw profit safety assertion
1 parent b5d1b05 commit 0b4bdae

32 files changed

+3325
-104
lines changed

contracts/ERC4626Adapter.sol

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity 0.8.28;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import {IERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
6+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
7+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8+
import {ILiquidityPoolBase} from "./interfaces/ILiquidityPoolBase.sol";
9+
10+
/// @title A middleware contract that allows Sprinter funds to be rebalanced to/from the underlying ERC4626 vault.
11+
/// Same as liquidity pools it is admin managed, and profits from underlying vault are accounted for.
12+
/// @author Oleksii Matiiasevych <oleksii@sprinter.tech>
13+
contract ERC4626Adapter is ILiquidityPoolBase, AccessControl {
14+
using SafeERC20 for IERC20;
15+
16+
bytes32 private constant LIQUIDITY_ADMIN_ROLE = "LIQUIDITY_ADMIN_ROLE";
17+
bytes32 private constant WITHDRAW_PROFIT_ROLE = "WITHDRAW_PROFIT_ROLE";
18+
bytes32 private constant PAUSER_ROLE = "PAUSER_ROLE";
19+
20+
IERC4626 public immutable TARGET_VAULT;
21+
IERC20 public immutable ASSETS;
22+
23+
uint256 public totalDeposited;
24+
bool public paused;
25+
26+
event Deposit(address caller, uint256 amount);
27+
event Withdraw(address caller, address to, uint256 amount);
28+
event Paused(address account);
29+
event Unpaused(address account);
30+
event ProfitWithdrawn(address token, address to, uint256 amount);
31+
32+
error ZeroAddress();
33+
error IncompatibleAssets();
34+
error InsufficientLiquidity();
35+
error EnforcedPause();
36+
error ExpectedPause();
37+
error NoProfit();
38+
error InvalidToken();
39+
40+
constructor(address assets, address targetVault, address admin) {
41+
require(assets != address(0), ZeroAddress());
42+
require(targetVault != address(0), ZeroAddress());
43+
require(admin != address(0), ZeroAddress());
44+
require(assets == IERC4626(targetVault).asset(), IncompatibleAssets());
45+
TARGET_VAULT = IERC4626(targetVault);
46+
ASSETS = IERC20(assets);
47+
_grantRole(DEFAULT_ADMIN_ROLE, admin);
48+
}
49+
50+
modifier whenNotPaused() {
51+
require(!paused, EnforcedPause());
52+
_;
53+
}
54+
55+
modifier whenPaused() {
56+
require(paused, ExpectedPause());
57+
_;
58+
}
59+
60+
function deposit(uint256 amount) external override onlyRole(LIQUIDITY_ADMIN_ROLE) whenNotPaused() {
61+
_deposit(_msgSender(), amount);
62+
}
63+
64+
function depositWithPull(uint256 amount) external override whenNotPaused() {
65+
ASSETS.safeTransferFrom(_msgSender(), address(this), amount);
66+
_deposit(_msgSender(), amount);
67+
}
68+
69+
function withdraw(address to, uint256 amount) override
70+
external
71+
onlyRole(LIQUIDITY_ADMIN_ROLE)
72+
whenNotPaused()
73+
{
74+
require(to != address(0), ZeroAddress());
75+
uint256 deposited = totalDeposited;
76+
require(deposited >= amount, InsufficientLiquidity());
77+
totalDeposited = deposited - amount;
78+
TARGET_VAULT.withdraw(amount, to, address(this));
79+
emit Withdraw(_msgSender(), to, amount);
80+
}
81+
82+
function withdrawProfit(
83+
address[] calldata tokens,
84+
address to
85+
) external override onlyRole(WITHDRAW_PROFIT_ROLE) whenNotPaused() {
86+
require(to != address(0), ZeroAddress());
87+
bool success;
88+
for (uint256 i = 0; i < tokens.length; i++) {
89+
IERC20 token = IERC20(tokens[i]);
90+
uint256 amountToWithdraw = _withdrawProfitLogic(token);
91+
if (amountToWithdraw == 0) continue;
92+
success = true;
93+
token.safeTransfer(to, amountToWithdraw);
94+
emit ProfitWithdrawn(address(token), to, amountToWithdraw);
95+
}
96+
require(success, NoProfit());
97+
}
98+
99+
function pause() external override onlyRole(PAUSER_ROLE) whenNotPaused() {
100+
paused = true;
101+
emit Paused(_msgSender());
102+
}
103+
104+
function unpause() external override onlyRole(PAUSER_ROLE) whenPaused() {
105+
paused = false;
106+
emit Unpaused(_msgSender());
107+
}
108+
109+
function _deposit(address caller, uint256 amount) private {
110+
ASSETS.forceApprove(address(TARGET_VAULT), amount);
111+
TARGET_VAULT.deposit(amount, address(this));
112+
totalDeposited += amount;
113+
emit Deposit(caller, amount);
114+
}
115+
116+
function _withdrawProfitLogic(IERC20 token) internal returns (uint256) {
117+
require(token != IERC20(TARGET_VAULT), InvalidToken());
118+
uint256 localBalance = token.balanceOf(address(this));
119+
if (token == ASSETS) {
120+
uint256 deposited = totalDeposited;
121+
uint256 depositedShares = TARGET_VAULT.previewWithdraw(deposited);
122+
uint256 totalShares = TARGET_VAULT.balanceOf(address(this));
123+
if (totalShares <= depositedShares) return localBalance;
124+
uint256 profit = TARGET_VAULT.redeem(totalShares - depositedShares, address(this), address(this));
125+
assert(TARGET_VAULT.previewRedeem(depositedShares) >= deposited);
126+
return profit + localBalance;
127+
}
128+
return localBalance;
129+
}
130+
}

contracts/LiquidityPool.sol

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
5959
IERC20 immutable public ASSETS;
6060

6161
BitMaps.BitMap private _usedNonces;
62-
uint256 public totalDeposited;
62+
uint256 internal _totalDeposited;
6363

6464
bool public paused;
6565
bool public borrowPaused;
6666
address public mpcAddress;
67+
address public signerAddress;
6768

6869
bytes32 private constant LIQUIDITY_ADMIN_ROLE = "LIQUIDITY_ADMIN_ROLE";
6970
bytes32 private constant WITHDRAW_PROFIT_ROLE = "WITHDRAW_PROFIT_ROLE";
@@ -72,8 +73,6 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
7273
bytes4 constant internal MAGICVALUE = 0x1626ba7e;
7374
IWrappedNativeToken immutable public WRAPPED_NATIVE_TOKEN;
7475

75-
address public signerAddress;
76-
7776
error ZeroAddress();
7877
error InvalidSignature();
7978
error NotEnoughToDeposit();
@@ -137,14 +136,17 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
137136
// Allow native token transfers.
138137
}
139138

140-
function deposit(uint256 amount) external override onlyRole(LIQUIDITY_ADMIN_ROLE) {
139+
/// @notice The liqudity admin is supposed to call this function after transferring exact amount of assets.
140+
/// Supplying amount less than the actual increase will result in the extra funds being treated as profit.
141+
/// Supplying amount greater than the actual increase will result in the future profits treated as deposit.
142+
function deposit(uint256 amount) external virtual override onlyRole(LIQUIDITY_ADMIN_ROLE) {
141143
// called after receiving deposit in USDC
142144
uint256 newBalance = ASSETS.balanceOf(address(this));
143145
require(newBalance >= amount, NotEnoughToDeposit());
144146
_deposit(_msgSender(), amount);
145147
}
146148

147-
function depositWithPull(uint256 amount) external override {
149+
function depositWithPull(uint256 amount) external virtual override {
148150
// pulls USDC from the sender
149151
ASSETS.safeTransferFrom(_msgSender(), address(this), amount);
150152
_deposit(_msgSender(), amount);
@@ -171,6 +173,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
171173
) external override whenNotPaused() whenBorrowNotPaused() {
172174
// - Validate MPC signature
173175
_validateMPCSignatureWithCaller(borrowToken, amount, target, targetCallData, nonce, deadline, signature);
176+
amount = _processBorrowAmount(amount, targetCallData);
174177
(uint256 nativeValue, address actualBorrowToken, bytes memory context) =
175178
_borrow(borrowToken, amount, target, NATIVE_ALLOWED, "");
176179
_afterBorrowLogic(actualBorrowToken, context);
@@ -237,6 +240,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
237240
bytes calldata signature
238241
) external override whenNotPaused() whenBorrowNotPaused() {
239242
_validateMPCSignatureWithCaller(borrowToken, amount, target, targetCallData, nonce, deadline, signature);
243+
amount = _processBorrowAmount(amount, targetCallData);
240244
// Native borrowing is denied because swap() is not payable.
241245
(,, bytes memory context) = _borrow(borrowToken, amount, _msgSender(), NATIVE_DENIED, "");
242246
_afterBorrowLogic(borrowToken, context);
@@ -277,18 +281,19 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
277281

278282
// Admin functions
279283

280-
/// @notice Can withdraw a maximum of totalDeposited. If anything is left, it is meant to be withdrawn through
284+
/// @notice Can withdraw a maximum of _totalDeposited. If anything is left, it is meant to be withdrawn through
281285
/// a withdrawProfit().
282286
function withdraw(address to, uint256 amount)
283287
external
288+
virtual
284289
override
285290
onlyRole(LIQUIDITY_ADMIN_ROLE)
286291
whenNotPaused()
287292
{
288293
require(to != address(0), ZeroAddress());
289-
uint256 deposited = totalDeposited;
294+
uint256 deposited = _totalDeposited;
290295
require(deposited >= amount, InsufficientLiquidity());
291-
totalDeposited = deposited - amount;
296+
_totalDeposited = deposited - amount;
292297
_withdrawLogic(to, amount);
293298
emit Withdraw(_msgSender(), to, amount);
294299
}
@@ -313,6 +318,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
313318
}
314319

315320
function setMPCAddress(address mpcAddress_) external onlyRole(DEFAULT_ADMIN_ROLE) {
321+
require(mpcAddress_ != address(0), ZeroAddress());
316322
address oldMPCAddress = mpcAddress;
317323
mpcAddress = mpcAddress_;
318324
emit MPCAddressSet(oldMPCAddress, mpcAddress_);
@@ -359,8 +365,8 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
359365
}
360366

361367
function _deposit(address caller, uint256 amount) private {
362-
totalDeposited += amount;
363-
_depositLogic(caller, amount);
368+
_totalDeposited += amount;
369+
_depositLogic(amount);
364370
emit Deposit(caller, amount);
365371
}
366372

@@ -484,7 +490,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
484490
}
485491
}
486492

487-
function _depositLogic(address /*caller*/, uint256 /*amount*/) internal virtual {
493+
function _depositLogic(uint256 /*amount*/) internal virtual {
488494
return;
489495
}
490496

@@ -495,6 +501,13 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
495501
return context;
496502
}
497503

504+
function _processBorrowAmount(
505+
uint256 amount,
506+
bytes calldata /*targetCallData*/
507+
) internal virtual returns (uint256) {
508+
return amount;
509+
}
510+
498511
function _afterBorrowLogic(address /*borrowToken*/, bytes memory /*context*/) internal virtual {
499512
return;
500513
}
@@ -511,7 +524,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
511524
function _withdrawProfitLogic(IERC20 token) internal virtual returns (uint256) {
512525
uint256 totalBalance = token.balanceOf(address(this));
513526
if (token == ASSETS) {
514-
uint256 deposited = totalDeposited;
527+
uint256 deposited = _totalDeposited;
515528
if (totalBalance < deposited) return 0;
516529
return totalBalance - deposited;
517530
}
@@ -529,6 +542,10 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
529542

530543
// View functions
531544

545+
function totalDeposited() external view virtual override returns (uint256) {
546+
return _totalDeposited;
547+
}
548+
532549
function balance(IERC20 token) external view override returns (uint256) {
533550
if (token == NATIVE_TOKEN) token = WRAPPED_NATIVE_TOKEN;
534551
return _balance(token);

contracts/LiquidityPoolAave.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ contract LiquidityPoolAave is LiquidityPool {
4343
error NothingToRepay();
4444
error CollateralNotSupported();
4545
error CannotWithdrawAToken();
46-
error InvalidLength();
4746

4847
event SuppliedToAave(uint256 amount);
4948
event BorrowTokenLTVSet(address token, uint256 oldLTV, uint256 newLTV);
@@ -70,8 +69,9 @@ contract LiquidityPoolAave is LiquidityPool {
7069
AaveDataTypes.ReserveData memory collateralData = AAVE_POOL.getReserveData(address(liquidityToken));
7170
ATOKEN = IERC20(collateralData.aTokenAddress);
7271
IAavePoolDataProvider poolDataProvider = IAavePoolDataProvider(provider.getPoolDataProvider());
73-
(,,,,,bool usageAsCollateralEnabled,,,,) = poolDataProvider.getReserveConfigurationData(liquidityToken);
74-
require(usageAsCollateralEnabled, CollateralNotSupported());
72+
(,,,,,bool usageAsCollateralEnabled,,,bool isActive, bool isFrozen) =
73+
poolDataProvider.getReserveConfigurationData(liquidityToken);
74+
require(usageAsCollateralEnabled && isActive && !isFrozen, CollateralNotSupported());
7575
_setMinHealthFactor(minHealthFactor_);
7676
_setDefaultLTV(defaultLTV_);
7777
}
@@ -148,7 +148,7 @@ contract LiquidityPoolAave is LiquidityPool {
148148
require(currentLtv <= ltv, TokenLtvExceeded());
149149
}
150150

151-
function _depositLogic(address /*caller*/, uint256 amount) internal override {
151+
function _depositLogic(uint256 amount) internal override {
152152
ASSETS.forceApprove(address(AAVE_POOL), amount);
153153
AAVE_POOL.supply(address(ASSETS), amount, address(this), NO_REFERRAL);
154154
emit SuppliedToAave(amount);
@@ -202,7 +202,7 @@ contract LiquidityPoolAave is LiquidityPool {
202202
uint256 totalBalance = token.balanceOf(address(this));
203203
if (token == ASSETS) {
204204
// Calculate accrued interest from deposits.
205-
uint256 interest = ATOKEN.balanceOf(address(this)) - totalDeposited;
205+
uint256 interest = ATOKEN.balanceOf(address(this)) - _totalDeposited;
206206
if (interest > 0) {
207207
_withdrawLogic(address(this), interest);
208208
totalBalance += interest;

contracts/LiquidityPoolAaveLongTerm.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ contract LiquidityPoolAaveLongTerm is LiquidityPoolAave, ILiquidityPoolLongTerm
108108
uint256 totalBalance = token.balanceOf(address(this));
109109
if (token == ASSETS) {
110110
// Calculate accrued interest from deposits.
111-
uint256 interest = ATOKEN.balanceOf(address(this)) - totalDeposited;
111+
uint256 interest = ATOKEN.balanceOf(address(this)) - _totalDeposited;
112112
if (interest > 0) {
113113
_withdrawLogic(address(this), interest);
114114
totalBalance += interest;

contracts/LiquidityPoolStablecoin.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ contract LiquidityPoolStablecoin is LiquidityPool {
3131

3232
function _withdrawProfitLogic(IERC20 token) internal view override returns (uint256) {
3333
uint256 assetBalance = ASSETS.balanceOf(address(this));
34-
uint256 deposited = totalDeposited;
34+
uint256 deposited = _totalDeposited;
3535
require(assetBalance >= deposited, WithdrawProfitDenied());
3636
if (token == ASSETS) return assetBalance - deposited;
3737
return token.balanceOf(address(this));

0 commit comments

Comments
 (0)