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/HourglassStableVaultKYC.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { HourglassStableVaultBridgeManager } from "./base/HourglassStableVaultBridgeManager.sol";
import { HourglassStableVaultDepositCapManager } from "./base/HourglassStableVaultDepositCapManager.sol";
import { HourglassStableVaultDepositWindowManager } from "./base/HourglassStableVaultDepositWindowManager.sol";
import { HourglassStableVaultKYCManager } from "./base/HourglassStableVaultKYCManager.sol";
import { HourglassStableVaultTreasuryManager } from "./base/HourglassStableVaultTreasuryManager.sol";
import { IBridgeableVault } from "./interfaces/IBridgeableVault.sol";
enum OperationalMode {
Deposit,
Kyc,
Yield,
Withdraw,
Recovery
}
/**
* @title HourglassStableVaultKYC
* @author Hourglass
* @notice ERC20 pre-deposit vault with KYC for Stable. Accepts USDC deposits, deploys to treasury for yield generation
* @dev This vault is a KYC-enabled vault that generates yield for KYC users only.
*
* Phase-by-phase logic:
* 1. Deposit Phase:
* - Users receive shares at a 1:1 ratio
*
* 2. KYC Phase:
* - Admin can mark users as KYC approved during the KYC phase
* - Non-KYC users can recover their USDC at a 1:1 ratio
*
* 3. Yield Phase:
* - Treasury can withdraw KYC-approved USDC to treasury address
* - Non-KYC users can recover their USDC at a 1:1 ratio
*
* 4. Withdraw Phase:
* - KYC users can withdraw pro-rata USDT via the bridge contract
* - Non-KYC users can recover their USDC at a 1:1 ratio
*
* 5. Recovery Phase:
* - After RECOVERY_TIMESTAMP, anyone can transition to Recovery mode
* - Non-KYC users recover their USDC at a 1:1 ratio
* - KYC users recover their USDT at a pro-rata ratio plus any undeployed USDC pro-rata
*
* Accounting State:
* - All deposited USDC is initially non-KYC
* - When a user is marked KYC, their USDC moves from non-KYC pool to KYC pool
* - Deployable assets are USDC in the KYC pool
* - Non-KYC recoveries pull from the non-KYC USDC pool
* - KYC recoveries pull from the USDT balance and undeployed USDC
*/
contract HourglassStableVaultKYC is
IBridgeableVault,
ERC20,
ERC20Permit,
AccessControl,
ReentrancyGuard,
HourglassStableVaultDepositWindowManager,
HourglassStableVaultDepositCapManager,
HourglassStableVaultBridgeManager,
HourglassStableVaultKYCManager,
HourglassStableVaultTreasuryManager
{
using SafeERC20 for IERC20;
using Math for uint256;
// ------------------------------------------
// State Variables
// ------------------------------------------
/// @notice USDC token contract address on Ethereum mainnet
IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
/// @notice USDT token contract address on Ethereum mainnet
IERC20 public constant USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
/// @notice Number of decimals for the vault shares (matches USDC/USDT)
uint8 private constant DECIMALS = 6;
/// @notice Role identifier for admin operations
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
/// @notice Role identifier for treasury operations
bytes32 public constant TREASURY_ROLE = keccak256("TREASURY_ROLE");
/// @notice Maximum number of users that can be processed in a single batch operation
uint256 public constant MAX_KYC_BATCH_SIZE = 100;
/// @notice Timestamp after which the vault can enter Recovery mode permissionlessly
/// @dev Set to 180 days from deployment
uint256 public immutable RECOVERY_TIMESTAMP;
/// @notice Current operational mode of the vault
/// @dev Starts in Deposit mode and transitions through phases
OperationalMode private _mode = OperationalMode.Deposit;
// sharesKyc
// sharesNonKyc
/// @notice Total shares held by non-KYC users
/// @dev Decreases when users are marked as KYC approved
uint256 public sharesNonKyc;
/// @notice Total shares held by KYC-approved users
/// @dev Increases when users are marked as KYC approved
uint256 public sharesKyc;
/// @notice Total USDC assets available for deployment from KYC pool
/// @dev Decreases when treasury withdraws for yield generation
uint256 public usdcKycDeployable;
// ------------------------------------------
// Events
// ------------------------------------------
/**
* @notice Emitted when the operational mode changes
* @param previousMode The previous operational mode
* @param newMode The new operational mode
*/
event OperationalModeChanged(OperationalMode indexed previousMode, OperationalMode indexed newMode);
/**
* @notice Emitted when treasury withdraws funds
* @param to Recipient address
* @param amount Amount withdrawn
*/
event TreasuryWithdrawal(address indexed to, uint256 indexed amount);
/**
* @notice Emitted when non-KYC user recovers their funds
* @param user User address
* @param usdcAmount Amount of USDC recovered
* @param shares Amount of shares burned
*/
event NonKYCRecovery(address indexed user, uint256 indexed usdcAmount, uint256 shares);
/**
* @notice Emitted when KYC user recovers both USDT and undeployed USDC in recovery mode
* @param user User address
* @param usdtAmount Amount of USDT recovered
* @param usdcAmount Amount of undeployed USDC recovered
* @param shares Amount of shares burned
*/
event KYCRecovery(address indexed user, uint256 indexed usdtAmount, uint256 usdcAmount, uint256 shares);
/**
* @notice Emitted when KYC user recovers USDT in withdrawal mode
* @param sender The address initiating the redeem
* @param receiver The address receiving the USDT
* @param owner The owner of the shares being redeemed
* @param usdtAmount Amount of USDT recovered
* @param shares Amount of shares burned
*/
event KYCRedeem(
address indexed sender, address indexed receiver, address indexed owner, uint256 usdtAmount, uint256 shares
);
/**
* @notice Emitted when ERC20 tokens are recovered from the contract
* @param token The token address
* @param to The recipient address
* @param amount The amount of tokens recovered
*/
event RecoveredERC20(address indexed token, address indexed to, uint256 indexed amount);
/**
* @notice Emitted when a deposit occurs
* @param sender The address initiating the deposit
* @param owner The address that will own the shares
* @param shares Amount of shares minted
*/
event Deposit(address indexed sender, address indexed owner, uint256 shares);
// ------------------------------------------
// Errors
// ------------------------------------------
/// @notice Thrown when attempting an operation without sufficient balance
error InsufficientBalance();
/// @notice Thrown when attempting to recover forbidden token
error RecoveryForbidden(address token);
/// @notice Thrown when attempting to recover zero amount of tokens or ETH
error RecoverZeroAmount();
/// @notice Thrown when an amount parameter is zero but must be non-zero
error ZeroAmount();
/// @notice Thrown when an address parameter is zero but must be non-zero
error ZeroAddress();
/// @notice Thrown when vault is not in the expected operational mode
error InvalidOperationalMode(OperationalMode currentMode, OperationalMode expectedMode);
/// @notice Thrown when trying to transition to Recovery mode before the recovery timestamp
error TransitionToRecoveryFailed();
/// @notice Thrown when there are insufficient funds for deployment to treasury
error InsufficientFundsForDeployment();
/// @notice Thrown when attempting to transfer shares outside of allowed conditions
error TransfersDisabled();
/// @notice Thrown when user has no shares
error NoSharesFound();
/// @notice Thrown when batch array is empty
error EmptyBatch();
/// @notice Thrown when batch size exceeds maximum allowed
error BatchTooLarge(uint256 provided, uint256 max);
// ------------------------------------------
// Modifiers
// ------------------------------------------
/**
* @notice Ensures the vault is in the expected operational mode
* @param expectedMode The operational mode that the vault must be in
*/
modifier onlyMode(OperationalMode expectedMode) {
if (_mode != expectedMode) revert InvalidOperationalMode(_mode, expectedMode);
_;
}
/**
* @notice Ensures the provided address is not zero address
* @param addr The address to validate
*/
modifier onlyNonZeroAddress(address addr) {
if (addr == address(0)) revert ZeroAddress();
_;
}
/**
* @notice Ensures the provided amount is not zero
* @param amount The amount to validate
*/
modifier onlyNonZeroAmount(uint256 amount) {
if (amount == 0) revert ZeroAmount();
_;
}
// ------------------------------------------
// Constructor & Receive
// ------------------------------------------
/**
* @notice Vault constructor
* @param _admin Address to receive all administrative roles
* @param _name Name of the vault token
* @param _symbol Symbol of the vault token
* @param _depositCap The initial deposit cap for USDC deposits
* @param _depositStart Unix timestamp when deposits open (inclusive)
* @param _depositEnd Unix timestamp when deposits close (inclusive)
* @param _treasuryAddress Address of the treasury for yield deployment
* @dev Bridge contract for withdrawals set later to avoid circular dependency.
* Sets RECOVERY_TIMESTAMP to 180 days from deployment.
* Grants DEFAULT_ADMIN_ROLE, ADMIN_ROLE, and TREASURY_ROLE to _admin.
*/
constructor(
address _admin,
string memory _name,
string memory _symbol,
uint256 _depositCap,
uint64 _depositStart,
uint64 _depositEnd,
address _treasuryAddress
)
HourglassStableVaultDepositWindowManager(_depositStart, _depositEnd)
HourglassStableVaultDepositCapManager(_depositCap)
HourglassStableVaultTreasuryManager(_treasuryAddress)
ERC20(_name, _symbol)
ERC20Permit(_name)
onlyNonZeroAddress(_admin)
{
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
_grantRole(ADMIN_ROLE, _admin);
_grantRole(TREASURY_ROLE, _admin);
RECOVERY_TIMESTAMP = block.timestamp + 180 days;
}
// ------------------------------------------
// External Functions - Admin
// ------------------------------------------
/**
* @notice Transitions the vault from Deposit to Kyc mode
* @dev Only callable by admin role when in Deposit mode.
* After transition, no new deposits are accepted.
*/
function transitionToKycMode() external onlyRole(ADMIN_ROLE) onlyMode(OperationalMode.Deposit) {
_transitionToModeUnsafe(OperationalMode.Kyc);
}
/**
* @notice Transitions the vault from KYC to Yield mode
* @dev Called after KYC verification is complete.
* Only callable by admin role when in Kyc mode.
* After transition, treasury can withdraw KYC-approved funds.
*/
function transitionToYieldMode() external onlyRole(ADMIN_ROLE) onlyMode(OperationalMode.Kyc) {
_transitionToModeUnsafe(OperationalMode.Yield);
}
/**
* @notice Transitions the vault from Yield to Withdraw mode
* @dev Called after yield generation is complete and USDT has been returned.
* Only callable by admin role when in Yield mode.
* Treasury should have returned USDT before this transition.
* After transition, KYC users can withdraw via bridge.
*/
function transitionToWithdrawMode() external onlyRole(ADMIN_ROLE) onlyMode(OperationalMode.Yield) {
_transitionToModeUnsafe(OperationalMode.Withdraw);
}
/**
* @notice Transitions the vault from any state to Recovery mode (a terminal state)
* @dev Must be past the recovery timestamp. Can be called permissionlessly.
* Recovery mode is a terminal state that cannot be exited.
* Provides emergency exit for all users if admin is compromised.
* Non-KYC users recover USDC at 1:1, KYC users recover USDT + undeployed USDC.
*/
function transitionToRecoveryMode() external {
if (block.timestamp < RECOVERY_TIMESTAMP) revert TransitionToRecoveryFailed();
_transitionToModeUnsafe(OperationalMode.Recovery);
}
/**
* @notice Modifies a deposit window that is pending (has not yet started)
* @param newStart Unix timestamp when deposits open
* @param newEnd Unix timestamp when deposits close
*/
function modifyPendingDepositWindow(
uint64 newStart,
uint64 newEnd
)
external
onlyRole(ADMIN_ROLE)
onlyMode(OperationalMode.Deposit)
{
_modifyPendingDepositWindow(newStart, newEnd);
}
/**
* @notice Modifies the end timestamp of a deposit window that has already started
* @param newEnd Unix timestamp when deposits close
*/
function modifyStartedDepositWindow(uint64 newEnd)
external
onlyRole(ADMIN_ROLE)
onlyMode(OperationalMode.Deposit)
{
_modifyStartedDepositWindow(newEnd);
}
/**
* @notice Updates the deposit cap for the vault
* @param newCap New maximum total USDT allowed (must be > 0 and >= current deposits)
*/
function setDepositCap(uint256 newCap) external onlyRole(ADMIN_ROLE) onlyMode(OperationalMode.Deposit) {
_setDepositCap(sharesNonKyc, newCap);
}
/**
* @notice Sets the authorized bridge contract address and marks it as KYC-approved
* @param newBridgeContract Address of the bridge contract that can call withdraw
* @dev Also marks the bridge as KYC-approved to allow it to redeem shares
*/
function setBridgeContract(address newBridgeContract) external onlyRole(ADMIN_ROLE) {
_setBridgeContract(newBridgeContract);
_setKycStatus(newBridgeContract, true);
}
/**
* @notice Sets the treasury address where funds are sent
* @param newTreasury Address of the new treasury
*/
function setTreasuryAddress(address newTreasury) external onlyRole(ADMIN_ROLE) {
_setTreasuryAddress(newTreasury);
}
/**
* @notice Batch set KYC status for multiple users
* @param users Array of addresses to update (max 100)
* @param status KYC status to set
* @dev Reverts if batch is empty or exceeds MAX_BATCH_SIZE
*/
function batchSetKycStatus(
address[] calldata users,
bool status
)
external
onlyRole(ADMIN_ROLE)
onlyMode(OperationalMode.Kyc)
{
uint256 length = users.length;
// Validate batch size
if (length == 0) revert EmptyBatch();
if (length > MAX_KYC_BATCH_SIZE) revert BatchTooLarge(length, MAX_KYC_BATCH_SIZE);
for (uint256 i = 0; i < length;) {
address user = users[i];
if (user == address(0)) revert ZeroAddress();
_setKycStatusWithAccounting(user, status);
++i;
}
}
/**
* @notice Recovers mistakenly sent ERC20 tokens from the contract
* @param token Address of the ERC20 token to recover
* @param to Recipient address for recovered tokens
* @dev Only recovers excess tokens beyond what users can claim.
* Protects sharesNonKyc USDC for non-KYC users.
* Protects usdcKycDeployable USDC for KYC users.
* Protects all USDT as it belongs to KYC users.
*/
function recoverErc20(
address token,
address to
)
external
onlyRole(ADMIN_ROLE)
onlyNonZeroAddress(token)
onlyNonZeroAddress(to)
nonReentrant
{
uint256 balance = IERC20(token).balanceOf(address(this));
uint256 protected = 0;
// Compute protected amounts for assets the contract holds
if (token == address(USDC)) {
// must protect sharesNonKyc so non-KYC users can recover their assets
// must protect usdcKycDeployable since it belongs to the kyc users
protected = sharesNonKyc + usdcKycDeployable;
} else if (token == address(USDT)) {
// all usdt belongs to the kyc users
revert RecoveryForbidden(address(USDT));
}
uint256 recoverable = balance > protected ? balance - protected : 0;
if (recoverable == 0) revert RecoverZeroAmount();
IERC20(token).safeTransfer(to, recoverable);
emit RecoveredERC20(token, to, recoverable);
}
// ---------------------
// Treasury Functions
// ---------------------
/**
* @notice Allows treasury to transfer USDC funds during Yield phase
* @param amount Amount of USDC to transfer to treasury for yield generation
* @dev Treasury can only access KYC pool funds, not non-KYC assets.
* Reduces usdcKycDeployable to track deployed funds.
* Ensures non-KYC user funds remain untouched and recoverable.
*/
function transferToTreasury(uint256 amount)
external
nonReentrant
onlyRole(TREASURY_ROLE)
onlyMode(OperationalMode.Yield)
onlyWhenTreasurySet
onlyNonZeroAmount(amount)
{
if (amount > usdcKycDeployable) {
revert InsufficientFundsForDeployment();
}
uint256 usdcBalance = USDC.balanceOf(address(this));
if (amount > usdcBalance) {
revert InsufficientFundsForDeployment();
}
usdcKycDeployable -= amount;
USDC.safeTransfer(_treasuryAddress, amount);
emit TreasuryWithdrawal(_treasuryAddress, amount);
}
// ------------------------------------------
// Public Functions
// ------------------------------------------
/**
* @notice Returns the current operational mode of the vault
* @return The current operational mode of the vault
*/
function operationalMode() public view returns (OperationalMode) {
return _mode;
}
/**
* @notice Returns the decimals of the vault shares
* @return decimals The number of decimals (6, same as USDC and USDT)
*/
function decimals() public pure override(ERC20) returns (uint8) {
return DECIMALS;
}
/**
* @notice Deposits USDC and mints shares for non-KYC users
* @param assets Amount of USDC to deposit
* @param receiver Address to receive the shares
* @dev Follows ERC4626-style pattern: check → preview → execute.
* Shares are minted at 1:1 ratio to prevent inflation attacks.
* Deposits are subject to the configured deposit cap.
*/
function deposit(
uint256 assets,
address receiver
)
public
nonReentrant
onlyMode(OperationalMode.Deposit)
onlyWhenDepositWindowOpen
onlyNonZeroAmount(assets)
{
// 1. Check against maximum
if (assets > maxDeposit()) {
revert DepositExceedsCap();
}
// 1. Transfer assets from depositor
USDC.safeTransferFrom(msg.sender, address(this), assets);
// 2. Update accounting
sharesNonKyc += assets;
// 3. Mint shares to receiver
_mint(receiver, assets);
// 4. Emit event
emit Deposit(msg.sender, receiver, assets);
}
/**
* @notice Allows non-KYC users to redeem shares for USDC at 1:1 ratio
* @param shares Amount of shares to burn
* @param receiver Address to receive the USDC
* @param owner Owner of the shares being redeemed
* @dev Available in any phase (including Deposit) - non-KYC funds are always protected.
* Non-KYC users maintain 1:1 redemption rate throughout all phases.
* This ensures principal preservation for users who don't complete KYC.
*/
function redeemNonKyc(
uint256 shares,
address receiver,
address owner
)
public
nonReentrant
onlyNonZeroAmount(shares)
onlyNonKyc(owner)
{
// 1. Handle allowance if caller is not owner
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
// 2. Update accounting
sharesNonKyc -= shares;
// 3. Burn shares from owner
_burn(owner, shares);
// 4. Transfer USDC to receiver
USDC.safeTransfer(receiver, shares);
// 5. Emit events
emit NonKYCRecovery(owner, shares, shares);
}
/**
* @notice Allows KYC users to redeem shares for both USDT and undeployed USDC in Recovery mode
* @param shares Amount of shares to redeem
* @param receiver Address to receive the assets
* @param owner Owner of the shares being redeemed
* @dev Can only be called in Recovery mode by KYC approved users.
* Returns pro-rata share of both USDT and any undeployed USDC.
* Non-KYC users should use redeemNonKyc() which works in any phase.
* This function ensures KYC users can recover all assets in emergency scenarios,
* including both yield-bearing USDT and any USDC not yet deployed.
*/
function redeemRecoveryKyc(
uint256 shares,
address receiver,
address owner
)
external
nonReentrant
onlyMode(OperationalMode.Recovery)
onlyNonZeroAmount(shares)
onlyKycApproved(owner)
{
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
// 2. Calculate both USDT and undeployed USDC amounts
(uint256 usdtOut, uint256 usdcOut,) = previewRedeem(owner, shares);
// 4. Update accounting
sharesKyc -= shares;
if (usdcOut > 0) {
usdcKycDeployable -= usdcOut;
}
// 5. Burn shares
_burn(owner, shares);
// 6. Transfer both assets
if (usdtOut > 0) {
USDT.safeTransfer(receiver, usdtOut);
}
if (usdcOut > 0) {
USDC.safeTransfer(receiver, usdcOut);
}
// 7. Emit event
emit KYCRecovery(owner, usdtOut, usdcOut, shares);
}
// ------------------------------------------
// Preview Functions (ERC4626-style)
// ------------------------------------------
function previewRedeem(
address user,
uint256 shares
)
public
view
returns (uint256 usdtOut, uint256 usdcOut, bool isKyc)
{
if (shares > balanceOf(user)) {
revert InsufficientBalance();
}
isKyc = _isKycApproved(user);
if (isKyc) {
usdtOut = _convertToAssets(shares, sharesKyc, USDT.balanceOf(address(this)));
usdcOut = _convertToAssets(shares, sharesKyc, usdcKycDeployable);
} else {
usdtOut = 0;
usdcOut = shares;
}
}
// ------------------------------------------
// Max Functions (ERC4626-style)
// ------------------------------------------
/**
* @notice Maximum amount of USDC that can be deposited
* @return maxAssets Maximum USDC that can be deposited
*/
function maxDeposit() public view returns (uint256 maxAssets) {
if (_mode != OperationalMode.Deposit || !_depositWindowOpen()) return 0;
return _getMaxDepositAgainstCap(sharesNonKyc);
}
// ------------------------------------------
// Transfer Override Functions
// ------------------------------------------
// IBridgeableVault Implementation
// ------------------------------------------
/**
* @notice Redeems vault shares for underlying assets for bridge operations
* @param shares Amount of vault shares to redeem
* @param receiver Address to receive the redeemed assets
* @param owner Address that owns the shares being redeemed
* @return usdtOut Amount of USDT returned to the receiver
* @dev Implements IBridgeableVault interface for bridge withdrawals
* @dev Can only be called in Withdraw mode by authorized bridge contract
* @dev KYC users receive pro-rata share of USDT based on their share ownership
*/
function redeemBridge(
uint256 shares,
address receiver,
address owner
)
external
override
nonReentrant
onlyNonZeroAmount(shares)
onlyCallerIsBridge
onlyMode(OperationalMode.Withdraw)
returns (uint256 usdtOut)
{
// Preview conversion
(usdtOut,,) = previewRedeem(owner, shares);
if (usdtOut == 0) revert ZeroAmount();
// Handle allowance if caller is not owner
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
// Update accounting
sharesKyc -= shares;
// Burn shares from owner
_burn(owner, shares);
// Transfer USDT to receiver
USDT.safeTransfer(receiver, usdtOut);
// Emit event
emit KYCRedeem(msg.sender, receiver, owner, usdtOut, shares);
}
/**
* @notice Preview the amount of assets that would be received for bridge redemption
* @param shares Amount of shares to preview redemption for
* @return assets Amount of USDT that would be received
* @dev Implements IBridgeableVault interface
* @dev Uses the KYC-specific previewRedeem with msg.sender for compatibility
*/
function previewRedeemBridge(uint256 shares) external view override returns (uint256 assets) {
// Use the KYC-specific previewRedeem function with msg.sender
// This will check if the caller is KYC'd and return appropriate amounts
(uint256 usdtOut,,) = previewRedeem(msg.sender, shares);
return usdtOut;
}
// ------------------------------------------
/**
* @notice Override internal _update to restrict share transfers
* @param from Sender address
* @param to Recipient address
* @param value Amount of shares to transfer
* @dev Only KYC users can transfer to bridge during Withdraw phase
* Mints and burns (from or to address(0)) are always allowed
*/
function _update(address from, address to, uint256 value) internal virtual override {
if (
from == address(0) || to == address(0)
|| (_mode == OperationalMode.Withdraw && _isKycApproved(from) && to == _bridgeContract)
) {
super._update(from, to, value);
return;
}
revert TransfersDisabled();
}
// ------------------------------------------
// Internal Functions
// ------------------------------------------
/**
* @notice Internal helper to convert shares to assets (always rounds down)
* @param shares Amount of shares to convert
* @param supply Current supply of shares in the pool
* @param totalAssets Current total assets in the pool
* @return Amount of assets equivalent to the shares
*/
function _convertToAssets(uint256 shares, uint256 supply, uint256 totalAssets) internal pure returns (uint256) {
if (supply == 0) return 0;
return Math.mulDiv(shares, totalAssets, supply, Math.Rounding.Floor);
}
/**
* @notice Transitions the vault to a new operational mode
* @param newMode The new operational mode
* @dev It is the job of the caller to ensure the prevMode -> newMode transition is valid
*/
function _transitionToModeUnsafe(OperationalMode newMode) private {
OperationalMode prevMode = _mode;
_mode = newMode;
emit OperationalModeChanged(prevMode, newMode);
}
// ------------------------------------------
// Internal Execution Functions (ERC4626-style)
// ------------------------------------------
/**
* @notice Internal function to set KYC status and update accounting
* @param user Address to update
* @param status KYC status to set
* @dev user validated to be non-zero address at call site
*/
function _setKycStatusWithAccounting(address user, bool status) internal {
uint256 userShares = balanceOf(user);
if (userShares == 0) revert NoSharesFound();
// Update KYC status using base contract function (emits KYCStatusSet event)
// Note: _setKycStatus will revert if status is unchanged, guaranteeing the status is different
_setKycStatus(user, status);
// Update share and asset accounting based on status change
// We know status changed, so we can simplify the logic
if (status) {
// Marking as KYC - move from non-KYC pool to KYC pool
sharesNonKyc -= userShares;
sharesKyc += userShares;
usdcKycDeployable += userShares;
} else {
// Unmarking as KYC - move from KYC pool back to non-KYC pool
sharesNonKyc += userShares;
sharesKyc -= userShares;
usdcKycDeployable -= userShares;
}
}
}
"
},
"lib/openzeppelin-contracts/contracts/access/AccessControl.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {IERC165, ERC165} from "../utils/introspection/ERC165.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControl is Context, IAccessControl, ERC165 {
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
mapping(bytes32 role => RoleData) private _roles;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
return _roles[role].hasRole[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
return _roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = getRoleAdmin(role);
_roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
if (!hasRole(role, account)) {
_roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
if (hasRole(role, account)) {
_roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {IERC20Errors} from "../../interfaces/draft-IERC6093.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}.
*
* 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 ERC-20
* applications.
*/
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev Sets the values for {name} and {symbol}.
*
* Both 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 returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view virtual 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 returns (uint8) {
return 18;
}
/// @inheritdoc IERC20
function totalSupply() public view virtual returns (uint256) {
return _totalSupply;
}
/// @inheritdoc IERC20
function balanceOf(address account) public view virtual 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 `value`.
*/
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
/// @inheritdoc IERC20
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `value` 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 value) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, value);
return true;
}
/**
* @dev See {IERC20-transferFrom}.
*
* Skips emitting an {Approval} event indicating an allowance update. This is not
* required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].
*
* 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 `value`.
* - the caller must have allowance for ``from``'s tokens of at least
* `value`.
*/
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, value);
_transfer(from, to, value);
return true;
}
/**
* @dev Moves a `value` 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.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
/**
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
/**
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
/**
* @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
* Relies on the `_update` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead
*/
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
/**
* @dev Sets `value` 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.
*
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
*/
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
/**
* @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
*
* By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
* `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
* `Approval` event during `transferFrom` operations.
*
* Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
* true using the following override:
*
* ```solidity
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
* super._approve(owner, spender, value, true);
* }
* ```
*
* Requirements are the same as {_approve}.
*/
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
/**
* @dev Updates `owner`'s allowance for `spender` based on spent `value`.
*
* Does not update the allowance value in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Does not emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance < type(uint256).max) {
if (currentAllowance < value) {
revert ERC20InsufficientAllowance(spender, currentAllowance, value);
}
unchecked {
_approve(owner, spender, currentAllowance - value, false);
}
}
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/ERC20Permit.sol)
pragma solidity ^0.8.20;
import {IERC20Permit} from "./IERC20Permit.sol";
import {ERC20} from "../ERC20.sol";
import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
import {Nonces} from "../../../utils/Nonces.sol";
/**
* @dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*/
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces {
bytes32 private constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
/**
* @dev Permit deadline has expired.
*/
error ERC2612ExpiredSignature(uint256 deadline);
/**
* @dev Mismatched signature.
*/
error ERC2612InvalidSigner(address signer, address owner);
/**
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
*
* It's a good idea to use the same `name` that is defined as the ERC-20 token name.
*/
constructor(string memory name) EIP712(name, "1") {}
/// @inheritdoc IERC20Permit
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, v, r, s);
if (signer != owner) {
revert ERC2612InvalidSigner(signer, owner);
}
_approve(owner, spender, value);
}
/// @inheritdoc IERC20Permit
function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
/// @inheritdoc IERC20Permit
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {
return _domainSeparatorV4();
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
pragma solidity >=0.4.16;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.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)));
}
/**
* @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 Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
* has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferFromAndCallRelaxed(
IERC1363 token,
address from,
address to,
uint256 value,
bytes memory data
) internal {
if (to.code.length == 0) {
safeTransferFrom(token, from, to, value);
} else if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
* Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
* once without retrying, and relies on the returned value to be true.
*
* Reverts if the returned value is other than `true`.
*/
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
forceApprove(token, to, value);
} else if (!token.approveAndCall(to, value, data)) {
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 {_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
Submitted on: 2025-11-06 10:11:09
Comments
Log in to comment.
No comments yet.