Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions contracts/ERC4626Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ILiquidityPoolBase} from "./interfaces/ILiquidityPoolBase.sol";

/// @title A middleware contract that allows Sprinter funds to be rebalanced to/from the underlying ERC4626 vault.
/// Same as liquidity pools it is admin managed, and profits from underlying vault are accounted for.
/// @author Oleksii Matiiasevych <[email protected]>
contract ERC4626Adapter is ILiquidityPoolBase, AccessControl {
using SafeERC20 for IERC20;

bytes32 private constant LIQUIDITY_ADMIN_ROLE = "LIQUIDITY_ADMIN_ROLE";
bytes32 private constant WITHDRAW_PROFIT_ROLE = "WITHDRAW_PROFIT_ROLE";
bytes32 private constant PAUSER_ROLE = "PAUSER_ROLE";

IERC4626 public immutable TARGET_VAULT;
IERC20 public immutable ASSETS;

uint256 public totalDeposited;
bool public paused;

event Deposit(address caller, uint256 amount);
event Withdraw(address caller, address to, uint256 amount);
event Paused(address account);
event Unpaused(address account);
event ProfitWithdrawn(address token, address to, uint256 amount);

error ZeroAddress();
error IncompatibleAssets();
error InsufficientLiquidity();
error EnforcedPause();
error ExpectedPause();
error NoProfit();
error InvalidToken();

constructor(address assets, address targetVault, address admin) {
require(assets != address(0), ZeroAddress());
require(targetVault != address(0), ZeroAddress());
require(admin != address(0), ZeroAddress());
require(assets == IERC4626(targetVault).asset(), IncompatibleAssets());
TARGET_VAULT = IERC4626(targetVault);
ASSETS = IERC20(assets);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}

modifier whenNotPaused() {
require(!paused, EnforcedPause());
_;
}

modifier whenPaused() {
require(paused, ExpectedPause());
_;
}

function deposit(uint256 amount) external override onlyRole(LIQUIDITY_ADMIN_ROLE) whenNotPaused() {
_deposit(_msgSender(), amount);
}

function depositWithPull(uint256 amount) external override whenNotPaused() {
ASSETS.safeTransferFrom(_msgSender(), address(this), amount);
_deposit(_msgSender(), amount);
}

function withdraw(address to, uint256 amount) override
external
onlyRole(LIQUIDITY_ADMIN_ROLE)
whenNotPaused()
{
require(to != address(0), ZeroAddress());
uint256 deposited = totalDeposited;
require(deposited >= amount, InsufficientLiquidity());
totalDeposited = deposited - amount;
TARGET_VAULT.withdraw(amount, to, address(this));
emit Withdraw(_msgSender(), to, amount);
}

function withdrawProfit(
address[] calldata tokens,
address to
) external override onlyRole(WITHDRAW_PROFIT_ROLE) whenNotPaused() {
require(to != address(0), ZeroAddress());
bool success;
for (uint256 i = 0; i < tokens.length; i++) {
IERC20 token = IERC20(tokens[i]);
uint256 amountToWithdraw = _withdrawProfitLogic(token);
if (amountToWithdraw == 0) continue;
success = true;
token.safeTransfer(to, amountToWithdraw);
emit ProfitWithdrawn(address(token), to, amountToWithdraw);
}
require(success, NoProfit());
}

function pause() external override onlyRole(PAUSER_ROLE) whenNotPaused() {
paused = true;
emit Paused(_msgSender());
}

function unpause() external override onlyRole(PAUSER_ROLE) whenPaused() {
paused = false;
emit Unpaused(_msgSender());
}

function _deposit(address caller, uint256 amount) private {
ASSETS.forceApprove(address(TARGET_VAULT), amount);
TARGET_VAULT.deposit(amount, address(this));
totalDeposited += amount;
emit Deposit(caller, amount);
}

function _withdrawProfitLogic(IERC20 token) internal returns (uint256) {
require(token != IERC20(TARGET_VAULT), InvalidToken());
uint256 localBalance = token.balanceOf(address(this));
if (token == ASSETS) {
uint256 deposited = totalDeposited;
uint256 vaultBalance = TARGET_VAULT.maxWithdraw(address(this));
if (vaultBalance < deposited) return localBalance;
uint256 profit = vaultBalance - deposited;
TARGET_VAULT.withdraw(profit, address(this), address(this));
return profit + localBalance;
}
return localBalance;
}
}
37 changes: 25 additions & 12 deletions contracts/LiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
IERC20 immutable public ASSETS;

BitMaps.BitMap private _usedNonces;
uint256 public totalDeposited;
uint256 internal _totalDeposited;

bool public paused;
bool public borrowPaused;
address public mpcAddress;
address public signerAddress;

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

address public signerAddress;

error ZeroAddress();
error InvalidSignature();
error NotEnoughToDeposit();
Expand Down Expand Up @@ -137,14 +136,14 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
// Allow native token transfers.
}

function deposit(uint256 amount) external override onlyRole(LIQUIDITY_ADMIN_ROLE) {
function deposit(uint256 amount) external virtual override onlyRole(LIQUIDITY_ADMIN_ROLE) {
// called after receiving deposit in USDC
uint256 newBalance = ASSETS.balanceOf(address(this));
require(newBalance >= amount, NotEnoughToDeposit());
_deposit(_msgSender(), amount);
}

function depositWithPull(uint256 amount) external override {
function depositWithPull(uint256 amount) external virtual override {
// pulls USDC from the sender
ASSETS.safeTransferFrom(_msgSender(), address(this), amount);
_deposit(_msgSender(), amount);
Expand All @@ -171,6 +170,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
) external override whenNotPaused() whenBorrowNotPaused() {
// - Validate MPC signature
_validateMPCSignatureWithCaller(borrowToken, amount, target, targetCallData, nonce, deadline, signature);
amount = _processBorrowAmount(amount, targetCallData);
(uint256 nativeValue, address actualBorrowToken, bytes memory context) =
_borrow(borrowToken, amount, target, NATIVE_ALLOWED, "");
_afterBorrowLogic(actualBorrowToken, context);
Expand Down Expand Up @@ -237,6 +237,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
bytes calldata signature
) external override whenNotPaused() whenBorrowNotPaused() {
_validateMPCSignatureWithCaller(borrowToken, amount, target, targetCallData, nonce, deadline, signature);
amount = _processBorrowAmount(amount, targetCallData);
// Native borrowing is denied because swap() is not payable.
(,, bytes memory context) = _borrow(borrowToken, amount, _msgSender(), NATIVE_DENIED, "");
_afterBorrowLogic(borrowToken, context);
Expand Down Expand Up @@ -277,18 +278,19 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {

// Admin functions

/// @notice Can withdraw a maximum of totalDeposited. If anything is left, it is meant to be withdrawn through
/// @notice Can withdraw a maximum of _totalDeposited. If anything is left, it is meant to be withdrawn through
/// a withdrawProfit().
function withdraw(address to, uint256 amount)
external
virtual
override
onlyRole(LIQUIDITY_ADMIN_ROLE)
whenNotPaused()
{
require(to != address(0), ZeroAddress());
uint256 deposited = totalDeposited;
uint256 deposited = _totalDeposited;
require(deposited >= amount, InsufficientLiquidity());
totalDeposited = deposited - amount;
_totalDeposited = deposited - amount;
_withdrawLogic(to, amount);
emit Withdraw(_msgSender(), to, amount);
}
Expand Down Expand Up @@ -359,8 +361,8 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
}

function _deposit(address caller, uint256 amount) private {
totalDeposited += amount;
_depositLogic(caller, amount);
_totalDeposited += amount;
_depositLogic(amount);
emit Deposit(caller, amount);
}

Expand Down Expand Up @@ -484,7 +486,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
}
}

function _depositLogic(address /*caller*/, uint256 /*amount*/) internal virtual {
function _depositLogic(uint256 /*amount*/) internal virtual {
return;
}

Expand All @@ -495,6 +497,13 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
return context;
}

function _processBorrowAmount(
uint256 amount,
bytes calldata /*targetCallData*/
) internal virtual returns (uint256) {
return amount;
}

function _afterBorrowLogic(address /*borrowToken*/, bytes memory /*context*/) internal virtual {
return;
}
Expand All @@ -511,7 +520,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
function _withdrawProfitLogic(IERC20 token) internal virtual returns (uint256) {
uint256 totalBalance = token.balanceOf(address(this));
if (token == ASSETS) {
uint256 deposited = totalDeposited;
uint256 deposited = _totalDeposited;
if (totalBalance < deposited) return 0;
return totalBalance - deposited;
}
Expand All @@ -529,6 +538,10 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {

// View functions

function totalDeposited() external view virtual override returns (uint256) {
return _totalDeposited;
}

function balance(IERC20 token) external view override returns (uint256) {
if (token == NATIVE_TOKEN) token = WRAPPED_NATIVE_TOKEN;
return _balance(token);
Expand Down
4 changes: 2 additions & 2 deletions contracts/LiquidityPoolAave.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ contract LiquidityPoolAave is LiquidityPool {
require(currentLtv <= ltv, TokenLtvExceeded());
}

function _depositLogic(address /*caller*/, uint256 amount) internal override {
function _depositLogic(uint256 amount) internal override {
ASSETS.forceApprove(address(AAVE_POOL), amount);
AAVE_POOL.supply(address(ASSETS), amount, address(this), NO_REFERRAL);
emit SuppliedToAave(amount);
Expand Down Expand Up @@ -202,7 +202,7 @@ contract LiquidityPoolAave is LiquidityPool {
uint256 totalBalance = token.balanceOf(address(this));
if (token == ASSETS) {
// Calculate accrued interest from deposits.
uint256 interest = ATOKEN.balanceOf(address(this)) - totalDeposited;
uint256 interest = ATOKEN.balanceOf(address(this)) - _totalDeposited;
if (interest > 0) {
_withdrawLogic(address(this), interest);
totalBalance += interest;
Expand Down
2 changes: 1 addition & 1 deletion contracts/LiquidityPoolAaveLongTerm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ contract LiquidityPoolAaveLongTerm is LiquidityPoolAave, ILiquidityPoolLongTerm
uint256 totalBalance = token.balanceOf(address(this));
if (token == ASSETS) {
// Calculate accrued interest from deposits.
uint256 interest = ATOKEN.balanceOf(address(this)) - totalDeposited;
uint256 interest = ATOKEN.balanceOf(address(this)) - _totalDeposited;
if (interest > 0) {
_withdrawLogic(address(this), interest);
totalBalance += interest;
Expand Down
2 changes: 1 addition & 1 deletion contracts/LiquidityPoolStablecoin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract LiquidityPoolStablecoin is LiquidityPool {

function _withdrawProfitLogic(IERC20 token) internal view override returns (uint256) {
uint256 assetBalance = ASSETS.balanceOf(address(this));
uint256 deposited = totalDeposited;
uint256 deposited = _totalDeposited;
require(assetBalance >= deposited, WithdrawProfitDenied());
if (token == ASSETS) return assetBalance - deposited;
return token.balanceOf(address(this));
Expand Down
Loading