Skip to content

Commit

Permalink
feat: introduce operational treasury and admin role for pools (#72)
Browse files Browse the repository at this point in the history
* feat: introduce the external treasury for liquidity pools
* test: cover the new functions and events by tests
* feat: reintroduce the admin role for liquidity pools
Now the following functions can only be called by an account
with the admin role:
* depositFromExternalTreasury;
* withdrawToExternalTreasury.
* test: cover the reintroduction of the admin role by tests
* docs: improve comments for new entities
* feat: update contracts version 1.12.0 => 1.13.0
* test: centralize expected contracts version to check
* feat: rename externalTreasury => operationalTreasury
  • Loading branch information
EvgeniiZaitsevCW authored Mar 7, 2025
1 parent a75a81f commit cabb92e
Show file tree
Hide file tree
Showing 9 changed files with 432 additions and 129 deletions.
129 changes: 100 additions & 29 deletions contracts/LiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ contract LiquidityPool is

/// @dev The role of this contract owner.
bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");
/// @dev The role of this contract admin.
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

// -------------------------------------------- //
// Modifiers //
Expand Down Expand Up @@ -137,6 +139,7 @@ contract LiquidityPool is
}

_setRoleAdmin(OWNER_ROLE, OWNER_ROLE);
_setRoleAdmin(ADMIN_ROLE, OWNER_ROLE);
_grantRole(OWNER_ROLE, owner_);

_market = market_;
Expand All @@ -152,46 +155,41 @@ contract LiquidityPool is
_setAddonTreasury(newTreasury);
}

/// @inheritdoc ILiquidityPoolConfiguration
function setOperationalTreasury(address newTreasury) external onlyRole(OWNER_ROLE) {
_setOperationalTreasury(newTreasury);
}

/// @dev Initializes the admin role for already deployed contracts.
///
/// This function can be removed after the admin role is initialized in all deployed contracts.
function initAdminRole() external onlyRole(OWNER_ROLE) {
_setRoleAdmin(ADMIN_ROLE, OWNER_ROLE);
_grantRole(ADMIN_ROLE, msg.sender);
}

// -------------------------------------------- //
// Primary transactional functions //
// -------------------------------------------- //

/// @inheritdoc ILiquidityPoolPrimary
function deposit(uint256 amount) external onlyRole(OWNER_ROLE) {
if (amount == 0) {
revert Error.InvalidAmount();
}

IERC20 underlyingToken = IERC20(_token);

if (underlyingToken.allowance(address(this), _market) == 0) {
underlyingToken.approve(_market, type(uint256).max);
}

_borrowableBalance += amount.toUint64();
underlyingToken.safeTransferFrom(msg.sender, address(this), amount);
_deposit(amount, msg.sender);
}

emit Deposit(amount);
/// @inheritdoc ILiquidityPoolPrimary
function depositFromOperationalTreasury(uint256 amount) external onlyRole(ADMIN_ROLE) {
_deposit(amount, _getAndCheckOperationalTreasury());
}

/// @inheritdoc ILiquidityPoolPrimary
function withdraw(uint256 borrowableAmount, uint256 addonAmount) external onlyRole(OWNER_ROLE) {
if (borrowableAmount == 0) {
revert Error.InvalidAmount();
}
if (addonAmount != 0) {
revert Error.InvalidAmount();
}

if (_borrowableBalance < borrowableAmount) {
revert InsufficientBalance();
}

_borrowableBalance -= borrowableAmount.toUint64();

IERC20(_token).safeTransfer(msg.sender, borrowableAmount + addonAmount);
_withdraw(borrowableAmount, addonAmount, msg.sender);
}

emit Withdrawal(borrowableAmount, addonAmount);
/// @inheritdoc ILiquidityPoolPrimary
function withdrawToOperationalTreasury(uint256 amount) external onlyRole(ADMIN_ROLE) {
_withdraw(amount, 0, _getAndCheckOperationalTreasury());
}

/// @inheritdoc ILiquidityPoolPrimary
Expand Down Expand Up @@ -255,6 +253,11 @@ contract LiquidityPool is
return _addonTreasury;
}

/// @inheritdoc ILiquidityPoolPrimary
function operationalTreasury() external view returns (address) {
return _operationalTreasury;
}

/// @inheritdoc ILiquidityPoolPrimary
function getBalances() external view returns (uint256, uint256) {
return (_borrowableBalance, _addonsBalance);
Expand All @@ -281,7 +284,50 @@ contract LiquidityPool is
// Internal functions //
// -------------------------------------------- //

/// @dev Sets the new address of the addon treasury internally.
/// @dev Deposits the tokens into the liquidity pool internally.
/// @param amount The amount of tokens to deposit.
/// @param sender The address of the tokens sender.
function _deposit(uint256 amount, address sender) internal {
if (amount == 0) {
revert Error.InvalidAmount();
}

IERC20 underlyingToken = IERC20(_token);

if (underlyingToken.allowance(address(this), _market) == 0) {
underlyingToken.approve(_market, type(uint256).max);
}

_borrowableBalance += amount.toUint64();
underlyingToken.safeTransferFrom(sender, address(this), amount);

emit Deposit(amount);
}

/// @dev Withdraws the tokens from the liquidity pool internally.
/// @param borrowableAmount The amount of borrowable tokens to withdraw.
/// @param addonAmount The amount of addon tokens to withdraw.
/// @param recipient The address of the tokens recipient.
function _withdraw(uint256 borrowableAmount, uint256 addonAmount, address recipient) internal {
if (borrowableAmount == 0) {
revert Error.InvalidAmount();
}
if (addonAmount != 0) {
revert Error.InvalidAmount();
}

if (_borrowableBalance < borrowableAmount) {
revert InsufficientBalance();
}

_borrowableBalance -= borrowableAmount.toUint64();

IERC20(_token).safeTransfer(recipient, borrowableAmount);

emit Withdrawal(borrowableAmount, addonAmount);
}

/// @dev Sets the new address of the addon treasury internally.
function _setAddonTreasury(address newTreasury) internal {
address oldTreasury = _addonTreasury;
if (oldTreasury == newTreasury) {
Expand All @@ -297,6 +343,31 @@ contract LiquidityPool is
_addonTreasury = newTreasury;
}

/// @dev Sets the new address of the operational treasury internally.
/// @param newTreasury The new address of the operational treasury.
function _setOperationalTreasury(address newTreasury) internal {
address oldTreasury = _operationalTreasury;
if (oldTreasury == newTreasury) {
revert Error.AlreadyConfigured();
}
if (newTreasury != address(0)) {
if (IERC20(_token).allowance(newTreasury, address(this)) == 0) {
revert OperationalTreasuryZeroAllowanceForPool();
}
}
emit OperationalTreasuryChanged(newTreasury, oldTreasury);
_operationalTreasury = newTreasury;
}

/// @dev Returns the operational treasury address and validates it.
function _getAndCheckOperationalTreasury() internal view returns (address) {
address operationalTreasury_ = _operationalTreasury;
if (operationalTreasury_ == address(0)) {
revert OperationalTreasuryAddressZero();
}
return operationalTreasury_;
}

/// @dev The upgrade validation function for the UUPSExtUpgradeable contract.
/// @param newImplementation The address of the new implementation.
///
Expand Down
7 changes: 6 additions & 1 deletion contracts/LiquidityPoolStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ abstract contract LiquidityPoolStorage {
/// Now the addon amount must always be output to an external wallet. The addon balance of the pool is always zero.
address internal _addonTreasury;

/// @dev The address of the operational treasury.
///
/// The operational treasury is used to deposit and withdraw tokens through special functions.
address internal _operationalTreasury;

/// @dev This empty reserved space is put in place to allow future versions
/// to add new variables without shifting down storage in the inheritance chain.
uint256[46] private __gap;
uint256[45] private __gap;
}
2 changes: 1 addition & 1 deletion contracts/base/Versionable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import "../interfaces/IVersionable.sol";
abstract contract Versionable is IVersionable {
/// @inheritdoc IVersionable
function $__VERSION() external pure returns (Version memory) {
return Version(1, 12, 0);
return Version(1, 13, 0);
}
}
42 changes: 39 additions & 3 deletions contracts/interfaces/ILiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,24 @@ interface ILiquidityPoolPrimary {
// Transactional functions //
// -------------------------------------------- //

/// @dev Deposits tokens to the liquidity pool.
/// @dev Deposits tokens to the liquidity pool from the caller account.
/// @param amount The amount of tokens to deposit.
function deposit(uint256 amount) external;

/// @dev Withdraws tokens from the liquidity pool.
/// @dev Deposits tokens to the liquidity pool from the operational treasury.
/// @param amount The amount of tokens to deposit.
function depositFromOperationalTreasury(uint256 amount) external;

/// @dev Withdraws tokens from the liquidity pool to the caller account.
/// @param borrowableAmount The amount of tokens to withdraw from the borrowable balance.
/// @param addonAmount This parameter has been deprecated since version 1.8.0 and must be zero.
/// See the {addonTreasury} function comments for more details.
function withdraw(uint256 borrowableAmount, uint256 addonAmount) external;

/// @dev Withdraws tokens from the liquidity pool to the operational treasury.
/// @param amount The amount of tokens to withdraw from the borrowable balance.
function withdrawToOperationalTreasury(uint256 amount) external;

/// @dev Rescues tokens from the liquidity pool.
/// @param token The address of the token to rescue.
/// @param amount The amount of tokens to rescue.
Expand All @@ -62,6 +70,13 @@ interface ILiquidityPoolPrimary {
/// @return The current address of the addon treasury.
function addonTreasury() external view returns (address);

/// @dev Returns the operational treasury address.
///
/// The operational treasury is used to deposit and withdraw tokens through special functions.
///
/// @return The current address of the operational treasury.
function operationalTreasury() external view returns (address);

/// @dev Gets the borrowable and addons balances of the liquidity pool.
///
/// The addons part of the balance has been deprecated since version 1.8.0 and now it always equals zero.
Expand All @@ -81,14 +96,22 @@ interface ILiquidityPoolConfiguration {
// Events //
// -------------------------------------------- //

/// @dev Emitted when the the addon treasury address has been changed.
/// @dev Emitted when the addon treasury address has been changed.
///
/// See the {addonTreasury} function comments for more details.
///
/// @param newTreasury The updated address of the addon treasury.
/// @param oldTreasury The previous address of the addon treasury.
event AddonTreasuryChanged(address newTreasury, address oldTreasury);

/// @dev Emitted when the operational treasury address has been changed.
///
/// See the {operationalTreasury} function comments for more details.
///
/// @param newTreasury The updated address of the operational treasury.
/// @param oldTreasury The previous address of the operational treasury.
event OperationalTreasuryChanged(address newTreasury, address oldTreasury);

// -------------------------------------------- //
// Transactional functions //
// -------------------------------------------- //
Expand All @@ -99,6 +122,13 @@ interface ILiquidityPoolConfiguration {
///
/// @param newTreasury The new address of the addon treasury to set.
function setAddonTreasury(address newTreasury) external;

/// @dev Sets the operational treasury address.
///
/// See the {operationalTreasury} function comments for more details.
///
/// @param newTreasury The new address of the operational treasury to set.
function setOperationalTreasury(address newTreasury) external;
}

/// @title ILiquidityPoolHooks interface
Expand Down Expand Up @@ -140,6 +170,12 @@ interface ILiquidityPoolErrors {

/// @dev Thrown when the token source balance is insufficient.
error InsufficientBalance();

/// @dev Thrown when the operational treasury address is zero.
error OperationalTreasuryAddressZero();

/// @dev Thrown when the operational treasury has not provided an allowance for the pool to transfer its tokens.
error OperationalTreasuryZeroAllowanceForPool();
}

/// @title ILiquidityPool interface
Expand Down
7 changes: 7 additions & 0 deletions contracts/testables/LiquidityPoolTestable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ contract LiquidityPoolTestable is LiquidityPool {
) public {
__LiquidityPool_init_unchained(lender_, market_, token_);
}

/// @dev Sets the admin role for a given role for testing purposes.
/// @param role The role to set the admin for.
/// @param adminRole The admin role to set.
function setRoleAdmin(bytes32 role, bytes32 adminRole) external {
_setRoleAdmin(role, adminRole);
}
}
13 changes: 13 additions & 0 deletions test-utils/specific.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface Version {
major: number;
minor: number;
patch: number;

[key: string]: number; // Indexing signature to ensure that fields are iterated over in a key-value style
}

export const EXPECTED_VERSION: Version = {
major: 1,
minor: 13,
patch: 0
};
15 changes: 1 addition & 14 deletions test/CreditLine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
proveTx
} from "../test-utils/eth";
import { checkEquality, maxUintForBits, setUpFixture } from "../test-utils/common";
import { EXPECTED_VERSION } from "../test-utils/specific";

enum BorrowingPolicy {
SingleActiveLoan = 0,
Expand Down Expand Up @@ -112,14 +113,6 @@ interface LoanState {
discountAmount: bigint;
}

interface Version {
major: number;
minor: number;
patch: number;

[key: string]: number; // Indexing signature to ensure that fields are iterated over in a key-value style
}

interface Fixture {
creditLine: Contract;
creditLineUnderAdmin: Contract;
Expand Down Expand Up @@ -257,12 +250,6 @@ const FUNC_DETERMINE_LATE_FEE_AMOUNT_NEW =
const FUNC_DETERMINE_LATE_FEE_AMOUNT_LEGACY =
"determineLateFeeAmount(uint256)";

const EXPECTED_VERSION: Version = {
major: 1,
minor: 12,
patch: 0
};

function processLoanClosing(borrowerState: BorrowerState, borrowedAmount: bigint) {
borrowerState.activeLoanCount -= 1n;
borrowerState.closedLoanCount += 1n;
Expand Down
15 changes: 1 addition & 14 deletions test/LendingMarket.base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
proveTx
} from "../test-utils/eth";
import { checkEquality, maxUintForBits, roundMath, setUpFixture } from "../test-utils/common";
import { EXPECTED_VERSION } from "../test-utils/specific";

enum LoanType {
Ordinary = 0,
Expand Down Expand Up @@ -117,14 +118,6 @@ interface Fixture {
installmentLoanParts: Loan[];
}

interface Version {
major: number;
minor: number;
patch: number;

[key: string]: number; // Indexing signature to ensure that fields are iterated over in a key-value style
}

enum PayerKind {
Borrower = 0,
Stranger = 1
Expand Down Expand Up @@ -219,12 +212,6 @@ const BORROWED_AMOUNTS: number[] = [BORROWED_AMOUNT * 3 - 1, BORROWED_AMOUNT * 2
const ADDON_AMOUNTS: number[] = [ADDON_AMOUNT * 3 - 1, ADDON_AMOUNT * 2 + 1, ADDON_AMOUNT];
const DURATIONS_IN_PERIODS: number[] = [0, DURATION_IN_PERIODS / 2, DURATION_IN_PERIODS];

const EXPECTED_VERSION: Version = {
major: 1,
minor: 12,
patch: 0
};

const defaultLoanState: LoanState = {
programId: 0,
borrowedAmount: 0,
Expand Down
Loading

0 comments on commit cabb92e

Please sign in to comment.