USUALx

High-Level Overview

The UsualX contract is an upgradeable ERC4626-compliant yield-bearing vault. It extends the YieldBearingVaultUpgradeable contract, incorporating features such as whitelisting, blacklisting, withdrawal fees, and yield distribution linearly over a predefined yield period. The contract leverages OpenZeppelin's upgradeable contracts for enhanced security and flexibility, including pausability and reentrancy protection. It also implements EIP712 for secure off-chain signing capabilities.

The primary objective of Usualx is to provide a secure, controllable environment for yield generation and distribution, while maintaining strict control over who can interact with the contract. This design allows for potential regulatory compliance and risk management in decentralized finance applications.

Contract Summary

The contract provides the following main functions:

  • initialize: Sets up the contract with customizable parameters.

  • initializeV1: Sets up the contract with new customizable parameters.

  • pause / unpause: Controls the operational state of the contract.

  • blacklist / unBlacklist: Manages addresses prohibited from interacting with the contract.

  • whitelist / unWhitelist: Controls addresses permitted to transfer tokens.

  • transfer / transferFrom: Overridden to enforce whitelist restrictions.

  • startYieldDistribution: Initiates a new yield accrual period with specified parameters.

  • withdraw / redeem: Handles asset withdrawals and share redemptions, incorporating withdrawal fees.

  • previewWithdraw / previewRedeem: Simulates withdrawal and redemption operations for users. The contract uses a separate storage structure (UsualXStorageV0) to store state variables for UsualX implementation.

  • sweepFees: Sweeps accumulated fees to a collector address.

  • setBurnRatio: Updates the burn ratio basis points (bps) in the contract.

Inherited Contracts

  • YieldBearingVaultUpgradeable: Provides core yield accrual and distribution mechanisms.

  • PausableUpgradeable: Enables emergency halt of contract operations.

  • ReentrancyGuardUpgradeable: Prevents reentrancy attacks in critical functions.

  • EIP712Upgradeable: Implements EIP712 for secure off-chain message signing.

Functionality Breakdown

  1. Access Control and Security:

    • Utilizes a registry contract for role-based access control.

    • Implements blacklist to prevent specific addresses from interacting with the contract.

  • Enforces whitelist for token transfers, allowing only approved addresses to transfer tokens at launch but anyone not blacklisted to mint or interact with the vault.

  1. Yield Management:

    • Allows admin-controlled yield distribution periods.

    • Accrues yield over time based on configurable parameters.

    • Integrates yield accrual with deposit and withdrawal operations.

  2. Asset Management:

    • Implements ERC4626 standard for standardized vault interactions.

    • Handles deposits, withdrawals, and redemptions with consideration for accrued yield.

    • Applies withdrawal fees, potentially for protocol revenue or discouraging rapid withdrawals.

  3. Upgradability and Pause Mechanism:

    • Utilizes OpenZeppelin's upgradeable contract pattern for future improvements.

    • Includes pause functionality for emergency situations.

Security Analysis

Method: initialize

Initializes the vault, token, yield module, EIP712 domain, registry contract and access control, setting up the vault's initial state.

 1  function initialize(
 2      address _registryContract,
 3      uint256 _withdrawFeeBps,
 4      string memory _name,
 5      string memory _symbol,
 6      IERC20 _underlying,
 7      uint256 _maxPeriodLength
 8  ) external initializer {
 9      __YieldBearingVault_init(_maxPeriodLength);
10      __ERC4626_init(_underlying);
11      __ERC20_init(_name, _symbol);
12      __Pausable_init_unchained();
13      __ReentrancyGuard_init();
14      __EIP712_init_unchained(_name, "1");
15  
16      if (_withdrawFeeBps > MAX_WITHDRAW_FEE) {
17          revert AmountTooBig();
18      }
19  
20      if (_registryContract == address(0)) {
21          revert NullContract();
22      }
23  
24      UsualXStorageV0 storage $ = _usualXStorageV0();
25      $.withdrawFeeBps = _withdrawFeeBps;
26      $.registryContract = IRegistryContract(_registryContract);
27      $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
28  }

1-8. Set the registry contract, withdrawal fee in BPS, token name and symbol for the vault, underlying asset, and the max yield period length.

9-14. Initializes inherited contracts, with initializer parameters.

16-18. Validates withdrawal fee is below 25% preventing excessive fees that could harm users.

20-22. Ensures a valid registry contract, reverts if zero address.

24-26. Sets up contract storage with validated parameter.

  1. Points at the access control registry in the registry contract.

Method: initializeV1

Initializes the burn ration in bips and a variable linked to the USUAL token.

1 function initializeV1() public reinitializer(2) {
2    UsualXStorageV0 storage $ = _usualXStorageV0();
3    if (INITIAL_BURN_RATIO_BPS > BASIS_POINT_BASE) {
4        revert AmountTooBig();
5    }
6    $.burnRatioBps = INITIAL_BURN_RATIO_BPS;
7    $.usualToken = IUsual($.registryContract.getContract(CONTRACT_USUAL));
8    emit BurnRatioUpdated($.burnRatioBps);
9 }

1. Uses the reintializer modifier to set the second version of the the initialize function.

2. Load the contract storage.

3-5. Validates INITIAL_BURN_RATIO_BPS constant isns't higher 100% preventing excessive burning fees.

6-7. Sets up contract storage with validated parameter.

8. Emits an event with the new burn ration.

Method: blacklist

Adds an address to the blacklist, preventing it from interacting with the contract.

 1  function blacklist(address account) external {
 2      if (account == address(0)) {
 3          revert NullAddress();
 4      }
 5      UsualXStorageV0 storage $ = _usualXStorageV0();
 6      $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
 7      if ($.isBlacklisted[account]) {
 8          revert SameValue();
 9      }
10      $.isBlacklisted[account] = true;
11  
12      emit Blacklist(account);
13  }
  1. Mark function as external to save gas.

2-4. Prevents blacklisting of zero address, and reverts if trying to pass zero address.

5-6. Utilizes the registry for role-based access control, restricting to admin.

7-9. Reverts if the account is already blacklisted.

  1. Adds the account to the blacklist in UsualXStorageV0.

  2. Emits an event to log the blacklisting action.

Method: _update

Internal hook ensuring that both sender and receiver are not blacklisted before updating the token balances.

 1  function _update(address from, address to, uint256 amount)
 2      internal
 3      override(ERC20Upgradeable)
 4  {
 5      UsualXStorageV0 storage $ = _usualXStorageV0();
 6      if ($.isBlacklisted[from] || $.isBlacklisted[to]) {
 7          revert Blacklisted();
 8      }
 9      super._update(from, to, amount);
10  }

1-4. Internal function overriding the base ERC20Upgradeable implementation.

  1. Retrieves storage pointer for UsualXStorageV0. 6-8. Checks both sender and receiver against blacklist, reverting if either is blacklisted.

  2. Passes through to parent implementation if checks pass.

Method: transfer

Overrides the standard ERC20 transfer function to enforce whitelist restrictions on token transfers when the contract is deployed. This can later be removed via smart contract upgrade.

 1  function transfer(address to, uint256 value)
 2      public
 3      override(ERC20Upgradeable, IERC20)
 4      returns (bool)
 5  {
 6      address owner = _msgSender();
 7      UsualXStorageV0 storage $ = _usualXStorageV0();
 8      if ($.isWhitelisted[owner]) {
 9          _transfer(owner, to, value);
10          return true;
11      }
12      revert NotWhitelisted();
13  }

1-5. Public function overriding ERC20 transfer base implementation.

6-7. Uses _msgSender() for potential meta-transaction support, and retrieve storage pointer.

8-11. Allows whitelisted senders to transfer tokens, otherwise reverts.

  1. Reverts if sender is not whitelisted.

Security considerations:

  • Correctly enforces whitelist for senders, but doesn't check recipient's whitelist status.

  • Consider adding a check for the contract's paused state.

  • The function doesn't emit a custom event for whitelisted transfers, which could aid in monitoring.

Method: startYieldDistribution

Initiates a new yield distribution period with specified parameters wrapping the internal call to add proper access control.

 1  function startYieldDistribution(uint256 yieldAmount, uint256 startTime, uint256 endTime)
 2      external
 3  {
 4      _requireOnlyAdmin();
 5      _startYieldDistribution(yieldAmount, startTime, endTime);
 6  }

1-3. External function for starting a new yield period.

  1. Ensures only admin set on registry access can call this function.

  2. Delegates to internal function for yield distribution logic.

Method: withdraw

Overrides the ERC4626 withdraw function to include withdrawal fees and enforce withdrawal limits, calculates shares internally to avoid another storage fetch from calling previewWithdraw.

 1  function withdraw(uint256 assets, address receiver, address owner)
 2      public
 3      override
 4      returns (uint256 shares)
 5  {
 6      UsualXStorageV0 storage $ = _usualXStorageV0();
 7      YieldDataStorage storage yieldStorage = _getYieldDataStorage();
 8  
 9      uint256 maxAssets = maxWithdraw(owner);
10      if (assets > maxAssets) {
11          revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
12      }
13  
14      uint256 fee = Math.mulDiv(assets, $.withdrawFeeBps, BPS_DENOMINATOR, Math.Rounding.Floor);
15      uint256 assetsWithFee = assets + fee;
16  
17      shares = convertToShares(assetsWithFee);
18  
19      yieldStorage.totalDeposits -= fee;
20  
21      super._withdraw(_msgSender(), receiver, owner, assets, shares);
22  }

1-5. Public function overriding the ERC4626 withdraw function.

6-7. Retrieves storage pointers for UsualXStorageV0 and YieldDataStorage.

9-12. Checks if the withdrawal amount exceeds the maximum allowed, and reverts if so.

14-15. Calculates the withdrawal fee based on the number of assets user wants to withdraw taking the precision into account.

  1. Converts assets to shares, considering the fee.

  2. Deducts the fee from the total deposits in the yield storage.

  3. Calls parent withdrawal function with calculated values.

Method: sweepFees

Sweeps accumulated fees to the specified collector address, optionally burning a portion of the fees based on the burn ratio. Enforces non-reentrancy, role-based access control, and validates input.

1 function sweepFees(address collector) external nonReentrant {
2    if (collector == address(0)) {
3       revert NullAddress();
4    }
5    UsualXStorageV0 storage $ = _usualXStorageV0();
6    $.registryAccess.onlyMatchingRole(FEE_SWEEPER_ROLE);
7    uint256 feesToSweep = $.accumulatedFees;
8    if (feesToSweep == 0) {
9        revert AmountIsZero();
10    }
11    $.accumulatedFees = 0;
12    uint256 burnAmount =
13        Math.mulDiv(feesToSweep, $.burnRatioBps, BASIS_POINT_BASE, Math.Rounding.Floor);
14    uint256 transferAmount = feesToSweep - burnAmount;
15    IUsual usualToken = $.usualToken;
16    if (burnAmount > 0) {
17        usualToken.burn(burnAmount);
18    }
19    if (transferAmount > 0) {
20        usualToken.safeTransfer(collector, transferAmount);
21    }
22    emit FeeSwept(collector, feesToSweep, burnAmount);
23 }

1. A public external function marked as nonReentrant to prevent reentrancy attacks.

2-4. Checks if the collector address is non-zero. If not, reverts with NullAddress().

5-6. Retrieves storage pointer for UsualXStorageV0 and validates that the caller has the FEE_SWEEPER_ROLE permission.

7-9. Reads the accumulated fees from storage and reverts with AmountIsZero() if no fees are available to sweep.

11. Sets the accumulatedFees storage variable to 0.

12-14. Calculates the portion of fees to burn (burnAmount) based on the burnRatioBps, and computes the remaining amount (transferAmount) to be transferred to the collector.

  1. Retrieves the reference to the usualToken used for burning and transferring.

16-18. If burnAmount is greater than 0, burns the calculated amount of fees.

19-21. If transferAmount is greater than 0, safely transfers the fees to the collector address.

  1. Emits a FeeSwept event with the collector address, total fees swept, and amount burned.

Method: setBurnRatio

Updates the burn ratio basis points (bps) in the contract. Enforces role-based access control and ensures the new ratio does not exceed the basis point maximum.

1 function setBurnRatio(uint256 burnRatioBps) external {
2    UsualXStorageV0 storage $ = _usualXStorageV0();
3    $.registryAccess.onlyMatchingRole(BURN_RATIO_UPDATER_ROLE);
4    if (burnRatioBps > BASIS_POINT_BASE) {
5        revert AmountTooBig();
6    }
7    $.burnRatioBps = burnRatioBps;
8    emit BurnRatioUpdated(burnRatioBps);
9 }

1. A public external function to update the burn ratio.

2-3. Retrieves the storage pointer for UsualXStorageV0 and validates that the caller has the BURN_RATIO_UPDATER_ROLE permission.

4-6. Checks if the provided burnRatioBps exceeds the maximum allowable value (BASIS_POINT_BASE, typically 10,000 for 100%). If it does, the function reverts with AmountTooBig().

7. Updates the burnRatioBps storage variable with the new value.

8. Emits a BurnRatioUpdated event with the updated burn ratio value.

Last updated