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/KYCOneWayVault.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;
import {ERC4626Upgradeable} from "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import {BaseAccount} from "./BaseAccount.sol";
import {OwnableUpgradeable} from "@openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {IZkMe} from './IZkMe.sol';
/**
* @title KYCOneWayVault
* @dev A one-way vault contract that enables deposits on one chain with withdrawal requests
* to be processed on another domain/chain. Uses ERC4626 tokenized vault standard.
*
* This vault handles:
* - Deposits with fee collection
* - Withdrawals with fee collection
* - Strategist-controlled redemption rate updates
* - Cross-domain withdrawal requests (one-way from source to destination)
* - Fee distribution between platform and strategist
* - Deposit caps
* - Pausability
*/
contract KYCOneWayVault is
Initializable,
ERC4626Upgradeable,
OwnableUpgradeable,
ReentrancyGuardUpgradeable,
UUPSUpgradeable
{
using Math for uint256;
/**
* @dev bytes32(uint256(keccak256('kyc_one_way_vault.zkme_config')) - 1)
*/
bytes32 internal constant _ZKME_CONFIG_SLOT = 0x6aa30f72c819d6ffdc11d4f73f6dbad58c3fe2ea745f3548ccd5eb0e816f9ff9;
function _getZkMeConfig() private pure returns (ZkMeConfig storage $) {
assembly {
$.slot := _ZKME_CONFIG_SLOT
}
}
/**
* @dev bytes32(uint256(keccak256('kyc_one_way_vault.kyc_allowed_users')) - 1)
*/
bytes32 internal constant _KYC_ALLOWED_USERS_SLOT = 0x100924d8e5ab1df33616bfd25eded8b01d3d68d2ade2d07beeff707eff244277;
function _getKycAllowedUsers() private pure returns (KycAllowedUsers storage $) {
assembly {
$.slot := _KYC_ALLOWED_USERS_SLOT
}
}
/**
* @dev Emitted when the vault's paused state changes
* @param paused New paused state
*/
event PausedStateChanged(bool paused);
/**
* @dev Emitted when the vault is paused due to a stale redemption rate
* @param lastUpdateTimestamp Timestamp of the last rate update
* @param currentTimestamp Current timestamp when the pause was triggered
*/
event StaleRatePaused(uint64 lastUpdateTimestamp, uint64 currentTimestamp);
/**
* @dev Emitted when the redemption rate is updated
* @param newRate The updated redemption rate
*/
event RateUpdated(uint256 newRate);
/**
* @dev Emitted when fees are distributed to strategist and platform accounts
* @param strategistAccount Address receiving strategist portion of fees
* @param platformAccount Address receiving platform portion of fees
* @param strategistShares Amount of shares distributed to strategist
* @param platformShares Amount of shares distributed to platform
*/
event FeesDistributed(
address indexed strategistAccount,
address indexed platformAccount,
uint256 strategistShares,
uint256 platformShares
);
/**
* @dev Emitted when a withdrawal request is created
* @param id Unique ID for the withdrawal request
* @param owner Address that owns the shares being withdrawn
* @param receiver Address on destination domain to receive assets (as string)
* @param shares Amount of shares being withdrawn
*/
event WithdrawRequested(uint64 indexed id, address owner, string receiver, uint256 shares);
/**
* @dev Emitted when the vault configuration is updated
* @param updater Address that updated the config
* @param newConfig The new configuration
*/
event ConfigUpdated(address indexed updater, KYCOneWayVaultConfig newConfig);
/**
* @dev Emitted when the zkMe configuration is updated
* @param updater Address that updated the config
* @param newConfig The new configuration
*/
event ZkMeConfigUpdated(address indexed updater, ZkMeConfig newConfig);
/**
* @dev Emitted when the user is expliticly allowed or removed from explicitly allowed list
* @param updater Address that updated user status
* @param user Address, the explicit approval of which was altered
* @param allowed Whether this address is explicitly allowed or not
*/
event UserAllowed(address indexed updater, address indexed user, bool allowed);
/**
* @dev Restricts function access to only the strategist
*/
modifier onlyStrategist() {
if (msg.sender != config.strategist) {
revert("Only strategist allowed");
}
_;
}
/**
* @dev Restricts function access to only the owner or strategist
*/
modifier onlyOwnerOrStrategist() {
if (msg.sender != owner() && msg.sender != config.strategist) {
revert("Only owner or strategist allowed");
}
_;
}
/**
* @dev Ensures the vault is not paused
*/
modifier whenNotPaused() {
if (vaultState.paused) {
revert("Vault is paused");
}
_;
}
/**
* @dev Ensures the vault is not paused by a different reason than stale redemption rate
*/
modifier whenNotManuallyPaused() {
if (vaultState.paused && !vaultState.pausedByStaleRate) {
revert("Vault is paused by owner or strategist");
}
_;
}
/**
* @dev Ensures that a function can only be called by either explicitly approved users
* or by users who completed the zkMe KYC procedure
*/
modifier onlyKyc() {
// First, check if user is explicitly approved inside of the KYC vault.
// Only then ask ZkMe if user is approved on its side.
// This order of execution saves gas, avoiding a call to ZkMe when possible.
if (
!_getKycAllowedUsers().allowedUsers[msg.sender] &&
!_getZkMeConfig().zkMe.hasApproved(_getZkMeConfig().cooperator, msg.sender)
) {
revert KycFailed();
}
_;
}
/**
* @dev Returned when user failed to complete KYC
*/
error KycFailed();
/**
* @dev Configuration structure for zkMe KYC service
* @param zkMe zkMe Verify & Certify contract address
* @param cooperator Selector address provided by zkMe
*/
struct ZkMeConfig {
IZkMe zkMe;
address cooperator;
}
/**
* @dev Explicitly allowed users which don't have to go through KYC process
* @param allowedUsers Mapping which stores true if a user is allowed
*/
struct KycAllowedUsers {
mapping(address => bool) allowedUsers;
}
/**
* @dev Configuration structure for the vault
* @param depositAccount Account where deposits are held
* @param strategist Address of the vault strategist
* @param depositFeeBps Fee charged on deposits in basis points (1 BPS = 0.01%)
* @param withdrawFeeBps Fee charged on withdrawals in basis points (1 BPS = 0.01%)
* @param maxRateIncrementBps Maximum allowed relative increase in redemption rate per update (in basis points).
* For example, if maxRateIncrementBps is 100 (1%), at a redemption rate of 200, the new rate can go up to 202.
* @param maxRateDecrementBps Maximum allowed relative decrease in redemption rate per update (in basis points).
* For example, if maxRateDecrementBps is 50 (0.5%), at a redemption rate of 200, the new rate can go down to 199
* @param minRateUpdateDelay Minimum time required between redemption rate updates (in seconds)
* @param maxRateUpdateDelay Maximum time allowed between redemption rate updates (in seconds) before vault automatically pauses
* @param depositCap Maximum assets that can be deposited (0 means no cap)
* @param feeDistribution Configuration for fee distribution between platform and strategist
*/
struct KYCOneWayVaultConfig {
BaseAccount depositAccount;
address strategist;
uint32 depositFeeBps;
uint32 withdrawFeeBps;
uint32 maxRateIncrementBps;
uint32 maxRateDecrementBps;
uint64 minRateUpdateDelay;
uint64 maxRateUpdateDelay;
uint256 depositCap;
FeeDistributionConfig feeDistribution;
}
/**
* @dev Configuration for fee distribution
* @param strategistAccount Address to receive strategist's portion of fees
* @param platformAccount Address to receive platform's portion of fees
* @param strategistRatioBps Strategist's percentage of total fees in basis points
*/
struct FeeDistributionConfig {
address strategistAccount; // Account to receive strategist's portion of fees
address platformAccount; // Account to receive platform's portion of fees
uint32 strategistRatioBps; // Strategist's share of total fees in basis points
}
/**
* @dev Vault state information
* @param paused Whether the vault is currently paused
* @param pausedByOwner Whether the vault was paused by the owner (affects who can unpause)
* @param pausedByStaleRate Whether the vault was paused due to a stale redemption rate (affects who can unpause and redemption rate updates)
*/
struct VaultState {
bool paused;
// If paused by owner, only owner can unpause it
bool pausedByOwner;
// If vault was paused due to a stale redemption rate
bool pausedByStaleRate;
}
/**
* @dev Structure for withdrawal requests to destination domain
* @param id Unique ID for the request
* @param owner Owner of the request who burned shares
* @param redemptionRate Redemption rate at time of request
* @param sharesAmount Amount of shares to be redeemed
* @param receiver Address to receive assets on destination domain (as string, e.g. Neutron address)
*/
struct WithdrawRequest {
uint64 id;
address owner;
uint256 redemptionRate;
uint256 sharesAmount;
string receiver;
}
// Main state variables
KYCOneWayVaultConfig public config;
uint256 public redemptionRate;
VaultState public vaultState;
uint64 public currentWithdrawRequestId;
uint64 public lastRateUpdateTimestamp;
/**
* @dev Total fees collected but not yet distributed, denominated in asset
*/
uint256 public feesAccruedInAsset;
/**
* @dev Mapping from request ID to withdrawal request details
*/
mapping(uint64 => WithdrawRequest) public withdrawRequests;
// Constants
/**
* @dev Constant for basis point calculations (100% = 10000)
*/
uint32 private constant BASIS_POINTS = 1e4;
uint256 internal ONE_SHARE;
/**
* @dev Constructor that disables initializers
* @notice Required for UUPS proxy pattern
*/
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/**
* @dev Initializes the contract replacing the constructor
* @param _owner Address of the contract owner
* @param _config Encoded configuration bytes
* @param underlying Address of the underlying token
* @param vaultTokenName Name of the vault token
* @param vaultTokenSymbol Symbol of the vault token
* @param startingRate Initial redemption rate
*/
function initialize(
address _owner,
bytes memory _config,
address underlying,
string memory vaultTokenName,
string memory vaultTokenSymbol,
uint256 startingRate
) external initializer {
__ERC20_init(vaultTokenName, vaultTokenSymbol);
__ERC4626_init(IERC20(underlying));
__Ownable_init(_owner);
__ReentrancyGuard_init();
__UUPSUpgradeable_init();
config = abi.decode(_config, (KYCOneWayVaultConfig));
_validateConfig(config);
ONE_SHARE = 10 ** decimals();
require(startingRate > 0, "Starting redemption rate cannot be zero");
redemptionRate = startingRate; // Initialize at specified starting rate
lastRateUpdateTimestamp = uint64(block.timestamp); // Set initial timestamp for rate updates
}
/**
* @dev Function that authorizes contract upgrades - required by UUPSUpgradeable
* @param newImplementation address of the new implementation
*/
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
// Upgrade logic comes here
// No additional logic required beyond owner check in modifier
}
/**
* @notice Updates the vault configuration
* @dev Validates all configuration parameters before updating
* @param _config Encoded KYCOneWayVaultConfig struct
*/
function updateConfig(bytes memory _config) external onlyOwner {
KYCOneWayVaultConfig memory decodedConfig = abi.decode(_config, (KYCOneWayVaultConfig));
_validateConfig(decodedConfig);
// All validations passed, update config
config = decodedConfig;
emit ConfigUpdated(msg.sender, decodedConfig);
}
/**
* @notice Updates the zkMe configuration
* @param _zkMe zkMe Verify & Certify contract address
* @param _cooperator Selector address provided by zkMe
*/
function updateZkMeConfig(address _zkMe, address _cooperator) external onlyOwner {
ZkMeConfig storage zkMeConfig = _getZkMeConfig();
zkMeConfig.zkMe = IZkMe(_zkMe);
zkMeConfig.cooperator = _cooperator;
emit ZkMeConfigUpdated(msg.sender, zkMeConfig);
}
/**
* @notice Updates explicit user allowance to use this contract without KYC
* @param _user Address, which status shall be altered
* @param _allowed Whether this address is explicitly allowed or not
*/
function setUserAllowed(address _user, bool _allowed) external onlyOwner {
if (_allowed) {
_getKycAllowedUsers().allowedUsers[_user] = true;
} else {
delete _getKycAllowedUsers().allowedUsers[_user];
}
emit UserAllowed(msg.sender, _user, _allowed);
}
/**
* @notice Returns explicit user allowance to use this contract without KYC
* @param _user Address, status of which to retrieve
* @return Whether this address is explicitly allowed or not
*/
function userAllowed(address _user) external view returns (bool) {
return _getKycAllowedUsers().allowedUsers[_user];
}
/**
* @dev Validates the configuration parameters
* @param decodedConfig The decoded KYCOneWayVaultConfig struct
*/
function _validateConfig(KYCOneWayVaultConfig memory decodedConfig) internal pure {
if (address(decodedConfig.depositAccount) == address(0)) {
revert("Deposit account cannot be zero address");
}
if (decodedConfig.strategist == address(0)) {
revert("Strategist cannot be zero address");
}
if (decodedConfig.depositFeeBps > BASIS_POINTS) {
revert("Deposit fee cannot exceed 100%");
}
if (decodedConfig.withdrawFeeBps > BASIS_POINTS) {
revert("Withdraw fee cannot exceed 100%");
}
if (decodedConfig.maxRateDecrementBps > BASIS_POINTS) {
revert("Max rate decrement cannot exceed 100%");
}
if (decodedConfig.maxRateUpdateDelay == 0) {
revert("Max rate update delay cannot be zero");
}
if (decodedConfig.minRateUpdateDelay > decodedConfig.maxRateUpdateDelay) {
revert("Minimum update delay cannot exceed maximum update delay");
}
if (decodedConfig.feeDistribution.strategistRatioBps > BASIS_POINTS) {
revert("Strategist account fee distribution ratio cannot exceed 100%");
}
if (decodedConfig.feeDistribution.platformAccount == address(0)) {
revert("Platform account cannot be zero address");
}
if (decodedConfig.feeDistribution.strategistAccount == address(0)) {
revert("Strategist account cannot be zero address");
}
}
/**
* @notice Returns the total amount of assets managed by the vault
* @dev Overrides ERC4626 totalAssets to use current redemption rate
* @return Total assets calculated from total shares using current redemption rate
*/
function totalAssets() public view override returns (uint256) {
return _convertToAssets(totalSupply(), Math.Rounding.Floor);
}
/**
* @notice Returns maximum amount that can be deposited for a receiver
* @dev Overrides ERC4626 maxDeposit to enforce deposit cap if configured
* @return Maximum deposit amount allowed
*/
function maxDeposit(address) public view override returns (uint256) {
uint256 cap = config.depositCap;
if (cap == 0) {
return type(uint256).max;
}
uint256 totalDeposits = totalAssets();
if (totalDeposits >= cap) {
return 0;
}
return cap - totalDeposits;
}
/**
* @notice Returns maximum shares that can be minted for a receiver
* @dev Overrides ERC4626 maxMint to enforce deposit cap if configured
* @return Maximum shares that can be minted
*/
function maxMint(address) public view override returns (uint256) {
uint256 cap = config.depositCap;
if (cap == 0) {
return type(uint256).max;
}
uint256 totalDeposits = totalAssets();
if (totalDeposits >= cap) {
return 0;
}
return _convertToShares(cap - totalDeposits, Math.Rounding.Floor);
}
/**
* @notice Deposits assets into the vault, charging a fee if configured
* @dev Overrides ERC4626 deposit to handle fees before calling _deposit
* @param assets Amount of assets to deposit
* @param receiver Address to receive the vault shares
* @return shares Amount of shares minted to receiver
*/
function deposit(uint256 assets, address receiver) public override onlyKyc whenNotPaused nonReentrant returns (uint256) {
if (_checkAndHandleStaleRate()) {
return 0; // Exit early if vault was just paused
}
uint256 maxAssets = maxDeposit(receiver);
if (assets > maxAssets) {
revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
}
uint256 depositFee = calculateDepositFee(assets);
uint256 assetsAfterFee = assets - depositFee;
if (depositFee > 0) {
feesAccruedInAsset += depositFee;
}
uint256 shares = previewDeposit(assetsAfterFee);
_deposit(msg.sender, receiver, assets, shares);
return shares;
}
/**
* @notice Mints exact shares to receiver, calculating and charging required assets
* @dev Overrides ERC4626 mint to handle fees before calling _deposit
* @param shares Amount of shares to mint
* @param receiver Address to receive the shares
* @return assets Total amount of assets deposited (including fees)
*/
function mint(uint256 shares, address receiver) public override onlyKyc whenNotPaused nonReentrant returns (uint256) {
if (_checkAndHandleStaleRate()) {
return 0; // Exit early if vault was just paused
}
uint256 maxShares = maxMint(receiver);
if (shares > maxShares) {
revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
}
(uint256 grossAssets, uint256 fee) = calculateMintFee(shares);
if (fee > 0) {
feesAccruedInAsset += fee;
}
_deposit(msg.sender, receiver, grossAssets, shares);
return grossAssets;
}
/**
* @notice Updates the redemption rate and distributes accumulated fees
* @dev Can only be called by the strategist when the vault is not paused
* @param newRate New redemption rate to set
*/
function update(uint256 newRate) external onlyStrategist whenNotManuallyPaused {
// Validate the new rate
if (newRate == 0) {
revert("Redemption rate cannot be zero");
}
KYCOneWayVaultConfig memory _config = config;
// Check that enough time has passed since last update
if (uint64(block.timestamp) - lastRateUpdateTimestamp < _config.minRateUpdateDelay) {
revert("Minimum rate update delay not passed");
}
// Check that the new rate is within allowed increment/decrement limits
if (newRate > redemptionRate) {
// Rate increase
uint256 maxIncrement = (redemptionRate * config.maxRateIncrementBps) / BASIS_POINTS;
require(newRate - redemptionRate <= maxIncrement, "Rate increase exceeds maximum allowed increment");
} else if (newRate < redemptionRate) {
// Rate decrease
uint256 maxDecrement = (redemptionRate * config.maxRateDecrementBps) / BASIS_POINTS;
require(redemptionRate - newRate <= maxDecrement, "Rate decrease exceeds maximum allowed decrement");
}
// Distribute accumulated fees
_distributeFees(_config.feeDistribution);
// Update state
redemptionRate = newRate;
// Update last update timestamp
lastRateUpdateTimestamp = uint64(block.timestamp);
emit RateUpdated(newRate);
}
/**
* @notice Pauses the vault, preventing deposits and withdrawals
* @dev Can be called by owner or strategist, but only owner can unpause if paused by owner
*/
function pause() external onlyOwnerOrStrategist {
// Check if vault is already paused
if (vaultState.paused) {
revert("Vault is already paused");
}
VaultState memory _vaultState;
if (msg.sender == owner()) {
_vaultState.pausedByOwner = true;
} else {
_vaultState.pausedByOwner = false;
}
_vaultState.paused = true;
vaultState = _vaultState;
emit PausedStateChanged(true);
}
/**
* @notice Unpauses the vault, allowing deposits and withdrawals
* @dev If paused by owner or due to a stale rate, only owner can unpause; otherwise either owner or strategist can unpause
*/
function unpause() external onlyOwnerOrStrategist {
VaultState memory _vaultState = vaultState;
// Check if vault is paused by owner or due to stale rate and the sender is the owner
if (_vaultState.pausedByOwner && msg.sender != owner()) {
revert("Only owner can unpause if paused by owner");
}
if (_vaultState.pausedByStaleRate && msg.sender != owner()) {
revert("Only owner can unpause if paused by stale rate");
}
if (
_vaultState.pausedByStaleRate
&& uint64(block.timestamp) - lastRateUpdateTimestamp > config.maxRateUpdateDelay
) {
revert("Cannot unpause while rate is stale");
}
delete vaultState;
emit PausedStateChanged(false);
}
/**
* @notice Calculates the gross assets needed and fee for minting shares
* @param shares Amount of shares to mint
* @return grossAssets Total assets needed including fee
* @return fee Fee amount in assets
*/
function calculateMintFee(uint256 shares) public view returns (uint256, uint256) {
// Calculate base assets needed for shares
uint256 baseAssets = previewMint(shares);
// Calculate gross assets required including fee
uint256 feeBps = config.depositFeeBps;
if (feeBps == 0) {
return (baseAssets, 0);
}
// grossAssets = baseAssets / (1 - feeRate)
// This formula ensures that after the fee is deducted, exactly baseAssets remain
uint256 grossAssets = baseAssets.mulDiv(BASIS_POINTS, BASIS_POINTS - feeBps, Math.Rounding.Ceil);
uint256 fee = grossAssets - baseAssets;
return (grossAssets, fee);
}
/**
* @notice Calculates the fee amount to be charged for a deposit
* @dev Uses basis points (BPS) for fee calculation where 1 BPS = 0.01%
* The fee is rounded up to ensure the protocol doesn't lose dust amounts
* If the deposit fee BPS is set to 0, returns 0 to optimize gas
* @param assets The amount of assets being deposited
* @return The fee amount in the same decimals as the asset
*/
function calculateDepositFee(uint256 assets) public view returns (uint256) {
uint32 feeBps = config.depositFeeBps;
if (feeBps == 0) return 0;
uint256 fee = assets.mulDiv(feeBps, BASIS_POINTS, Math.Rounding.Ceil);
return fee;
}
/**
* @notice Calculates the withdrawal fee for a given amount of assets
* @dev Uses basis points (BPS) for fee calculation where 1 BPS = 0.01%
* The fee is rounded up to ensure the protocol doesn't lose dust amounts
* If the withdraw rate BPS is set to 0, returns 0 to optimize gas
* @param assets The amount of assets being withdrawn
* @return The withdrawal fee amount in the same decimals as the asset
*/
function calculateWithdrawalFee(uint256 assets) public view returns (uint256) {
uint32 feeBps = config.withdrawFeeBps;
if (feeBps == 0) return 0;
uint256 fee = assets.mulDiv(feeBps, BASIS_POINTS, Math.Rounding.Ceil);
return fee;
}
/**
* @notice Creates a withdrawal request for assets to be processed on destination domain
* @dev Assets are calculated from shares based on current redemption rate
* @param assets Amount of assets to withdraw (gross amount including fee)
* @param receiver Address to receive the withdrawn assets on the destination domain (as string)
* @param owner Address that owns the shares
*/
function withdraw(uint256 assets, string calldata receiver, address owner) external nonReentrant whenNotPaused {
if (_checkAndHandleStaleRate()) {
return; // Exit early if vault was just paused
}
_validateWithdrawParams(owner, receiver, assets);
// Check if assets exceed max withdraw amount
uint256 maxAssets = maxWithdraw(owner);
if (assets > maxAssets) {
revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
}
// Calculate withdrawal fee
uint256 withdrawalFee = calculateWithdrawalFee(assets);
// Calculate net assets after fee
uint256 netAssets = assets - withdrawalFee;
// Calculate shares to burn based on FULL asset amount (including fee)
uint256 sharesToBurn = previewWithdraw(assets);
// Calculate shares for request based on NET assets (what will be processed)
uint256 postFeeShares = previewWithdraw(netAssets);
// Track fee for later distribution
if (withdrawalFee > 0) {
feesAccruedInAsset += withdrawalFee;
}
_withdraw(sharesToBurn, postFeeShares, receiver, owner);
}
/**
* @notice Creates a redemption request for shares to be processed on destination domain
* @param shares Amount of shares to redeem (gross amount)
* @param receiver Address to receive the redeemed assets on destination domain (as string)
* @param owner Address that owns the shares
*/
function redeem(uint256 shares, string calldata receiver, address owner) external nonReentrant whenNotPaused {
if (_checkAndHandleStaleRate()) {
return; // Exit early if vault was just paused
}
_validateWithdrawParams(owner, receiver, shares);
// Check if shares exceed max redeem amount
uint256 maxShares = maxRedeem(owner);
if (shares > maxShares) {
revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
}
// Calculate gross assets from shares
uint256 grossAssets = _convertToAssets(shares, Math.Rounding.Floor);
// Calculate withdrawal fee
uint256 withdrawalFee = calculateWithdrawalFee(grossAssets);
// Calculate net assets after fee
uint256 netAssets = grossAssets - withdrawalFee;
// Calculate shares for request based on net assets (what will be processed)
uint256 postFeeShares = _convertToShares(netAssets, Math.Rounding.Ceil);
// Track fee for later distribution
if (withdrawalFee > 0) {
feesAccruedInAsset += withdrawalFee;
}
// Burn the full shares amount specified by user, store net shares in request
_withdraw(shares, postFeeShares, receiver, owner);
}
/**
* @dev Internal function to handle withdrawal/redemption request creation
* @param sharesToBurn Amount of shares to burn from user's balance
* @param postFeeShares Amount of shares to store in withdrawal request (net after fees)
* @param receiver Address to receive the assets on the destination domain (as string)
* @param owner Address that owns the shares
*/
function _withdraw(uint256 sharesToBurn, uint256 postFeeShares, string calldata receiver, address owner) internal {
// Burn shares first (CEI pattern - Checks, Effects, Interactions)
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, sharesToBurn);
}
_burn(owner, sharesToBurn);
WithdrawRequest memory request = WithdrawRequest({
id: currentWithdrawRequestId,
owner: owner,
sharesAmount: postFeeShares, // Net shares that will be processed
redemptionRate: redemptionRate,
receiver: receiver
});
// Store the request
withdrawRequests[currentWithdrawRequestId] = request;
// Emit the event with the shares post fee that are being withdrawn
emit WithdrawRequested(currentWithdrawRequestId, owner, receiver, postFeeShares);
// Increment the request ID for the next request
currentWithdrawRequestId++;
}
/**
* @dev Internal function to handle deposit implementation
* @param caller Address initiating the deposit
* @param receiver Address to receive the minted shares
* @param assets Amount of assets being deposited
* @param shares Amount of shares to mint
*/
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
// Transfer assets to the deposit account (external contract)
SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(config.depositAccount), assets);
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
}
/**
* @dev Converts shares to assets using current redemption rate
* @param shares Amount of shares to convert
* @param rounding Rounding direction (up or down)
* @return Equivalent amount of assets
*/
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) {
return shares.mulDiv(redemptionRate, ONE_SHARE, rounding);
}
/**
* @dev Converts assets to shares using current redemption rate
* @param assets Amount of assets to convert
* @param rounding Rounding direction (up or down)
* @return Equivalent amount of shares
*/
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
return assets.mulDiv(ONE_SHARE, redemptionRate, rounding);
}
/**
* @dev Distributes accumulated fees between strategist and platform
* @param feeDistribution Fee distribution configuration
*/
function _distributeFees(FeeDistributionConfig memory feeDistribution) internal {
uint256 _feesAccruedInAsset = feesAccruedInAsset;
if (_feesAccruedInAsset == 0) return;
// Calculate fee shares for strategist
uint256 strategistAssets =
_feesAccruedInAsset.mulDiv(feeDistribution.strategistRatioBps, BASIS_POINTS, Math.Rounding.Floor);
// Calculate platform's share as the remainder
uint256 platformAssets = _feesAccruedInAsset - strategistAssets;
// Convert assets to shares
uint256 strategistShares = _convertToShares(strategistAssets, Math.Rounding.Floor);
uint256 platformShares = _convertToShares(platformAssets, Math.Rounding.Floor);
// Reset fees accrued
feesAccruedInAsset = 0;
// Mint shares to respective accounts
if (strategistShares > 0) {
_mint(feeDistribution.strategistAccount, strategistShares);
}
if (platformShares > 0) {
_mint(feeDistribution.platformAccount, platformShares);
}
emit FeesDistributed(
feeDistribution.strategistAccount, feeDistribution.platformAccount, strategistShares, platformShares
);
}
/**
* @dev Internal function to validate common withdraw/redeem parameters
* @param owner Address that owns the shares
* @param receiver Address to receive the assets on the destination domain (as string)
* @param amount Amount of shares/assets to withdraw
*/
function _validateWithdrawParams(address owner, string calldata receiver, uint256 amount) internal pure {
if (owner == address(0)) revert("Owner of shares cannot be zero address");
if (bytes(receiver).length == 0) revert("Receiver cannot be empty");
if (amount == 0) revert("Amount to withdraw cannot be zero");
}
/**
* @dev Internal function that will pause the vault if the redemption rate has been stale for too long
* @return True if the vault was paused due to stale rate, false otherwise
*/
function _checkAndHandleStaleRate() internal returns (bool) {
// Check if the last update was too long ago
if (uint64(block.timestamp) - lastRateUpdateTimestamp > config.maxRateUpdateDelay) {
// Pause the vault due to stale rate
vaultState.paused = true;
vaultState.pausedByStaleRate = true;
emit PausedStateChanged(true);
emit StaleRatePaused(lastRateUpdateTimestamp, uint64(block.timestamp));
return true;
}
return false;
}
/**
* @notice Fallback function that reverts all calls to non-existent functions
* @dev Called when no other function matches the function signature
*/
fallback() external {
revert("Function not found");
}
}
"
},
"dependencies/@openzeppelin-contracts-upgradeable-5.2.0/token/ERC20/extensions/ERC4626Upgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC4626.sol)
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ERC20Upgradeable} from "../ERC20Upgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Initializable} from "../../../proxy/utils/Initializable.sol";
/**
* @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in
* https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].
*
* This extension allows the minting and burning of "shares" (represented using the ERC-20 inheritance) in exchange for
* underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
* the ERC-20 standard. Any additional extensions included along it would affect the "shares" token represented by this
* contract and not the "assets" token which is an independent contract.
*
* [CAUTION]
* ====
* In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning
* with a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation
* attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial
* deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may
* similarly be affected by slippage. Users can protect against this attack as well as unexpected slippage in general by
* verifying the amount received is as expected, using a wrapper that performs these checks such as
* https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router].
*
* Since v4.9, this implementation introduces configurable virtual assets and shares to help developers mitigate that risk.
* The `_decimalsOffset()` corresponds to an offset in the decimal representation between the underlying asset's decimals
* and the vault decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which
* itself determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default
* offset (0) makes it non-profitable even if an attacker is able to capture value from multiple user deposits, as a result
* of the value being captured by the virtual shares (out of the attacker's donation) matching the attacker's expected gains.
* With a larger offset, the attack becomes orders of magnitude more expensive than it is profitable. More details about the
* underlying math can be found xref:erc4626.adoc#inflation-attack[here].
*
* The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued
* to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets
* will cause the first user to exit to experience reduced losses in detriment to the last users that will experience
* bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the
* `_convertToShares` and `_convertToAssets` functions.
*
* To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide].
* ====
*/
abstract contract ERC4626Upgradeable is Initializable, ERC20Upgradeable, IERC4626 {
using Math for uint256;
/// @custom:storage-location erc7201:openzeppelin.storage.ERC4626
struct ERC4626Storage {
IERC20 _asset;
uint8 _underlyingDecimals;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC4626")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC4626StorageLocation = 0x0773e532dfede91f04b12a73d3d2acd361424f41f76b4fb79f090161e36b4e00;
function _getERC4626Storage() private pure returns (ERC4626Storage storage $) {
assembly {
$.slot := ERC4626StorageLocation
}
}
/**
* @dev Attempted to deposit more assets than the max amount for `receiver`.
*/
error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max);
/**
* @dev Attempted to mint more shares than the max amount for `receiver`.
*/
error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max);
/**
* @dev Attempted to withdraw more assets than the max amount for `receiver`.
*/
error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max);
/**
* @dev Attempted to redeem more shares than the max amount for `receiver`.
*/
error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max);
/**
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777).
*/
function __ERC4626_init(IERC20 asset_) internal onlyInitializing {
__ERC4626_init_unchained(asset_);
}
function __ERC4626_init_unchained(IERC20 asset_) internal onlyInitializing {
ERC4626Storage storage $ = _getERC4626Storage();
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
$._underlyingDecimals = success ? assetDecimals : 18;
$._asset = asset_;
}
/**
* @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
*/
function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool ok, uint8 assetDecimals) {
(bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
abi.encodeCall(IERC20Metadata.decimals, ())
);
if (success && encodedDecimals.length >= 32) {
uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
if (returnedDecimals <= type(uint8).max) {
return (true, uint8(returnedDecimals));
}
}
return (false, 0);
}
/**
* @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
* "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
* asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.
*
* See {IERC20Metadata-decimals}.
*/
function decimals() public view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) {
ERC4626Storage storage $ = _getERC4626Storage();
return $._underlyingDecimals + _decimalsOffset();
}
/** @dev See {IERC4626-asset}. */
function asset() public view virtual returns (address) {
ERC4626Storage storage $ = _getERC4626Storage();
return address($._asset);
}
/** @dev See {IERC4626-totalAssets}. */
function totalAssets() public view virtual returns (uint256) {
ERC4626Storage storage $ = _getERC4626Storage();
return $._asset.balanceOf(address(this));
}
/** @dev See {IERC4626-convertToShares}. */
function convertToShares(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}
/** @dev See {IERC4626-convertToAssets}. */
function convertToAssets(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}
/** @dev See {IERC4626-maxDeposit}. */
function maxDeposit(address) public view virtual returns (uint256) {
return type(uint256).max;
}
/** @dev See {IERC4626-maxMint}. */
function maxMint(address) public view virtual returns (uint256) {
return type(uint256).max;
}
/** @dev See {IERC4626-maxWithdraw}. */
function maxWithdraw(address owner) public view virtual returns (uint256) {
return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
}
/** @dev See {IERC4626-maxRedeem}. */
function maxRedeem(address owner) public view virtual returns (uint256) {
return balanceOf(owner);
}
/** @dev See {IERC4626-previewDeposit}. */
function previewDeposit(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}
/** @dev See {IERC4626-previewMint}. */
function previewMint(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Ceil);
}
/** @dev See {IERC4626-previewWithdraw}. */
function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Ceil);
}
/** @dev See {IERC4626-previewRedeem}. */
function previewRedeem(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}
/** @dev See {IERC4626-deposit}. */
function deposit(uint256 assets, address receiver) public virtual returns (uint256) {
uint256 maxAssets = maxDeposit(receiver);
if (assets > maxAssets) {
revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
}
uint256 shares = previewDeposit(assets);
_deposit(_msgSender(), receiver, assets, shares);
return shares;
}
/** @dev See {IERC4626-mint}. */
function mint(uint256 shares, address receiver) public virtual returns (uint256) {
uint256 maxShares = maxMint(receiver);
if (shares > maxShares) {
revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
}
uint256 assets = previewMint(shares);
_deposit(_msgSender(), receiver, assets, shares);
return assets;
}
/** @dev See {IERC4626-withdraw}. */
function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {
uint256 maxAssets = maxWithdraw(owner);
if (assets > maxAssets) {
revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
}
uint256 shares = previewWithdraw(assets);
_withdraw(_msgSender(), receiver, owner, assets, shares);
return shares;
}
/** @dev See {IERC4626-redeem}. */
function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) {
uint256 maxShares = maxRedeem(owner);
if (shares > maxShares) {
revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
}
uint256 assets = previewRedeem(shares);
_withdraw(_msgSender(), receiver, owner, assets, shares);
return assets;
}
/**
* @dev Internal conversion function (from assets to shares) with support for rounding direction.
*/
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}
/**
* @dev Internal conversion function (from shares to assets) with support for rounding direction.
*/
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {
return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding);
}
/**
* @dev Deposit/mint common workflow.
*/
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual {
ERC4626Storage storage $ = _getERC4626Storage();
// If _asset is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
// assets are transferred and before the shares are minted, which is a valid state.
// slither-disable-next-line reentrancy-no-eth
SafeERC20.safeTransferFrom($._asset, caller, address(this), assets);
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
}
/**
* @dev Withdraw/redeem common workflow.
*/
function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual {
ERC4626Storage storage $ = _getERC4626Storage();
if (caller != owner) {
_spendAllowance(owner, caller, shares);
}
// If _asset is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
// `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
// shares are burned and after the assets are transferred, which is a valid state.
_burn(owner, shares);
SafeERC20.safeTransfer($._asset, receiver, assets);
emit Withdraw(caller, receiver, owner, assets, shares);
}
function _decimalsOffset() internal view virtual returns (uint8) {
return 0;
}
}
"
},
"dependencies/@openzeppelin-contracts-upgradeable-5.2.0/utils/ReentrancyGuardUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)
pragma solidity ^0.8.20;
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
* consider using {ReentrancyGuardTransient} instead.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuardUpgradeable is Initializable {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
/// @custom:storage-location erc7201:openzeppelin.storage.ReentrancyGuard
struct ReentrancyGuardStorage {
uint256 _status;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ReentrancyGuardStorageLocation = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
function _getReentrancyGuardStorage() private pure returns (ReentrancyGuardStorage storage $) {
assembly {
$.slot := ReentrancyGuardStorageLocation
}
}
/**
* @dev Unauthorized reentrant call.
*/
error ReentrancyGuardReentrantCall();
function __ReentrancyGuard_init() internal onlyInitializing {
__ReentrancyGuard_init_unchained();
}
function __ReentrancyGuard_init_unchained() internal onlyInitializing {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
$._status = NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
// On the first call to nonReentrant, _status will be NOT_ENTERED
if ($._status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
$._status = ENTERED;
}
function _nonReentrantAfter() private {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
$._status = NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
return $._status == ENTERED;
}
}
"
},
"src/BaseAccount.sol": {
"content": "// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;
import {Account} from "./Account.sol";
/**
* @title BaseAccount
* @dev Basic implementation of Account contract with no additional functionality
*/
contract BaseAccount is Account {
constructor(address _owner, address[] memory _libraries) Account(_owner, _libraries) {}
}
"
},
"dependencies/@openzeppelin-contracts-upgradeable-5.2.0/access/OwnableUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable {
/// @custom:storage-location erc7201:openzeppelin.storage.Ownable
struct OwnableStorage {
address _owner;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;
function _getOwnableStorage() private pure returns (OwnableStorage storage $) {
assembly {
$.slot := OwnableStorageLocation
}
}
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
function __Ownable_init(address initialOwner) internal onlyInitializing {
__Ownable_init_unchained(initialOwner);
}
function __Ownable_init_unchained(address initialOwner) internal onlyInitializing {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
OwnableStorage storage $ = _getOwnableStorage();
return $._owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
OwnableStorage storage $ = _getOwnableStorage();
address oldOwner = $._owner;
$._owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"dependencies/@openzeppelin-contracts-upgradeable-5.2.0/proxy/utils/UUPSUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (proxy/utils/UUPSUpgradeable.sol)
pragma solidity ^0.8.22;
import {IERC1822Proxiable} from "@openzeppelin/contracts/interfaces/draft-IERC1822.sol";
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
import {Initializable} from "./Initializable.sol";
/**
* @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an
* {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy.
*
* A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is
* reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing
* `UUPSUpgradeable` with a custom implementation of upgrades.
*
* The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism.
*/
abstract contract UUPSUpgradeable is Initializable, IERC1822Proxiable {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address private immutable __self = address(this);
/**
* @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgradeTo(address)`
* and `upgradeToAndCall(address,bytes)` are present, and `upgradeTo` must be used if no function should be called,
* while `upgradeToAndCall` will invoke the `receive` function if the second argument is the empty byte string.
* If the getter returns `"5.0.0"`, only `upgradeToAndCall(address,bytes)` is present, and the second argument must
* be the empty byte string if no function should be called, making it impossible to invoke the `receive` function
* during an upgrade.
*/
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
/**
* @dev The call is from an unauthorized context.
*/
error UUPSUnauthorizedCallContext();
/**
* @dev The storage `slot` is unsupported as a UUID.
*/
error UUPSUnsupportedProxiableUUID(bytes32 slot);
/**
* @dev Check that the execution is being performed through a delegatecall call and that the execution context is
* a proxy contract with an implementation (as defined in ERC-1967) pointing to self. This should only be the case
* for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a
* function through ERC-1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to
* fail.
*/
modifier onlyProxy() {
_checkProxy();
_;
}
/**
* @dev Check that the execution is not being performed through a delegate call. This allows a function to be
* callable on the implementing contract but not through proxies.
*/
modifier notDelegated() {
_checkNotDelegated();
_;
}
function __UUPSUpgradeable_init() internal onlyInitializing {
}
function __UUPSUpgradeable_init_unchained() internal onlyInitializing {
}
/**
* @dev Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the
* implementation. It is used to validate the implementation's compatibility when performing an upgrade.
*
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
* function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.
*/
function proxiableUUID() external view virtual notDelegated returns (bytes32) {
return ERC1967Utils.IMPLEMENTATION_SLOT;
}
/**
* @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call
* encoded in `data`.
*
* Calls {_authorizeUpgrade}.
*
* Emits an {Upgraded} event.
*
* @custom:oz-upgrades-unsafe-allow-reachable delegatecall
*/
function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, data);
}
/**
* @dev Reverts if the execution is not performed via delegatecall or the execution
* context is not of a proxy with an ERC-1967 compliant implementation pointing to self.
* See {_onlyProxy}.
*/
function _checkProxy() internal view virtual {
if (
address(this) == __self || // Must be called through delegatecall
ERC1967Utils.getImplementation() != __self // Must be called through an active proxy
) {
revert UUPSUnauthorizedCallContext();
}
}
/**
* @dev Reverts if the execution is performed via delegatecall.
* See {notDelegated}.
*/
function _checkNotDelegated() internal view virtual {
if (address(this) != __self) {
// Must not be called through delegatecall
revert UUPSUnauthorizedCallContext();
}
}
/**
* @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
* {upg
Submitted on: 2025-09-17 20:40:38
Comments
Log in to comment.
No comments yet.