Organization
- @euler
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Low Risk
3 findings
3 fixed
0 acknowledged
Informational
6 findings
5 fixed
1 acknowledged
Low Risk3 findings
Default fee recipient can be set to zero address
Severity
- Severity: Low
Submitted by
slowfi
Description
The function
setDefaultRecipientfrom contractEulerSwapProtocolFeeConfigassignsdefaultRecipient = recipientwithout rejectingaddress(0). IfdefaultRecipientis set to zero, any pool without an override may resolve to a zero recipient, which can burn fees or break downstream transfers depending on token behavior.Also it is important to know that this breaks the flow from the previous implementation. Previously if protocol fee recipient is address(0), the fees are deposited into the euler account vs now they are burned. As a result, the account's NAV won't increase (as expected).
Recommendation
Consider to enforce a non-zero
recipientinsetDefaultRecipient. If you need a “reset” control path, consider to add an explicitunsetDefaultRecipient()(with a safe fallback recipient) or track a separate boolean sentinel rather than usingaddress(0).Missing events for administrative state changes
Severity
- Severity: Low
Submitted by
slowfi
Description
The functions
setAdmin,setDefault,setOverride, andremoveOverridefrom contractEulerSwapProtocolFeeConfigupdate critical configuration without emitting events (src/EulerSwapProtocolFeeConfig.sol:48+). The absence of events reduces on-chain observability for indexers, off-chain monitoring, audits, and incident response, and makes it harder to track configuration history per pool.- The function
setAdminfrom contractEulerSwapProtocolFeeConfigchanges the admin without an event. - The function
setDefaultfrom contractEulerSwapProtocolFeeConfigupdates the global default fee recipient/fee without an event. - The function
setOverridefrom contractEulerSwapProtocolFeeConfigsets a per-pool override without an event. - The function
removeOverridefrom contractEulerSwapProtocolFeeConfigclears a per-pool override without an event.
Recommendation
Consider to emit explicit events for each state transition, including previous and new values and indexed fields for efficient querying. For example:
event AdminUpdated(address indexed oldAdmin, address indexed newAdmin);event DefaultUpdated(address indexed oldRecipient, address indexed newRecipient, uint64 oldFee, uint64 newFee);event OverrideSet(address indexed pool, address indexed recipient, uint64 fee);event OverrideRemoved(address indexed pool);Emit the corresponding event in
setAdmin,setDefault,setOverride, andremoveOverride. Also consider to emit events during contract initialization to establish the baseline configuration in logs.Activation marks pool active before initialization completes
Severity
- Severity: Low
Submitted by
slowfi
Description
The function
activatefrom contractEulerSwapManagementsetss.status = 1atsrc/EulerSwapManagement.sol:68before completing all initialization. If status1represents “active,” any external call made afterward (directly or via hooks) could observe the pool as active and enter flows that assume full initialization is finished.Recommendation
Consider to mark the pool as “initializing” first (e.g.,
s.status = 2) and only sets.status = 1at the very end, after all external calls and state are finalized. Also consider to replace magic numbers with anenumfor clarity and to enforce status checks (require(s.status == Initializing/Inactive/Active)) at critical entry points.
Informational6 findings
Remove unused code
Severity
- Severity: Informational
Submitted by
Sujith S
Description
Multiple files under the scope of the audit contain unused file imports, error declarations, or event declarations. This dead code will affect code quality. Consider deleting any unused file imports, error, and event declarations referenced in this finding.
Improve code documentation
Severity
- Severity: Informational
Submitted by
Sujith S
Description
- In
FundsLib.depositAssets(), the inline documentation mentions that only E_ZeroShares is handled by setting the amount to zero, but the code actually manages both E_ZeroShares and ZeroShares errors.
- /// @dev If the deposit fails with E_ZeroShares error, it safely returns 0 (this happens with very small amounts).+ /// @dev If the deposit fails with E_ZeroShares or ZeroShares error, it safely returns 0 (this happens with very small amounts).- All non-abstract contracts in the repository have inline documentation and a security contact before declaration, except the new
EulerSwapManagementandEulerSwapcontracts.
+ /// @title EulerSwapManagement contract+ /// @custom:security-contact [email protected]+ /// @author Euler Labs (https://www.eulerlabs.com/) contract EulerSwapManagement is EulerSwapBase {- The function
eulerSwapCall()from contractIEulerSwapCalleedefines the flash-swap callback, but its parameters are sparsely documented. This can hinder integrators from implementing correct settlement logic, leading to subtle bugs. Consider to expand the NatSpec for eulerSwapCall to clarify integrators on received parameters.
Internal function delegateToManagementImpl violates conventions
Severity
- Severity: Informational
Submitted by
Sujith S
Description
The internal function
delegateToManagementImpl()violates standard Solidity conventions in two ways:-
Naming Convention: Internal functions should be prefixed with an underscore _ to distinguish them from external/public functions clearly. The function is named
delegateToManagementImpl()instead of_delegateToManagementImpl(). -
File Organization: Internal and private functions are conventionally placed at the bottom of the contract, after all external and public functions.
Consider fixing the above-mentioned convention issue.
Do not use EVC sub-accounts as fee recipients
Severity
- Severity: Informational
Submitted by
slowfi
Description
The functions
setDefaultandsetOverridefrom contractEulerSwapProtocolFeeConfig, and the fee transfer paths inSwapLib(LP and protocol fee handling), do not remap EVC sub-accounts to their owners before ERC20 transfers. Unlike the registry’s bond redemption flow, which callsevc.getAccountOwner(recipient)to resolve the owner, fee recipients are used as-is. As a result, configuring LP or protocol fee recipients to an EVC sub-account can direct fees to an address without control keys. Additionally, the activation guard only forbidsfeeRecipient == eulerAccountfor the pool; other EVC sub-accounts are still permitted.Recommendation
Consider to explicitly document that LP and protocol fee recipients must be externally owned addresses or controllable contract addresses, and should not be EVC sub-accounts. Include this constraint in deployment runbooks, admin UI validations, and configuration examples. Optionally, consider to note that the registry remaps bond recipients to owners, but fee flows currently do not, to avoid assumptions of parity.
Admin address must not be an EVC sub-account
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
setAdminfrom contractEulerSwapProtocolFeeConfigupdates theadminwithout restricting it to a non-EVC sub-account. Subsequent admin-only calls useonlyAdmin, which invokes_authenticateCallerWithStandardContextState(true). If the storedadminis an EVC sub-account, or if calls are routed through EVC withonBehalfOfAccountset to a sub-account, authentication can fail, effectively bricking administrative functions for this contract.Recommendation
Consider to document that
adminmust be a controllable EOA or standard contract address, not an EVC sub-account. Include this in deployment runbooks and any admin tooling. Optionally, consider to validate in ops tooling thatnewAdminis not an EVC sub-account (e.g., warn whenevc.getAccountOwner(newAdmin) != address(0)).Protocol fee override can target non existent pool
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
setOverridefrom contractEulerSwapProtocolFeeConfigsetsoverrides[pool]without validating thatpoolis a real EulerSwap pool. This enables typos or stale addresses to create dead configuration entries that will never be used, complicating ops and audits.Recommendation
Consider to verify the
poolagainst the factory/registry before storing the override (e.g.,IEulerSwapFactory.isPool(pool)or equivalent), and revert if the pool is unknown. Also consider to emit an event so indexers can track successful validations.Euler: There may exist a use case for overriding the fees before the pool creation to have the existing data at pool time creation.
Cantina managed: Acknowledged by Euler team.