Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/WrappedNLP.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC20Metadata as IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {NativeLPToken} from "./NativeLPToken.sol";
import {WithdrawQueue} from "./WithdrawQueue.sol";
import {SafeERC20} from "./libraries/SafeERC20.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {ReentrancyGuardTransient} from "./libraries/ReentrancyGuardTransient.sol";
/**
* @title WrappedNLP - Wrapped NativeLPToken
* @dev Converts rebasing NativeLPToken into static-balance ERC20 for protocol compatibility
* while preserving accumulated yield value.
*
* @notice Redemption Options:
* - Queue-based: Free redemption via WithdrawQueue contract (time-delayed for JIT protection)
* - Instant: Direct redemption with configurable fee (immediate, open to all users)
*/
contract WrappedNLP is ERC20, Ownable2Step, ReentrancyGuardTransient {
using SafeERC20 for IERC20;
/*//////////////////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/
/// @notice Whether instant redeem is enabled
bool public instantRedeemEnabled = true;
/// @notice The Native LPToken address
NativeLPToken public immutable nlp;
/// @notice The underlying token of the NativeLPToken
IERC20 public immutable underlying;
/// @notice Address that receives instant redeem fees
address public feeRecipient;
/// @notice The authorized WithdrawQueue contract
WithdrawQueue public withdrawQueue;
/// @notice Minimum amount for instant redeem
uint256 public constant MIN_INSTANT_REDEEM_AMOUNT = 1000;
/// @notice Instant redeem fee in basis points (1 bip = 0.01%)
uint256 public instantRedeemFeeBips;
/// @notice Mapping of whitelisted addresses that can directly call unwrapAndRedeem
mapping(address => bool) public redeemWhitelist;
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Event emitted when NativeLP tokens are wrapped into WrappedNLP tokens
event Wrapped(address indexed user, uint256 nlpAmount, uint256 wNLPAmount);
/// @notice Event emitted when underlying tokens are deposited and wrapped into WrappedNLP tokens
event DepositAndWrapped(address indexed from, address indexed to, uint256 underlyingAmount, uint256 wNLPAmount);
/// @notice Event emitted when WrappedNLP tokens are unwrapped and redeemed for underlying tokens
event UnwrappedAndRedeemed(address indexed from, address indexed to, uint256 wNLPAmount, uint256 underlyingAmount);
/// @notice Event emitted when instant redeem fee is charged
event InstantRedeemFeeCollected(address indexed user, uint256 feeAmount);
/// @notice Event emitted when fee recipient is updated
event FeeRecipientUpdated(address indexed oldRecipient, address indexed newRecipient);
/// @notice Event emitted when instant redeem is executed
event InstantRedeemed(address indexed user, uint256 wNLPAmount, uint256 underlyingAmount, uint256 feeAmount);
/// @notice Event emitted when withdraw queue address is updated
event WithdrawQueueUpdated(address indexed oldQueue, address indexed newQueue);
/// @notice Event emitted when redeem whitelist status is updated
event RedeemWhitelistUpdated(address indexed account, bool status);
/// @notice Event emitted when instant redeem fee bips is updated
event InstantRedeemFeeBipsUpdated(uint256 oldFeeBips, uint256 newFeeBips);
/// @notice Event emitted when instant redeem enabled status is updated
event InstantRedeemEnabledUpdated(bool enabled);
constructor(string memory _name, string memory _symbol, NativeLPToken _nlp) ERC20(_name, _symbol) {
nlp = _nlp;
underlying = nlp.underlying();
feeRecipient = msg.sender;
instantRedeemFeeBips = nlp.earlyWithdrawFeeBips();
}
/*//////////////////////////////////////////////////////////////////////////
WRAP AND REDEEM
//////////////////////////////////////////////////////////////////////////*/
/// @notice Wraps NativeLP tokens into WrappedNLP tokens
/// @param amount Amount of NativeLP tokens to deposit
/// @return wNLPAmount Amount of WrappedNLP tokens received
function wrap(uint256 amount) external nonReentrant returns (uint256 wNLPAmount) {
require(amount > 0, ErrorsLib.ZeroAmount());
wNLPAmount = nlp.getSharesByUnderlying(amount);
IERC20(nlp).safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, wNLPAmount);
emit Wrapped(msg.sender, amount, wNLPAmount);
}
/// @notice Deposit underlying tokens directly into NLP and receive WrappedNLP tokens for a specified recipient
/// @param to The address to receive the WrappedNLP tokens
/// @param amount Amount of underlying tokens to deposit
/// @return wNLPAmount Amount of WrappedNLP tokens received
function depositAndWrap(address to, uint256 amount) external nonReentrant returns (uint256 wNLPAmount) {
require(amount > 0, ErrorsLib.ZeroAmount());
require(to != address(0), ErrorsLib.ZeroAddress());
underlying.safeTransferFrom(msg.sender, address(this), amount);
underlying.safeApprove(address(nlp), amount);
wNLPAmount = nlp.deposit(amount);
_mint(to, wNLPAmount);
emit DepositAndWrapped(msg.sender, to, amount, wNLPAmount);
}
/// @notice Unwrap WrappedNLP tokens and redeem underlying tokens to a specified address
/// @dev Only WithdrawQueue contract or whitelisted addresses can call this function
/// @param wNLPAmount Amount of WrappedNLP tokens to unwrap and redeem
/// @param to Address that will receive the underlying tokens
/// @return underlyingAmount Amount of underlying tokens sent to the recipient
function unwrapAndRedeem(uint256 wNLPAmount, address to) external nonReentrant returns (uint256 underlyingAmount) {
require(wNLPAmount > 0, ErrorsLib.ZeroAmount());
require(to != address(0), ErrorsLib.ZeroAddress());
require(msg.sender == address(withdrawQueue) || redeemWhitelist[msg.sender], ErrorsLib.NotTrustedOperator());
_burn(msg.sender, wNLPAmount);
underlyingAmount = nlp.redeemTo(wNLPAmount, to);
emit UnwrappedAndRedeemed(msg.sender, to, wNLPAmount, underlyingAmount);
}
/// @notice Instantly redeem WrappedNLP tokens for underlying tokens with fee
/// @param wNLPAmount Amount of WrappedNLP tokens to redeem
/// @param to Address that will receive the underlying tokens
/// @return underlyingAmount Amount of underlying tokens received after fee
function instantRedeem(uint256 wNLPAmount, address to) external nonReentrant returns (uint256 underlyingAmount) {
require(instantRedeemEnabled, "Instant redeem disabled");
require(wNLPAmount >= MIN_INSTANT_REDEEM_AMOUNT, "Below minimum instant redeem amount");
require(to != address(0), ErrorsLib.ZeroAddress());
_burn(msg.sender, wNLPAmount);
underlyingAmount = nlp.redeemTo(wNLPAmount, address(this));
uint256 fee = (underlyingAmount * instantRedeemFeeBips) / 10_000;
if (fee > 0) {
underlyingAmount -= fee;
// Transfer fee to fee recipient
underlying.safeTransfer(feeRecipient, fee);
emit InstantRedeemFeeCollected(msg.sender, fee);
}
underlying.safeTransfer(to, underlyingAmount);
emit InstantRedeemed(msg.sender, wNLPAmount, underlyingAmount, fee);
}
/*//////////////////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Gets the number of decimals for this token
/// @return The number of decimals, matching the underlying token
function decimals() public view override returns (uint8) {
return nlp.decimals();
}
/// @notice Calculates the amount of WrappedNLP tokens for a given amount of NativeLP tokens
/// @param _nlpAmount Amount of NativeLP tokens
/// @return Amount of WrappedNLP tokens
function getWnlpByNlp(uint256 _nlpAmount) external view returns (uint256) {
return nlp.getSharesByUnderlying(_nlpAmount);
}
/// @notice Calculates the amount of NLP tokens for a given amount of WrappedNLP tokens
/// @param wNLPAmount Amount of WrappedNLP tokens
/// @return Amount of NativeLP tokens
function getNlpByWnlp(uint256 wNLPAmount) external view returns (uint256) {
return nlp.getUnderlyingByShares(wNLPAmount);
}
/// @notice Returns the amount of NativeLP tokens per 1 WrappedNLP token
/// @dev Used to calculate the exchange rate from WrappedNLP to NativeLP
/// @return Amount of NativeLP tokens equivalent to 1 WrappedNLP token
function nlpPerToken() external view returns (uint256) {
return nlp.getUnderlyingByShares(10 ** decimals());
}
/// @notice Returns the amount of WrappedNLP tokens per 1 NativeLP token
/// @dev Used to calculate the exchange rate from NativeLP to WrappedNLP
/// @return Amount of WrappedNLP tokens equivalent to 1 NativeLP token
function tokensPerNlp() external view returns (uint256) {
return nlp.getSharesByUnderlying(10 ** decimals());
}
/*//////////////////////////////////////////////////////////////////////////
ADMIN FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Sets the WithdrawQueue contract address
/// @param _withdrawQueue The new WithdrawQueue contract address
function setWithdrawQueue(address _withdrawQueue) external onlyOwner {
require(_withdrawQueue != address(0), ErrorsLib.ZeroAddress());
address oldQueue = address(withdrawQueue);
withdrawQueue = WithdrawQueue(_withdrawQueue);
emit WithdrawQueueUpdated(oldQueue, _withdrawQueue);
}
/// @notice Sets the redeem whitelist status for multiple addresses
/// @param accounts The addresses to update
/// @param statuses The new whitelist statuses corresponding to each account
function setRedeemWhitelist(address[] calldata accounts, bool[] calldata statuses) external onlyOwner {
for (uint256 i = 0; i < accounts.length; i++) {
require(accounts[i] != address(0), ErrorsLib.ZeroAddress());
redeemWhitelist[accounts[i]] = statuses[i];
emit RedeemWhitelistUpdated(accounts[i], statuses[i]);
}
}
/// @notice Sets the address that receives instant redeem fees
/// @param _feeRecipient The new fee recipient address
function setFeeRecipient(address _feeRecipient) external onlyOwner {
require(_feeRecipient != address(0), ErrorsLib.ZeroAddress());
address oldRecipient = feeRecipient;
feeRecipient = _feeRecipient;
emit FeeRecipientUpdated(oldRecipient, _feeRecipient);
}
/// @notice Sets the instant redeem fee in basis points
/// @param _instantRedeemFeeBips The new instant redeem fee in basis points
function setInstantRedeemFeeBips(uint256 _instantRedeemFeeBips) external onlyOwner {
require(_instantRedeemFeeBips <= 2500, ErrorsLib.InvalidFeeBips());
uint256 oldFeeBips = instantRedeemFeeBips;
instantRedeemFeeBips = _instantRedeemFeeBips;
emit InstantRedeemFeeBipsUpdated(oldFeeBips, _instantRedeemFeeBips);
}
/// @notice Sets whether instant redeem is enabled
/// @param _enabled Whether instant redeem should be enabled
function setInstantRedeemEnabled(bool _enabled) external onlyOwner {
instantRedeemEnabled = _enabled;
emit InstantRedeemEnabledUpdated(_enabled);
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.0;
import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";
/**
* @dev Implementation of the {IERC20} interface.
*
* This implementation is agnostic to the way tokens are created. This means
* that a supply mechanism has to be added in a derived contract using {_mint}.
* For a generic mechanism see {ERC20PresetMinterPauser}.
*
* TIP: For a detailed writeup see our guide
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC20
* applications.
*
* Additionally, an {Approval} event is emitted on calls to {transferFrom}.
* This allows applications to reconstruct the allowance for all accounts just
* by listening to said events. Other implementations of the EIP may not emit
* these events, as it isn't required by the specification.
*
* Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
* functions have been added to mitigate the well-known issues around setting
* allowances. See {IERC20-approve}.
*/
contract ERC20 is Context, IERC20, IERC20Metadata {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev Sets the values for {name} and {symbol}.
*
* All two of these values are immutable: they can only be set once during
* construction.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev Returns the name of the token.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev See {IERC20-totalSupply}.
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
/**
* @dev See {IERC20-balanceOf}.
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
/**
* @dev See {IERC20-transfer}.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - the caller must have a balance of at least `amount`.
*/
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
* - the caller must have allowance for ``from``'s tokens of at least
* `amount`.
*/
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
/**
* @dev Atomically increases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
/**
* @dev Atomically decreases the allowance granted to `spender` by the caller.
*
* This is an alternative to {approve} that can be used as a mitigation for
* problems described in {IERC20-approve}.
*
* Emits an {Approval} event indicating the updated allowance.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
/**
* @dev Moves `amount` of tokens from `from` to `to`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
*/
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
unchecked {
// Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
/**
* @dev Destroys `amount` tokens from `account`, reducing the
* total supply.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
* - `account` must have at least `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
/**
* @dev Updates `owner` s allowance for `spender` based on spent `amount`.
*
* Does not update the allowance amount in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Might emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
/**
* @dev Hook that is called before any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* will be transferred to `to`.
* - when `from` is zero, `amount` tokens will be minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
/**
* @dev Hook that is called after any transfer of tokens. This includes
* minting and burning.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
* has been transferred to `to`.
* - when `from` is zero, `amount` tokens have been minted for `to`.
* - when `to` is zero, `amount` of ``from``'s tokens have been burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}
"
},
"lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable2Step.sol)
pragma solidity ^0.8.0;
import "./Ownable.sol";
/**
* @dev Contract module which provides access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership} and {acceptOwnership}.
*
* This module is used through inheritance. It will make available all functions
* from parent (Ownable).
*/
abstract contract Ownable2Step is Ownable {
address private _pendingOwner;
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
/**
* @dev Returns the address of the pending owner.
*/
function pendingOwner() public view virtual returns (address) {
return _pendingOwner;
}
/**
* @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual override onlyOwner {
_pendingOwner = newOwner;
emit OwnershipTransferStarted(owner(), newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual override {
delete _pendingOwner;
super._transferOwnership(newOwner);
}
/**
* @dev The new owner accepts the ownership transfer.
*/
function acceptOwnership() public virtual {
address sender = _msgSender();
require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner");
_transferOwnership(sender);
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
/**
* @dev Interface for the optional metadata functions from the ERC20 standard.
*
* _Available since v4.1._
*/
interface IERC20Metadata is IERC20 {
/**
* @dev Returns the name of the token.
*/
function name() external view returns (string memory);
/**
* @dev Returns the symbol of the token.
*/
function symbol() external view returns (string memory);
/**
* @dev Returns the decimals places of the token.
*/
function decimals() external view returns (uint8);
}
"
},
"src/NativeLPToken.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20Metadata as IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "./libraries/ConstantsLib.sol";
import {CreditVault} from "./CreditVault.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {ReentrancyGuardTransient} from "./libraries/ReentrancyGuardTransient.sol";
/// @title NativeLPToken - Yield-bearing LP token contract
/// @notice A token contract that represents liquidity provider positions and distributes yield
/// @dev This contract manages LP shares and underlying assets, accruing yield based on protocol revenue
contract NativeLPToken is ERC20, Ownable2Step, ReentrancyGuardTransient {
using SafeERC20 for IERC20;
/*//////////////////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/
/// @notice Whether deposit operations are paused
bool public depositPaused;
/// @notice Whether redeem operations are paused
bool public redeemPaused;
/// @notice The underlying token that this LP token represents
IERC20 public underlying;
/// @notice The address of the credit vault contract
address public creditVault;
/// @notice The number of decimals for this token, matching the underlying token's decimals
uint8 private _decimals;
/// @notice Total amount of underlying assets deposited by LPs
uint256 public totalUnderlying;
/// @notice Total number of shares issued
uint256 public totalShares;
/// @notice Early withdrawal fee in basis points (1 bip = 0.01%)
/// @dev Applied to prevent front-running by users who deposit right before yield distribution and immediately redeem after
uint256 public earlyWithdrawFeeBips;
/// @notice Accumulated early withdrawal fee
uint256 public accEarlyWithdrawFee;
/// @notice Minimum time interval between deposit and redeem (in seconds)
uint256 public minRedeemInterval;
/// @notice Minimum amount required for deposits
uint256 public minDeposit;
/// @notice Mapping of user addresses to their share balances
mapping(address => uint256) public shares;
/// @notice Mapping of trusted operators who can call depositFor and redeemTo functions
mapping(address => bool) public trustedOperators;
/// @notice Mapping of user addresses to their last deposit timestamp
mapping(address => uint256) public lastDepositTimestamp;
/// @notice Mapping of addresses exempt from redeem cooldown period and early withdrawal fees
mapping(address => bool) public redeemCooldownExempt;
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Event emitted when deposit operation is paused
event DepositPaused();
/// @notice Event emitted when deposit operation is unpaused
event DepositUnpaused();
/// @notice Event emitted when redeem operation is paused
event RedeemPaused();
/// @notice Event emitted when redeem operation is unpaused
event RedeemUnpaused();
/// @notice Event emitted when yield is distributed to LP holders
event YieldDistributed(uint256 yieldAmount);
/// @notice Event emitted when minimum redeem interval is updated
event MinRedeemIntervalUpdated(uint256 newInterval);
/// @notice Event emitted when shares are transferred between addresses
event TransferShares(address indexed from, address indexed to, uint256 shares);
/// @notice Event emitted when new shares are minted
event SharesMinted(address indexed from, address indexed to, uint256 shares, uint256 underlyingAmount);
/// @notice Event emitted when shares are burned
event SharesBurned(address indexed from, address indexed to, uint256 shares, uint256 underlyingAmount);
/// @notice Event emitted when minimum deposit amount is updated
event MinDepositUpdated(uint256 oldAmount, uint256 newAmount);
/// @notice Event emitted when early withdraw fee is updated
event EarlyWithdrawFeeBipsUpdated(uint256 oldFeeBips, uint256 newFeeBips);
/// @notice Event emitted when a trusted operator status is updated
event TrustedOperatorUpdated(address indexed account, bool status);
/// @notice Event emitted when an address's redeem cooldown exemption status is updated
event RedeemCooldownExemptUpdated(address indexed account, bool status);
/// @notice Event emitted when early withdrawal fees are withdrawn
event EarlyWithdrawFeeWithdrawn(address indexed recipient, uint256 amount);
/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
address _underlying,
address _creditVault
) ERC20(_name, _symbol) {
underlying = IERC20(_underlying);
creditVault = _creditVault;
_decimals = IERC20(address(underlying)).decimals();
}
/// @notice Deposit underlying tokens to mint LP tokens
/// @param amount Amount of underlying tokens to deposit
/// @return sharesToMint Amount of LP tokens minted
function deposit(uint256 amount)
external
nonReentrant
whenNotPaused(depositPaused)
returns (uint256 sharesToMint)
{
sharesToMint = _deposit(msg.sender, msg.sender, amount);
}
/// @notice Deposit underlying tokens on behalf of another address
/// @param to The address to mint shares to
/// @param amount Amount of underlying tokens to deposit
/// @return sharesToMint Amount of LP tokens minted
function depositFor(
address to,
uint256 amount
) external nonReentrant whenNotPaused(depositPaused) returns (uint256 sharesToMint) {
require(to != address(0), ErrorsLib.ZeroAddress());
require(trustedOperators[msg.sender], ErrorsLib.NotTrustedOperator());
sharesToMint = _deposit(msg.sender, to, amount);
}
/// @notice Redeem LP tokens for underlying tokens
/// @param sharesToBurn Amount of LP tokens to burn
/// @return underlyingAmount Amount of underlying tokens received
function redeem(uint256 sharesToBurn)
external
nonReentrant
whenNotPaused(redeemPaused)
returns (uint256 underlyingAmount)
{
underlyingAmount = _redeem(sharesToBurn, msg.sender);
}
/// @notice Redeem LP tokens and send underlying tokens to a specified address
/// @param sharesToBurn Amount of LP tokens to burn from caller's balance
/// @param to Address that will receive the underlying tokens
/// @return underlyingAmount Amount of underlying tokens sent to the recipient
function redeemTo(
uint256 sharesToBurn,
address to
) external nonReentrant whenNotPaused(redeemPaused) returns (uint256 underlyingAmount) {
require(to != address(0), ErrorsLib.ZeroAddress());
require(trustedOperators[msg.sender], ErrorsLib.NotTrustedOperator());
underlyingAmount = _redeem(sharesToBurn, to);
}
/// @notice Transfers shares from sender to recipient
/// @param recipient The address to transfer shares to
/// @param sharesAmount The number of shares to transfer
/// @return The amount of underlying tokens the shares represent
function transferShares(address recipient, uint256 sharesAmount) external returns (uint256) {
_transferShares(msg.sender, recipient, sharesAmount);
uint256 tokensAmount = getUnderlyingByShares(sharesAmount);
_emitTransferEvents(msg.sender, recipient, tokensAmount, sharesAmount);
return tokensAmount;
}
/// @notice Distributes yield to LP token holders
/// @param yieldAmount Amount of yield to distribute
/// @dev Can only be called by the credit vault
function distributeYield(uint256 yieldAmount) external {
require(totalShares > 0, ErrorsLib.PoolNotInitialized());
require(yieldAmount > 0, ErrorsLib.NoYieldToDistribute());
require(msg.sender == creditVault, ErrorsLib.OnlyCreditVault());
totalUnderlying += yieldAmount;
emit YieldDistributed(yieldAmount);
}
/// @notice Gets the underlying token balance of an account
/// @param account The address to check the balance for
/// @return The amount of underlying tokens the account effectively owns
function balanceOf(address account) public view override returns (uint256) {
return getUnderlyingByShares(shares[account]);
}
/// @notice Gets the total supply of underlying tokens in the pool
/// @return The total amount of underlying tokens managed by this contract
function totalSupply() public view override returns (uint256) {
return totalUnderlying;
}
/// @notice Gets the number of shares owned by an account
/// @param account The address to check shares for
/// @return The number of shares owned by the account
function sharesOf(address account) public view returns (uint256) {
return shares[account];
}
/// @notice Calculates the underlying token amount for a given number of shares
/// @param sharesAmount The number of shares to convert
/// @return The corresponding amount of underlying tokens
function getUnderlyingByShares(uint256 sharesAmount) public view returns (uint256) {
if (totalShares == 0) {
return sharesAmount;
}
return (sharesAmount * totalUnderlying) / totalShares;
}
/// @notice Calculates the number of shares for a given amount of underlying tokens
/// @param underlyingAmount The amount of underlying tokens to convert
/// @return The corresponding number of shares
function getSharesByUnderlying(uint256 underlyingAmount) public view returns (uint256) {
if (totalShares == 0) {
return underlyingAmount;
}
return (underlyingAmount * totalShares) / totalUnderlying;
}
/// @notice Gets the current exchange rate between shares and underlying tokens
/// @return The exchange rate scaled by 1e18 (1:1 = 1e18)
function exchangeRate() public view returns (uint256) {
if (totalShares == 0) {
return 1e18; // Initial exchange rate 1:1
}
return (totalUnderlying * 1e18) / totalShares;
}
/// @notice Gets the number of decimals for this token
/// @return The number of decimals, matching the underlying token
function decimals() public view override returns (uint8) {
return _decimals;
}
/*//////////////////////////////////////////////////////////////////////////
OWNER ONLY OPERATIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Sets the minimum deposit amount
/// @param _minDeposit New minimum deposit amount
/// @dev Can only be called by the owner
function setMinDeposit(uint256 _minDeposit) external onlyOwner {
uint256 oldAmount = minDeposit;
minDeposit = _minDeposit;
emit MinDepositUpdated(oldAmount, _minDeposit);
}
/// @notice Sets the minimum time interval required between deposit and redeem
/// @param _interval New minimum interval in seconds
/// @dev Can only be called by the owner
function setMinRedeemInterval(uint256 _interval) external onlyOwner {
minRedeemInterval = _interval;
emit MinRedeemIntervalUpdated(_interval);
}
/// @notice Sets the early withdrawal fee in basis points (BIPs)
/// @param _earlyWithdrawFeeBips New early withdrawal fee in BIPs
/// @dev Can only be called by the owner
function setEarlyWithdrawFeeBips(uint256 _earlyWithdrawFeeBips) external onlyOwner {
require(_earlyWithdrawFeeBips <= MAX_EARLY_WITHDRAW_FEE_BIPS, ErrorsLib.InvalidFeeBips());
uint256 oldFeeBips = earlyWithdrawFeeBips;
earlyWithdrawFeeBips = _earlyWithdrawFeeBips;
emit EarlyWithdrawFeeBipsUpdated(oldFeeBips, _earlyWithdrawFeeBips);
}
/// @notice Sets the trusted operator status for multiple addresses
/// @param accounts The addresses to update
/// @param statuses The new operator statuses corresponding to each account
function setTrustedOperator(address[] calldata accounts, bool[] calldata statuses) external onlyOwner {
for (uint256 i = 0; i < accounts.length; i++) {
require(accounts[i] != address(0), ErrorsLib.ZeroAddress());
trustedOperators[accounts[i]] = statuses[i];
emit TrustedOperatorUpdated(accounts[i], statuses[i]);
}
}
/// @notice Sets the redeem cooldown exemption status for multiple addresses
/// @param accounts The addresses to update
/// @param statuses The new exemption statuses corresponding to each account
/// @dev Can only be called by the owner
function setRedeemCooldownExempt(address[] calldata accounts, bool[] calldata statuses) external onlyOwner {
for (uint256 i = 0; i < accounts.length; i++) {
require(accounts[i] != address(0), ErrorsLib.ZeroAddress());
redeemCooldownExempt[accounts[i]] = statuses[i];
emit RedeemCooldownExemptUpdated(accounts[i], statuses[i]);
}
}
/// @notice Withdraws accumulated early withdrawal fees
/// @param recipient The address to receive the withdrawn fees
function withdrawEarlyFees(address recipient) external onlyOwner {
require(recipient != address(0), ErrorsLib.ZeroAddress());
// Transfer fees from credit vault to recipient
CreditVault(creditVault).pay(recipient, accEarlyWithdrawFee);
emit EarlyWithdrawFeeWithdrawn(recipient, accEarlyWithdrawFee);
accEarlyWithdrawFee = 0;
}
/*//////////////////////////////////////////////////////////////////////////
PAUSE OPERATIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Function to pause deposit operation
function pauseDeposit() external onlyOwner {
depositPaused = true;
emit DepositPaused();
}
/// @notice Function to unpause deposit operation
function unpauseDeposit() external onlyOwner {
depositPaused = false;
emit DepositUnpaused();
}
/// @notice Function to pause redeem operation
function pauseRedeem() external onlyOwner {
redeemPaused = true;
emit RedeemPaused();
}
/// @notice Function to unpause redeem operation
function unpauseRedeem() external onlyOwner {
redeemPaused = false;
emit RedeemUnpaused();
}
modifier whenNotPaused(bool feature) {
if (feature) {
revert ErrorsLib.FeaturePaused();
}
_;
}
/*//////////////////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
function _deposit(address from, address to, uint256 amount) internal returns (uint256 sharesToMint) {
require(amount >= minDeposit && amount > 0, ErrorsLib.BelowMinimumDeposit());
// Transfer underlying to vault
uint256 balanceBefore = underlying.balanceOf(creditVault);
underlying.safeTransferFrom(from, creditVault, amount);
amount = underlying.balanceOf(creditVault) - balanceBefore;
// Calculate shares to mint
if (totalShares == 0) {
// Lock 1000 shares from first depositor to prevent inflation attack
_mintShares(address(0), MINIMUM_SHARE);
sharesToMint = amount - MINIMUM_SHARE;
} else {
sharesToMint = (amount * totalShares) / totalUnderlying;
}
// Mint shares
_mintShares(to, sharesToMint);
// Update total underlying
totalUnderlying += amount;
lastDepositTimestamp[to] = block.timestamp;
emit SharesMinted(from, to, sharesToMint, amount);
}
function _redeem(uint256 sharesToBurn, address to) internal returns (uint256 underlyingAmount) {
require(sharesToBurn > 0, ErrorsLib.ZeroAmount());
require(shares[msg.sender] >= sharesToBurn, ErrorsLib.InsufficientShares());
// Calculate underlying amount
uint256 grossUnderlyingAmount = (sharesToBurn * totalUnderlying) / totalShares;
underlyingAmount = grossUnderlyingAmount;
if (
block.timestamp < lastDepositTimestamp[msg.sender] + minRedeemInterval && earlyWithdrawFeeBips > 0
&& !redeemCooldownExempt[msg.sender]
) {
uint256 fee = (underlyingAmount * earlyWithdrawFeeBips) / 10_000;
accEarlyWithdrawFee += fee;
underlyingAmount -= fee;
}
// Burn shares first
_burnShares(msg.sender, sharesToBurn);
// Transfer underlying from vault to msg.sender
CreditVault(creditVault).pay(to, underlyingAmount);
// Update total underlying
totalUnderlying -= grossUnderlyingAmount;
emit SharesBurned(msg.sender, to, sharesToBurn, grossUnderlyingAmount);
}
function _mintShares(address to, uint256 shareAmount) internal {
require(shareAmount > 0, ErrorsLib.ZeroAmount());
shares[to] += shareAmount;
totalShares += shareAmount;
}
function _burnShares(address from, uint256 shareAmount) internal {
require(shareAmount > 0, ErrorsLib.ZeroAmount());
require(from != address(0), ErrorsLib.ZeroAddress());
shares[from] -= shareAmount;
totalShares -= shareAmount;
}
/// @notice Override ERC20's _transfer to handle yield-bearing LP token transfers
/// @dev Since this is a yield-bearing token, the actual transfer is done by transferring shares
/// rather than token amounts directly. The shares represent the user's proportion of the
/// total underlying assets including yield.
/// @param from The address to transfer from
/// @param to The address to transfer to
/// @param amount The underlying token amount to transfer
function _transfer(address from, address to, uint256 amount) internal override {
uint256 sharesToTransfer = getSharesByUnderlying(amount);
_transferShares(from, to, sharesToTransfer);
_emitTransferEvents(from, to, amount, sharesToTransfer);
}
function _transferShares(address from, address to, uint256 _shares) internal {
require(from != address(0) && to != address(0), ErrorsLib.ZeroAddress());
require(from != to, ErrorsLib.TransferSelf());
require(to != address(this), ErrorsLib.TransferToContract());
_validateTransferCooldown(from);
shares[from] -= _shares;
shares[to] += _shares;
}
function _validateTransferCooldown(address user) internal view {
// During cooldown period, user can't transfer shares, but can still redeem
require(
lastDepositTimestamp[user] + minRedeemInterval <= block.timestamp || redeemCooldownExempt[user],
ErrorsLib.TransferInCooldown()
);
}
function _emitTransferEvents(address from, address to, uint256 tokenAmount, uint256 sharesAmount) internal {
emit Transfer(from, to, tokenAmount);
emit TransferShares(from, to, sharesAmount);
}
}
"
},
"src/WithdrawQueue.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
import {IERC20Metadata as IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {WrappedNLP} from "./WrappedNLP.sol";
import {SafeERC20} from "./libraries/SafeERC20.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {ReentrancyGuardTransient} from "./libraries/ReentrancyGuardTransient.sol";
/**
* @title WithdrawQueue - Queue-based withdrawal system for WrappedNLP
* @dev Manages withdrawal requests with time windows to prevent JIT attacks
*/
contract WithdrawQueue is Ownable2Step, Pausable, ReentrancyGuardTransient {
using SafeERC20 for IERC20;
struct WithdrawalRequest {
uint256 wNLPAmount; // Amount of wNLP tokens to withdraw
uint256 claimableTime; // When the request becomes claimable
}
/*//////////////////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/
/// @notice Name of the queue
string public name;
/// @notice The WrappedNLP contract
WrappedNLP public immutable wrappedNLP;
/// @notice Minimum withdrawal amount
uint256 public constant MIN_WITHDRAWAL_AMOUNT = 1000;
/// @notice Maximum withdrawal window that can be set
uint256 public constant MAX_WITHDRAWAL_WINDOW = 14 days;
/// @notice Default withdrawal window in seconds
uint256 public withdrawalWindow = 8 hours;
/// @notice Total unclaimed wNLP amount in the queue
uint256 public totalUnclaimedAmount;
/// @notice Mapping from user address to their withdrawal request
mapping(address => WithdrawalRequest) public withdrawalRequests;
/*//////////////////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Emitted when a withdrawal request is created
event WithdrawalRequested(address indexed user, uint256 wNLPAmount, uint256 claimableTime);
/// @notice Emitted when a withdrawal is claimed
event WithdrawalClaimed(address indexed user, address indexed to, uint256 wNLPAmount, uint256 underlyingAmount);
/// @notice Emitted when a withdrawal request is cancelled
event WithdrawalCancelled(address indexed user, uint256 wNLPAmount);
/// @notice Emitted when withdrawal window is updated
event WithdrawalWindowUpdated(uint256 oldWindow, uint256 newWindow);
constructor(WrappedNLP _wrappedNLP) {
wrappedNLP = _wrappedNLP;
name = string.concat(_wrappedNLP.symbol(), "-Queue");
}
/// @notice Request a withdrawal of wNLP tokens
/// @param wNLPAmount Amount of wNLP tokens to withdraw
function requestWithdrawal(uint256 wNLPAmount) external nonReentrant whenNotPaused {
require(wNLPAmount >= MIN_WITHDRAWAL_AMOUNT, "Below minimum withdrawal amount");
require(withdrawalRequests[msg.sender].wNLPAmount == 0, "Pending request exists");
// Transfer wNLP tokens for escrow
IERC20(wrappedNLP).safeTransferFrom(msg.sender, address(this), wNLPAmount);
uint256 claimableTime = block.timestamp + withdrawalWindow;
withdrawalRequests[msg.sender] = WithdrawalRequest({wNLPAmount: wNLPAmount, claimableTime: claimableTime});
// Update total unclaimed amount
totalUnclaimedAmount += wNLPAmount;
emit WithdrawalRequested(msg.sender, wNLPAmount, claimableTime);
}
/// @notice Claim a withdrawal request to the caller's address
/// @return underlyingAmount Amount of underlying tokens received
function claimWithdrawal() external nonReentrant returns (uint256 underlyingAmount) {
return _claimWithdrawal(msg.sender, msg.sender);
}
/// @notice Claim a withdrawal request to a specified address
/// @param to Address to receive the underlying tokens
/// @return underlyingAmount Amount of underlying tokens received
function claimWithdrawalTo(address to) external nonReentrant returns (uint256 underlyingAmount) {
require(to != address(0), ErrorsLib.ZeroAddress());
return _claimWithdrawal(msg.sender, to);
}
/// @notice Cancel a withdrawal request and return the wNLP tokens to the caller
/// @dev Users can cancel at any time (even after claimableTime) to maintain flexibility and user autonomy
/// @return wNLPAmount Amount of wNLP tokens returned to the user
function cancelWithdrawal() external nonReentrant returns (uint256 wNLPAmount) {
WithdrawalRequest storage request = withdrawalRequests[msg.sender];
require(request.wNLPAmount > 0, "No pending request");
wNLPAmount = request.wNLPAmount;
delete withdrawalRequests[msg.sender];
// Update total unclaimed amount
totalUnclaimedAmount -= wNLPAmount;
// Return wNLP tokens to the user
IERC20(wrappedNLP).safeTransfer(msg.sender, wNLPAmount);
emit WithdrawalCancelled(msg.sender, wNLPAmount);
}
/// @notice Set the withdrawal window
/// @param newWindow New window duration in seconds
function setWithdrawalWindow(uint256 newWindow) external onlyOwner {
require(newWindow <= MAX_WITHDRAWAL_WINDOW, ErrorsLib.InvalidAmount());
uint256 oldWindow = withdrawalWindow;
withdrawalWindow = newWindow;
emit WithdrawalWindowUpdated(oldWindow, newWindow);
}
/// @notice Get withdrawal request details for a user
/// @param user The user address
/// @return request The withdrawal request struct
function getWithdrawalRequest(address user) external view returns (WithdrawalRequest memory request) {
return withdrawalRequests[user];
}
function _claimWithdrawal(address user, address to) internal returns (uint256 underlyingAmount) {
WithdrawalRequest storage request = withdrawalRequests[user];
require(request.wNLPAmount > 0, ErrorsLib.ZeroAmount());
require(block.timestamp >= request.claimableTime, "Not claimable");
uint256 wNLPAmount = request.wNLPAmount;
delete withdrawalRequests[user];
// Update total unclaimed amount
totalUnclaimedAmount -= wNLPAmount;
// Execute withdrawal through WrappedNLP
underlyingAmount = wrappedNLP.unwrapAndRedeem(wNLPAmount, to);
emit WithdrawalClaimed(user, to, wNLPAmount, underlyingAmount);
}
}
"
},
"src/libraries/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.28;
import "openzeppelin/token/ERC20/IERC20.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
/**
* @dev An operation with an ERC-20 token failed.
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
function safeApprove(IERC20 token, address spender, uint256 value) internal {
forceApprove(token, spender, value);
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*
* NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
* only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
* set here.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
}
}
"
},
"src/libraries/ErrorsLib.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;
/// @title Custom error definitions for the protocol
/// @notice This library contains all the error definitions used throughout the contract
/// @dev The errors are arranged in alphabetical order
library ErrorsLib {
/// @notice Thrown when actual amount deviation exceeds 10%
error AmountDeviationExceeds();
/// @notice Thrown when deposit amount is below minimum required
error BelowMinimumDeposit();
/// @notice Thrown when epoch update is attempted before minimum interval
error EpochUpdateInCoolDown();
/// @notice Thrown when LP token exchange rate increases more than allowed
error ExchangeRateIncreaseTooMuch();
/// @notice Thrown when feature is paused
error FeaturePaused();
/// @notice Thrown when there are insufficient funding fees to withdraw
error InsufficientFundingFees();
/// @notice Thrown when LP token shares are insufficient
error InsufficientShares();
/// @notice
Submitted on: 2025-10-28 12:56:59
Comments
Log in to comment.
No comments yet.