sUSD0
High-Level Overview
The sUSD0 contract is an upgradeable ERC4626-compliant yield-bearing vault. It extends the YieldBearingVault contract, incorporating features such as 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 sUSD0 is to provide a secure, controllable environment for yield generation and distribution on USD0 deposits, 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, including initial shares minting.
pause / unpause: Controls the operational state of the contract.
blacklist / unBlacklist: Manages addresses prohibited from interacting with the contract.
startYieldDistribution: Initiates a new yield accrual period with specified parameters.
deposit / mint: Handles asset deposits and share minting.
depositWithPermit: Enables gasless deposits using permit functionality.
withdraw / redeem: Handles asset withdrawals and share redemptions, incorporating withdrawal fees that are immediately transferred to the treasury yield contract.
previewWithdraw / previewRedeem: Simulates withdrawal and redemption operations for users.
updateWithdrawFee: Updates the withdrawal fee rate with proper access control.
The contract uses a separate storage structure (SUsd0StorageV0) to store state variables for sUSD0 implementation.
Inherited Contracts
YieldBearingVault: 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
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 pause mechanism for emergency situations.
Uses role-based permissions for administrative functions.
Yield Management:
Allows admin-controlled yield distribution periods via
SUSD0_YIELD_DISTRIBUTOR_ROLE.Accrues yield over time based on configurable parameters.
Integrates yield accrual with deposit and withdrawal operations.
Asset Management:
Implements ERC4626 standard for standardized vault interactions.
Handles deposits, withdrawals, and redemptions with consideration for accrued yield.
Applies withdrawal fees, immediately transferring them to the treasury yield contract for protocol revenue.
Mints initial "dead" shares during initialization to support early yield distribution.
Upgradability and Pause Mechanism:
Utilizes OpenZeppelin's upgradeable contract pattern for future improvements.
Includes pause functionality for emergency situations with separate roles for pausing and unpausing.
Security Analysis
Method: initialize
Initializes the vault, token, yield module, EIP712 domain, registry contract and access control, setting up the vault's initial state.
function initialize(address _registryContract, uint256 _withdrawFeeBps, uint256 initialShares)
external
initializer
{
if (_registryContract == address(0)) {
revert NullContract();
}
SUsd0StorageV0 storage $ = _susd0StorageV0();
address _underlyingToken = IRegistryContract(_registryContract).getContract(CONTRACT_USD0);
__YieldBearingVault_init(_underlyingToken, SUSD0Name, SUSD0Symbol);
__Pausable_init_unchained();
__ReentrancyGuard_init();
__EIP712_init_unchained(SUSD0Name, "1");
if (_withdrawFeeBps > MAX_25_PERCENT_WITHDRAW_FEE) {
revert AmountTooBig();
}
if (initialShares == 0) {
revert AmountIsZero();
}
$.registryAccess = IRegistryAccess(
IRegistryContract(_registryContract).getContract(CONTRACT_REGISTRY_ACCESS)
);
$.withdrawFeeBps = _withdrawFeeBps;
$.usd0Token = IUsd0(_underlyingToken);
$.treasuryYield = IRegistryContract(_registryContract).getContract(CONTRACT_YIELD_TREASURY);
YieldDataStorage storage y = _getYieldDataStorage();
// mint dead shares to the vault
y.totalDeposits = initialShares;
_mint(address(this), initialShares);
emit WithdrawFeeUpdated(_withdrawFeeBps);
}1-3. Set the registry contract, withdrawal fee in BPS, and initial shares to mint.
4-6. Validates registry contract is not zero address, reverts if zero address.
7. Load the contract storage.
8. Retrieves the USD0 token address from the registry contract.
9-12. Initializes inherited contracts, with initializer parameters including vault name and symbol from constants.
14-16. Validates withdrawal fee is below 25% preventing excessive fees that could harm users.
18-20. Validates initialShares is not zero, ensuring vault starts with a valid state.
22-24. Sets up contract storage with registry access retrieved from the registry contract.
25-26. Stores withdrawal fee and USD0 token reference.
27. Retrieves and stores the treasury yield contract address from the registry.
29-31. Initializes yield storage with initial shares and mints them to the contract itself, creating "dead" shares that allow for early yield distribution before user deposits.
33. Emits an event with the new withdrawal fee.
Method: blacklist
Adds an address to the blacklist, preventing it from interacting with the contract.
function blacklist(address account) external {
if (account == address(0)) {
revert NullAddress();
}
SUsd0StorageV0 storage $ = _susd0StorageV0();
$.registryAccess.onlyMatchingRole(BLACKLIST_ROLE);
if ($.isBlacklisted[account]) {
revert SameValue();
}
$.isBlacklisted[account] = true;
emit Blacklist(account);
}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 BLACKLIST_ROLE.
7-9. Reverts if the account is already blacklisted.
10. Adds the account to the blacklist in SUsd0StorageV0.
12. 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.
function _update(address from, address to, uint256 amount)
internal
override(ERC20Upgradeable)
whenNotPaused
{
SUsd0StorageV0 storage $ = _susd0StorageV0();
if ($.isBlacklisted[from] || $.isBlacklisted[to]) {
revert Blacklisted();
}
super._update(from, to, amount);
}1-4. Internal function overriding the base ERC20Upgradeable implementation, includes whenNotPaused modifier to prevent transfers when contract is paused.
5. Retrieves storage pointer for SUsd0StorageV0.
6-8. Checks both sender and receiver against blacklist, reverting if either is blacklisted.
9. Passes through to parent implementation if checks pass.
Method: startYieldDistribution
Initiates a new yield distribution period with specified parameters, wrapping the internal call to add proper access control.
function startYieldDistribution(uint256 yieldAmount, uint256 startTime, uint256 endTime)
external
{
SUsd0StorageV0 storage $ = _susd0StorageV0();
$.registryAccess.onlyMatchingRole(SUSD0_YIELD_DISTRIBUTOR_ROLE);
_startYieldDistribution(yieldAmount, startTime, endTime);
}1-3. External function for starting a new yield period.
4-5. Ensures only addresses with SUSD0_YIELD_DISTRIBUTOR_ROLE set on registry access can call this function.
6. Delegates to internal function for yield distribution logic.
Method: _startYieldDistribution
Internal function to start the yield distribution with validation checks.
function _startYieldDistribution(uint256 yieldAmount, uint256 startTime, uint256 endTime)
internal
override
{
IERC20 _asset = IERC20(asset());
if (yieldAmount == 0) {
revert ZeroYieldAmount();
}
if (startTime < block.timestamp) {
revert StartTimeNotInFuture();
}
if (endTime <= startTime) {
revert EndTimeNotAfterStartTime();
}
YieldDataStorage storage $ = _getYieldDataStorage();
if (startTime < $.periodFinish) {
revert StartTimeBeforePeriodFinish();
}
if (block.timestamp < $.periodFinish) {
revert CurrentTimeBeforePeriodFinish();
}
_updateYield();
uint256 periodDuration = endTime - startTime;
uint256 newYieldRate =
Math.mulDiv(yieldAmount, YIELD_PRECISION, periodDuration, Math.Rounding.Floor);
if (_asset.balanceOf(address(this)) < $.totalDeposits + yieldAmount) {
revert InsufficientAssetsForYield();
}
$.yieldRate = newYieldRate;
$.periodStart = startTime;
$.periodFinish = endTime;
$.lastUpdateTime = startTime;
$.isActive = true;
}1-4. Internal function overriding the base YieldBearingVault implementation.
5. Gets reference to the underlying asset (USD0).
7-9. Validates yield amount is not zero.
10-12. Validates start time is in the future.
13-15. Validates end time is after start time.
17. Retrieves yield data storage.
19-21. Validates new period starts after the current period finishes.
22-24. Validates current time is after the current period finishes (prevents overlapping periods).
25. Updates yield state to account for any accrued yield before starting new period.
27-28. Calculates the yield rate per second using floor rounding to ensure conservative estimates.
30-32. Validates contract has sufficient assets (deposits + yield amount) to support the yield distribution.
34-37. Sets the new yield period parameters and activates yield distribution.
Method: depositWithPermit
Enables users to deposit assets using permit functionality, allowing gasless approvals.
function depositWithPermit(
uint256 assets,
address receiver,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public nonReentrant whenNotPaused returns (uint256 shares) {
try IERC20Permit(asset()).permit(msg.sender, address(this), assets, deadline, v, r, s) {} // solhint-disable-line no-empty-blocks
catch {} // solhint-disable-line no-empty-blocks
return deposit(assets, receiver);
}1-8. Public function marked as nonReentrant and whenNotPaused to prevent reentrancy and ensure contract is active.
9. Attempts to execute permit signature to approve the contract to spend user's assets. If permit fails (e.g., token doesn't support permit), continues execution assuming approval already exists.
11. Calls the standard deposit function with the approved assets.
Method: withdraw
Overrides the ERC4626 withdraw function to include withdrawal fees and enforce withdrawal limits. Fees are immediately transferred to the treasury yield contract.
function withdraw(uint256 assets, address receiver, address owner)
public
override(ERC4626Upgradeable, ISUsd0)
nonReentrant
whenNotPaused
returns (uint256 shares)
{
SUsd0StorageV0 storage $ = _susd0StorageV0();
YieldDataStorage storage yieldStorage = _getYieldDataStorage();
// Check withdrawal limit
uint256 maxAssets = maxWithdraw(owner);
// maxAssets take into account the fee
if (assets > maxAssets) {
revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
}
shares = previewWithdraw(assets);
// we need to add the fee on the assets to know exactly the shares to burn
// assets is 100% - fee% of the total assets
uint256 fee = Math.mulDiv(
assets, $.withdrawFeeBps, BASIS_POINT_BASE - $.withdrawFeeBps, Math.Rounding.Ceil
);
// Perform withdrawal (exact assets to receiver)
super._withdraw(_msgSender(), receiver, owner, assets, shares);
// take the fee
yieldStorage.totalDeposits -= fee;
$.usd0Token.safeTransfer($.treasuryYield, fee);
}1-7. Public function overriding the ERC4626 withdraw function, protected by nonReentrant and whenNotPaused modifiers.
8-9. Retrieves storage pointers for SUsd0StorageV0 and YieldDataStorage.
10-14. Checks if the withdrawal amount exceeds the maximum allowed (accounting for fees), and reverts if so.
16. Calculates shares needed using previewWithdraw which accounts for fees.
17-20. Calculates the withdrawal fee based on the assets being withdrawn. Uses ceiling rounding to ensure fees are not undercharged. The formula accounts for the fee being taken from assets beyond what the user receives.
22. Calls parent withdrawal function with calculated values, which handles share burning and asset transfer to receiver.
23-24. Deducts the fee from total deposits and immediately transfers the fee to the treasury yield contract.
Method: redeem
Overrides the ERC4626 redeem function to include redemption fees and enforce redemption limits. Fees are immediately transferred to the treasury yield contract.
function redeem(uint256 shares, address receiver, address owner)
public
override(ERC4626Upgradeable, ISUsd0)
nonReentrant
whenNotPaused
returns (uint256 assets)
{
SUsd0StorageV0 storage $ = _susd0StorageV0();
YieldDataStorage storage yieldStorage = _getYieldDataStorage();
// Check redemption limit
uint256 maxShares = maxRedeem(owner);
if (shares > maxShares) {
revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
}
// Calculate assets after fee
assets = previewRedeem(shares);
uint256 assetsWithFee = convertToAssets(shares);
uint256 fee = assetsWithFee - assets;
// Perform redemption
super._withdraw(_msgSender(), receiver, owner, assets, shares);
// take the fee
yieldStorage.totalDeposits -= fee;
$.usd0Token.safeTransfer($.treasuryYield, fee);
}1-7. Public function overriding the ERC4626 redeem function, protected by nonReentrant and whenNotPaused modifiers.
8-9. Retrieves storage pointers for SUsd0StorageV0 and YieldDataStorage.
11-14. Checks if the redemption amount exceeds the maximum allowed, and reverts if so.
16-18. Calculates assets the user will receive (after fee) and the total assets represented by the shares (before fee). The difference is the fee amount.
20. Calls parent withdrawal function which handles share burning and asset transfer to receiver.
22-23. Deducts the fee from total deposits and immediately transfers the fee to the treasury yield contract.
Method: previewWithdraw
Calculates the number of shares required to withdraw a given amount of assets, accounting for withdrawal fees.
function previewWithdraw(uint256 assets)
public
view
override(ERC4626Upgradeable, ISUsd0)
returns (uint256 shares)
{
SUsd0StorageV0 storage $ = _susd0StorageV0();
// Calculate the fee based on the equivalent assets of these shares
uint256 fee = Math.mulDiv(
assets, $.withdrawFeeBps, BASIS_POINT_BASE - $.withdrawFeeBps, Math.Rounding.Ceil
);
// Calculate total assets needed, including fee
uint256 assetsWithFee = assets + fee;
// Convert the total assets (including fee) to shares
shares = _convertToShares(assetsWithFee, Math.Rounding.Ceil);
}1-5. Public view function overriding ERC4626 previewWithdraw.
6. Retrieves storage pointer for SUsd0StorageV0.
7-10. Calculates the fee amount based on the assets being withdrawn. Uses ceiling rounding to ensure conservative estimates. The formula accounts for the fee being a percentage of the total assets needed.
11-12. Calculates total assets needed including the fee.
13. Converts the total assets (including fee) to shares using ceiling rounding.
Method: previewRedeem
Calculates the amount of assets that would be received for redeeming a given number of shares, accounting for redemption fees.
function previewRedeem(uint256 shares)
public
view
override(ERC4626Upgradeable, ISUsd0)
returns (uint256 assets)
{
SUsd0StorageV0 storage $ = _susd0StorageV0();
// Calculate the raw amount of assets for the given shares
uint256 assetsWithFee = convertToAssets(shares);
// Calculates the fee part of an amount `assets` that already includes fees.
uint256 fee =
Math.mulDiv(assetsWithFee, $.withdrawFeeBps, BASIS_POINT_BASE, Math.Rounding.Ceil);
assets = assetsWithFee - fee;
}1-5. Public view function overriding ERC4626 previewRedeem.
6. Retrieves storage pointer for SUsd0StorageV0.
7-8. Calculates the raw amount of assets represented by the shares (before fee deduction).
10-11. Calculates the fee portion using ceiling rounding to ensure conservative estimates.
12. Subtracts the fee from the total assets to get the net assets the user will receive.
Method: updateWithdrawFee
Updates the withdrawal fee rate with proper access control and validation.
function updateWithdrawFee(uint256 newWithdrawFeeBps) external {
SUsd0StorageV0 storage $ = _susd0StorageV0();
$.registryAccess.onlyMatchingRole(SUSD0_WITHDRAW_FEE_UPDATER_ROLE);
if (newWithdrawFeeBps > MAX_25_PERCENT_WITHDRAW_FEE) {
revert AmountTooBig();
}
if (newWithdrawFeeBps == $.withdrawFeeBps) {
revert SameValue();
}
$.withdrawFeeBps = newWithdrawFeeBps;
emit WithdrawFeeUpdated(newWithdrawFeeBps);
}1. External function for updating withdrawal fee.
2-3. Retrieves storage pointer and validates caller has SUSD0_WITHDRAW_FEE_UPDATER_ROLE.
5-7. Validates new fee is below 25% maximum, preventing excessive fees.
9-11. Validates new fee is different from current fee, preventing unnecessary updates.
13. Updates the withdrawal fee in storage.
14. Emits an event to log the fee update.
Last updated
Was this helpful?