sEUR0
High-Level Overview
The sEUR0 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 sEUR0 is to provide a secure, controllable environment for yield generation and distribution on EUR0 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 (SEur0StorageV0) to store state variables for sEUR0 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 SEUR0_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();
}
SEur0StorageV0 storage $ = _seur0StorageV0();
address _underlyingToken = IRegistryContract(_registryContract).getContract(CONTRACT_EUR0);
__YieldBearingVault_init(_underlyingToken, SEUR0Name, SEUR0Symbol);
__Pausable_init_unchained();
__ReentrancyGuard_init();
__EIP712_init_unchained(SEUR0Name, "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;
$.eur0Token = IEur0(_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 EUR0 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 EUR0 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: pause
Pauses the contract operations, preventing most function calls until unpaused.
function pause() external {
SEur0StorageV0 storage $ = _seur0StorageV0();
$.registryAccess.onlyMatchingRole(PAUSING_CONTRACTS_ROLE);
_pause();
}External function to pause the contract.
Retrieves storage pointer for SEur0StorageV0.
Ensures only addresses with PAUSING_CONTRACTS_ROLE set on registry access can call this function.
Calls the internal pause function from PausableUpgradeable.
Method: unpause
Unpauses the contract operations, allowing normal function calls to resume.
function unpause() external {
SEur0StorageV0 storage $ = _seur0StorageV0();
$.registryAccess.onlyMatchingRole(UNPAUSING_CONTRACTS_ROLE);
_unpause();
}External function to unpause the contract.
Retrieves storage pointer for SEur0StorageV0.
Ensures only addresses with UNPAUSING_CONTRACTS_ROLE set on registry access can call this function.
Calls the internal unpause function from PausableUpgradeable.
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();
}
SEur0StorageV0 storage $ = _seur0StorageV0();
$.registryAccess.onlyMatchingRole(BLACKLIST_ROLE);
if ($.isBlacklisted[account]) {
revert SameValue();
}
$.isBlacklisted[account] = true;
emit Blacklist(account);
}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.
Adds the account to the blacklist in SEur0StorageV0.
Emits an event to log the blacklisting action.
Method: unBlacklist
Removes an address from the blacklist, allowing it to interact with the contract again.
function unBlacklist(address account) external {
SEur0StorageV0 storage $ = _seur0StorageV0();
$.registryAccess.onlyMatchingRole(BLACKLIST_ROLE);
if (!$.isBlacklisted[account]) {
revert SameValue();
}
$.isBlacklisted[account] = false;
emit UnBlacklist(account);
}External function to remove an address from the blacklist.
Retrieves storage pointer for SEur0StorageV0.
Ensures only addresses with BLACKLIST_ROLE set on registry access can call this function. 4-6. Reverts if the account is not currently blacklisted.
Removes the account from the blacklist.
Emits an event to log the unblacklisting 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
{
SEur0StorageV0 storage $ = _seur0StorageV0();
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 SEur0StorageV0. 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
{
SEur0StorageV0 storage $ = _seur0StorageV0();
$.registryAccess.onlyMatchingRole(SEUR0_YIELD_DISTRIBUTOR_ROLE);
_startYieldDistribution(yieldAmount, startTime, endTime);
}1-3. External function for starting a new yield period. 4-5. Ensures only addresses with SEUR0_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 (EUR0). 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: deposit
Allows users to deposit assets and receive shares in return.
function deposit(uint256 assets, address receiver)
public
override(ERC4626Upgradeable, ISEur0)
whenNotPaused
returns (uint256 shares)
{
return super.deposit(assets, receiver);
}1-6. Public function overriding the ERC4626 deposit function, protected by whenNotPaused modifier. 7. Delegates to parent implementation which handles share calculation and minting.
Method: mint
Allows users to mint shares by depositing the equivalent amount of assets.
function mint(uint256 shares, address receiver)
public
override(ERC4626Upgradeable, ISEur0)
whenNotPaused
returns (uint256)
{
return super.mint(shares, receiver);
}1-6. Public function overriding the ERC4626 mint function, protected by whenNotPaused modifier. 7. Delegates to parent implementation which handles asset deposit and share minting.
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, ISEur0)
nonReentrant
whenNotPaused
returns (uint256 shares)
{
SEur0StorageV0 storage $ = _seur0StorageV0();
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;
$.eur0Token.safeTransfer($.treasuryYield, fee);
}1-7. Public function overriding the ERC4626 withdraw function, protected by nonReentrant and whenNotPaused modifiers. 8-9. Retrieves storage pointers for SEur0StorageV0 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, ISEur0)
nonReentrant
whenNotPaused
returns (uint256 assets)
{
SEur0StorageV0 storage $ = _seur0StorageV0();
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;
$.eur0Token.safeTransfer($.treasuryYield, fee);
}1-7. Public function overriding the ERC4626 redeem function, protected by nonReentrant and whenNotPaused modifiers. 8-9. Retrieves storage pointers for SEur0StorageV0 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, ISEur0)
returns (uint256 shares)
{
SEur0StorageV0 storage $ = _seur0StorageV0();
// 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 SEur0StorageV0. 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: maxWithdraw
Returns the maximum amount of assets that can be withdrawn by a given owner, accounting for fees.
function maxWithdraw(address owner)
public
view
override(ERC4626Upgradeable, ISEur0)
returns (uint256)
{
return previewRedeem(balanceOf(owner));
}1-5. Public view function overriding ERC4626 maxWithdraw. 6. Calculates the maximum withdrawable assets by previewing the redemption of all shares owned by the owner, which automatically accounts for fees.
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, ISEur0)
returns (uint256 assets)
{
SEur0StorageV0 storage $ = _seur0StorageV0();
// 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 SEur0StorageV0. 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 {
SEur0StorageV0 storage $ = _seur0StorageV0();
$.registryAccess.onlyMatchingRole(SEUR0_WITHDRAW_FEE_UPDATER_ROLE);
if (newWithdrawFeeBps > MAX_25_PERCENT_WITHDRAW_FEE) {
revert AmountTooBig();
}
if (newWithdrawFeeBps == $.withdrawFeeBps) {
revert SameValue();
}
$.withdrawFeeBps = newWithdrawFeeBps;
emit WithdrawFeeUpdated(newWithdrawFeeBps);
}External function for updating withdrawal fee. 2-3. Retrieves storage pointer and validates caller has SEUR0_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.
Updates the withdrawal fee in storage.
Emits an event to log the fee update.
Method: isBlacklisted
Checks if an address is currently blacklisted.
function isBlacklisted(address account) external view returns (bool) {
SEur0StorageV0 storage $ = _seur0StorageV0();
return $.isBlacklisted[account];
}External view function to check blacklist status.
Retrieves storage pointer for SEur0StorageV0.
Returns the blacklist status for the given account.
Method: withdrawFeeBps
Returns the current withdrawal fee in basis points.
function withdrawFeeBps() public view returns (uint256) {
SEur0StorageV0 storage $ = _seur0StorageV0();
return $.withdrawFeeBps;
}Public view function to retrieve withdrawal fee.
Retrieves storage pointer for SEur0StorageV0.
Returns the current withdrawal fee in basis points.
Method: getYieldRate
Returns the current yield rate used for yield distribution.
function getYieldRate() external view returns (uint256) {
YieldDataStorage storage $ = _getYieldDataStorage();
return $.yieldRate;
}External view function to retrieve yield rate.
Retrieves yield data storage.
Returns the current yield rate value.
Last updated
Was this helpful?