Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2704a67
Fixes after review
viatrix Feb 21, 2025
3a00493
deposit(amount)
viatrix Feb 21, 2025
5f80a56
Use new deposit interface
lastperson Feb 21, 2025
d5f7894
Fix tests
viatrix Feb 21, 2025
214f603
Update deposit and withdraw
viatrix Feb 23, 2025
c92021a
Add pause for borrowing
viatrix Feb 23, 2025
5ec11f8
Add Pausable
viatrix Feb 24, 2025
73193dc
Remove upgradeability
viatrix Feb 24, 2025
d4b9d5b
Fix lint
viatrix Feb 24, 2025
4904505
Update LiquidityPool
viatrix Feb 25, 2025
dee5cb1
Update Rebalancer with multiple local pools support
lastperson Feb 25, 2025
a8bdda4
Add BorrowAndSwap
viatrix Feb 25, 2025
be08e32
Merge branch 'fix-liq-pool' of https://github.com/sprintertech/sprint…
viatrix Feb 25, 2025
3a4cc22
Update signature for borrowAndSwap
viatrix Feb 25, 2025
c9c34fe
Fix lint
viatrix Feb 25, 2025
a0e4bd9
Add LiquidityPoolBase and refactor
lastperson Feb 26, 2025
fb869d1
Fix deploy
lastperson Feb 26, 2025
fe0974d
Deploy LiquidityPoolBase in networks without Aave
lastperson Feb 26, 2025
84301e7
Add pauser role to deploy
lastperson Feb 26, 2025
b57cca0
Fix AAVE pool deploy params order
lastperson Feb 26, 2025
5de2bb0
Add tests for liq pools
viatrix Feb 26, 2025
eb19aaf
Fix test
viatrix Feb 27, 2025
bc6bc13
Increase multiplier precision in mining
lastperson Feb 27, 2025
adde2ee
Merge branch 'main' into fix-liq-pool
lastperson Feb 27, 2025
92ef954
Rename pools
lastperson Feb 27, 2025
96b0555
Remove fill params from borrow and swap sig
lastperson Feb 27, 2025
013fbfa
Always include caller in the MPC signature
lastperson Feb 27, 2025
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
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ ADMIN=
ADJUSTER=
# Deposits to Liquidity Hub are only allowed till this limit is reached.
ASSETS_LIMIT=
# Liquidity mining tiers. Multiplier will be divided by 100. So 175 will result in 1.75x.
# Liquidity mining tiers. Multiplier will be divided by 1000,000,000. So 1750000000 will result in 1.75x.
# There is no limit to the number of tiers, but has to be atleast one.
TIER_1_SECONDS=7776000
TIER_1_MULTIPLIER=30
TIER_1_MULTIPLIER=300000000
TIER_2_SECONDS=15552000
TIER_2_MULTIPLIER=80
TIER_2_MULTIPLIER=800000000
TIER_3_SECONDS=31104000
TIER_3_MULTIPLIER=170
TIER_3_MULTIPLIER=1666666667
# Rebalance caller.
REBALANCE_CALLER=
# Liquidity Pool parameters.
Expand All @@ -21,6 +21,7 @@ MIN_HEALTH_FACTOR=500
DEFAULT_LTV=20
MPC_ADDRESS=
WITHDRAW_PROFIT=
PAUSER=
# General deployment parameters.
DEPLOYER_PRIVATE_KEY=
VERIFY=false
Expand Down
2 changes: 1 addition & 1 deletion contracts/LiquidityHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgrade
SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(LIQUIDITY_POOL), assets);
_mint(receiver, shares);
$.totalAssets += assets;
LIQUIDITY_POOL.deposit();
LIQUIDITY_POOL.deposit(assets);
emit Deposit(caller, receiver, assets, shares);
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/LiquidityMining.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract LiquidityMining is ERC20, Ownable {
using SafeERC20 for IERC20;

uint32 public constant MULTIPLIER_PRECISION = 100;
uint32 public constant MULTIPLIER_PRECISION = 1000000000;
IERC20 public immutable STAKING_TOKEN;

struct Tier {
Expand Down
353 changes: 108 additions & 245 deletions contracts/LiquidityPool.sol

Large diffs are not rendered by default.

327 changes: 327 additions & 0 deletions contracts/LiquidityPoolBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ILiquidityPool} from "./interfaces/ILiquidityPool.sol";
import {IBorrower} from "./interfaces/IBorrower.sol";

contract LiquidityPoolBase is ILiquidityPool, AccessControl, EIP712, Pausable {
using SafeERC20 for IERC20;
using ECDSA for bytes32;
using BitMaps for BitMaps.BitMap;

bytes32 private constant BORROW_TYPEHASH = keccak256(
"Borrow("
"address borrowToken,"
"uint256 amount,"
"address target,"
"bytes targetCallData,"
"uint256 nonce,"
"uint256 deadline"
")"
);
bytes32 private constant BORROW_AND_SWAP_TYPEHASH = keccak256(
"BorrowAndSwap("
"address caller,"
"address borrowToken,"
"uint256 borrowAmount,"
"address fillToken,"
"uint256 fillAmount,"
"address target,"
"bytes targetCallData,"
"uint256 nonce,"
"uint256 deadline"
")"
);

IERC20 immutable public ASSETS;

bool public borrowPaused;
address public mpcAddress;
uint256 public totalDeposited;

BitMaps.BitMap private _usedNonces;

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

error ZeroAddress();
error InvalidSignature();
error NotEnoughToDeposit();
error TargetCallFailed();
error ExpiredSignature();
error NonceAlreadyUsed();
error BorrowingIsPaused();
error BorrowingIsNotPaused();
error InsufficientLiquidity();
error InvalidBorrowToken();
error NotImplemented();
error NoProfit();

event Deposit(address from, uint256 amount);
event Withdraw(address caller, address to, uint256 amount);
event ProfitWithdrawn(address token, address to, uint256 amount);
event BorrowPaused();
event BorrowUnpaused();
event MPCAddressSet(address oldMPCAddress, address newMPCAddress);

constructor(
address liquidityToken,
address admin,
address mpcAddress_
) EIP712("LiquidityPool", "1.0.0") {
require(liquidityToken != address(0), ZeroAddress());
ASSETS = IERC20(liquidityToken);
require(admin != address(0), ZeroAddress());
_grantRole(DEFAULT_ADMIN_ROLE, admin);
require(mpcAddress_ != address(0), ZeroAddress());
mpcAddress = mpcAddress_;
}

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

function depositWithPull(uint256 amount) external override {
// pulls USDC from the sender
ASSETS.safeTransferFrom(msg.sender, address(this), amount);
_deposit(msg.sender, amount);
}

function borrow(
address borrowToken,
uint256 amount,
address target,
bytes calldata targetCallData,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external override whenNotPaused() {
// - Validate MPC signature
_validateMPCSignature(borrowToken, amount, target, targetCallData, nonce, deadline, signature);
_borrow(borrowToken, amount, target);
// - Invoke the recipient's address with calldata provided in the MPC signature to complete
// the operation securely.
(bool success,) = target.call(targetCallData);
require(success, TargetCallFailed());
}

function borrowAndSwap(
address borrowToken,
uint256 borrowAmount,
SwapParams calldata swapInputData,
address target,
bytes calldata targetCallData,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) external override whenNotPaused() {
_validateMPCSignatureWithSwap(
borrowToken,
borrowAmount,
swapInputData,
target,
targetCallData,
nonce,
deadline,
signature
);
_borrow(borrowToken, borrowAmount, msg.sender);
// Call the swap function on caller
IBorrower(msg.sender).swap(swapInputData.swapData);
IERC20(swapInputData.fillToken).safeTransferFrom(msg.sender, address(this), swapInputData.fillAmount);
IERC20(swapInputData.fillToken).forceApprove(target, swapInputData.fillAmount);
// - Invoke the recipient's address with calldata provided in the MPC signature to complete
// the operation securely.
(bool success,) = target.call(targetCallData);
require(success, TargetCallFailed());
}

function repay(address[] calldata) external virtual override {
revert NotImplemented();
}

// Admin functions

function withdraw(address to, uint256 amount)
external
override
onlyRole(LIQUIDITY_ADMIN_ROLE)
whenNotPaused()
returns (uint256)
{
uint256 withdrawn = _withdrawLogic(to, amount);
uint256 deposited = totalDeposited;
require(deposited >= withdrawn, InsufficientLiquidity());
totalDeposited = deposited - withdrawn;
emit Withdraw(msg.sender, to, withdrawn);
return withdrawn;
}

function withdrawProfit(
address[] calldata tokens,
address to
) external override onlyRole(WITHDRAW_PROFIT_ROLE) whenNotPaused() {
_withdrawProfit(tokens, to);
}

function setMPCAddress(address mpcAddress_) external onlyRole(DEFAULT_ADMIN_ROLE) {
address oldMPCAddress = mpcAddress;
mpcAddress = mpcAddress_;
emit MPCAddressSet(oldMPCAddress, mpcAddress_);
}

function pauseBorrow() external override onlyRole(WITHDRAW_PROFIT_ROLE) {
borrowPaused = true;
emit BorrowPaused();
}

function unpauseBorrow() external override onlyRole(WITHDRAW_PROFIT_ROLE) {
borrowPaused = false;
emit BorrowUnpaused();
}

function pause() external override onlyRole(PAUSER_ROLE) whenNotPaused() {
_pause();
}

function unpause() external override onlyRole(PAUSER_ROLE) whenPaused() {
_unpause();
}

function paused() public view override(Pausable, ILiquidityPool) returns (bool) {
return super.paused();
}

// Internal functions

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

function _validateMPCSignature(
address borrowToken,
uint256 amount,
address target,
bytes calldata targetCallData,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) private {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
BORROW_TYPEHASH,
borrowToken,
amount,
target,
keccak256(targetCallData),
nonce,
deadline
)));
_validateSig(digest, nonce, deadline, signature);
}

function _validateMPCSignatureWithSwap(
address borrowToken,
uint256 amount,
SwapParams calldata swapInputData,
address target,
bytes calldata targetCallData,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) internal {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
BORROW_AND_SWAP_TYPEHASH,
msg.sender,
borrowToken,
amount,
swapInputData.fillToken,
swapInputData.fillAmount,
target,
keccak256(targetCallData),
nonce,
deadline
)));
_validateSig(digest, nonce, deadline, signature);
}

function _validateSig(bytes32 digest, uint256 nonce, uint256 deadline, bytes calldata signature) internal {
address signer = digest.recover(signature);
require(signer == mpcAddress, InvalidSignature());
require(_usedNonces.get(nonce) == false, NonceAlreadyUsed());
_usedNonces.set(nonce);
require(notPassed(deadline), ExpiredSignature());
}

function _borrow(address borrowToken, uint256 amount, address target) internal {
require(!borrowPaused, BorrowingIsPaused());
_borrowLogic(borrowToken, amount, target);
IERC20(borrowToken).forceApprove(target, amount);
}

function _withdrawProfit(
address[] calldata tokens,
address to
) internal {
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;
// Withdraw from this contract
token.safeTransfer(to, amountToWithdraw);
emit ProfitWithdrawn(address(token), to, amountToWithdraw);
}
require(success, NoProfit());
}

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

function _borrowLogic(address borrowToken, uint256 /*amount*/, address /*target*/) internal virtual {
require(borrowToken == address(ASSETS), InvalidBorrowToken());
}

function _withdrawLogic(address to, uint256 amount) internal virtual returns (uint256) {
ASSETS.safeTransfer(to, amount);
return amount;
}

function _withdrawProfitLogic(IERC20 token) internal virtual returns (uint256) {
uint256 totalBalance = token.balanceOf(address(this));
if (token == ASSETS) {
uint256 deposited = totalDeposited;
if (totalBalance < deposited) return 0;
return totalBalance - deposited;
}
return totalBalance;
}

// View functions

function timeNow() internal view returns (uint32) {
return uint32(block.timestamp);
}

function passed(uint256 timestamp) internal view returns (bool) {
return timeNow() > timestamp;
}

function notPassed(uint256 timestamp) internal view returns (bool) {
return !passed(timestamp);
}
}
Loading