Summary
Type: Financial Institutions
Timeline: From 2026-03-11 → To 2026-03-12
Languages: Solidity
Findings
Total issues: 15 (15 resolved)
Critical: 0 (0 resolved) · High: 1 (1 resolved) · Medium: 1 (1 resolved) · Low: 5 (5 resolved)
Notes & Additional Information
8 notes raised (8 resolved)
OpenZeppelin performed an audit of the Develop-ltda/efixdi-backend repository at commit 8b9b217ae3630dcca37a43ad366f49432310be31.
In scope was the following file:
contracts
└── EfixDIToken.sol
EfixDIToken (efixDI) is an ERC-20 token designed to represent a tokenized position in a Brazilian Deposit Insurance (DI) Fund. The system's core component is the EfixDIToken smart contract, which manages the token's lifecycle, including issuance, transfers, and redemptions. While the token aims to be backed 1:1 by Brazilian Real (BRL) in an underlying fund, the enforcement of this backing and the application of yield are managed by off-chain processes and a separate EfixVault contract, not by the token itself.
The EfixDIToken contract is a pausable ERC-20 token with role-based access control for minting and burning. It is designed to interact with a vault and potentially a bridge for cross-chain functionality.
MINTER_ROLE can create new tokens, while the BURNER_ROLE can burn tokens from any account without requiring prior approval. Standard user-initiated burns are also possible.permit function.The security of the EfixDIToken system relies on a combination of on-chain access control and trusted off-chain operational procedures. The system's integrity depends on the correct management of privileged roles and the secure execution of off-chain processes that are outside the scope of the smart contract's control.
DEFAULT_ADMIN_ROLE, has extensive power over the contract. This role can grant and revoke all other privileged roles, pause and unpause the contract, set the maximum token supply, and update the treasury address. The security of the system is therefore highly dependent on the security of the key or account controlling this role.MINTER_ROLE and BURNER_ROLE are intended to be assigned to trusted smart contracts, such as the EfixVault or a bridge. There is an implicit trust that these roles will be granted only to audited and secure contracts, as a compromised or malicious actor with these roles could mint an arbitrary number of tokens or burn tokens from any user.The EfixDIToken contract uses OpenZeppelin's AccessControl to manage permissions. The following roles are defined:
DEFAULT_ADMIN_ROLE: This is the highest-level administrative role. The holder of this role can grant and revoke any other role, pause and unpause the contract, set the maximum supply, update the treasury address, and perform emergency withdrawals of other ERC-20 tokens.MINTER_ROLE: Accounts with this role are permitted to mint new efixDI tokens. This role is intended for the EfixVault contract and potentially a bridge contract.BURNER_ROLE: Accounts with this role can burn tokens from any address without requiring an allowance. This is intended for use by the EfixVault contract for operations such as withdrawals and deleveraging.The vault maintains per-user position state in positions[user], including efixDIBalance, and relies on burning the user’s efixDI tokens for withdrawal, deleverage, and auto-deleverage. However, the vault does not reconcile this balance with the user’s on-chain efixDI balance. Since efixDI is a standard ERC-20 token, users can transfer tokens to any address.
If a user has transferred some or all of their efixDI elsewhere, the vault’s accounting still shows the original balance while the tokens are held by another address. Any flow that burns from the user—such as an admin-triggered emergency withdrawal for that user or the keeper calling autoDeleverage(user)—will revert when the user’s on-chain balance is insufficient. The position cannot then be closed or deleveraged, so the protocol may be left with an uncollateralized debt position and bad debt in the external lending pool.
Consider keeping the vault’s position accounting in sync with on-chain efixDI balances—for example, by having the token notify the vault on transfer so that the sender’s position is debited and the receiver’s position is credited (or otherwise updated). That way, emergency withdrawals and auto-deleverage can always operate on the correct balances and avoid reverts due to transferred tokens.
Update: Resolved in pull request #1 and pull request #2. The vulnerability has been addressed with a two-phase design: EfixDIToken V2 adds an _update() override that calls EfixCollateralVault.getPosition(from) — any transfer where the sender has borrowed > 0 reverts with TransferBlockedByVault() (mints and burns are exempt). EfixCollateralVault V2 takes custody via depositFor() before bridging/borrowing, and blocks withdrawals while borrowed > 0.
EfixDIToken is the protocol's ERC-20 token. It is pausable and overrides _update with whenNotPaused, so all balance-changing operations (transfers, mints, and burns) are disabled while the token is paused. The vault holds BURNER_ROLE and uses burnFrom in withdrawal, deleverage, and autoDeleverage to burn user efixDI and settle positions off-chain (e.g., via fiat).
When the token is paused, the vault's flows that call burnFrom cannot get completed: burnFrom triggers _burn, which calls _update, and that path reverts due to whenNotPaused. Unpausing to allow these operations would re-enable all transfers and contradict the goal of keeping tokens frozen during an emergency.
Consider allowing burns by BURNER_ROLE when the contract is paused—for example, by overriding _update to skip the pause check when the caller has BURNER_ROLE and the recipient is the zero address, or by exposing a separate burn path for the vault that does not go through the paused _update.
Update: Resolved in pull request #2.
emergencyWithdraw Can Report Success While No Tokens Are SentEfixDIToken allows an admin to rescue ERC-20 tokens sent to the contract by calling emergencyWithdraw(token, amount), which forwards the tokens to the treasury. This is intended only for recovering mistakenly sent tokens, not the contract's own efixDI token.
The function uses IERC20Minimal(token).transfer(treasury, amount) and does not use the return value. The ERC-20 standard defines transfer to return a boolean, but some widely used tokens (e.g., USDT on mainnet) do not revert on failure and instead return false. For such tokens, a failed transfer does not revert the transaction, so emergencyWithdraw would complete and still emit EmergencyWithdraw. The result is a mismatch between on-chain state (no tokens moved) and the emitted event (withdrawal logged), and off-chain indexers would treat it as a successful withdrawal.
Consider using OpenZeppelin's SafeERC20 and calling SafeERC20.safeTransfer(IERC20(token), treasury, amount) so that both reverting and boolean-returning tokens are handled correctly and the transaction reverts when the transfer fails, ensuring that the event is only emitted when tokens are actually sent.
Update: Resolved in pull request #2.
The pragma directive specifies which Solidity compiler version(s) may be used to compile the contract. A fixed pragma ensures that the same bytecode is produced across environments and makes audits and deployments reproducible.
EfixDIToken.sol uses pragma solidity ^0.8.20, which allows compilation with any compatible 0.8.x version and can lead to inconsistent builds across environments.
Consider using a fixed pragma (e.g., pragma solidity 0.8.20;) so that the contracts are always compiled with the intended compiler version.
Update: Resolved in pull request #2.
Setters that emit events allow off-chain systems to track configuration changes. When the new value is not compared to the current one, the same value can be set repeatedly and the same event emitted each time.
In EfixDIToken, setMaxSupply updates maxSupply and emits MaxSupplyUpdated without checking if the value has changed. setTreasury updates treasury and emits TreasuryUpdated without that check. Repeated emission of identical events can confuse off-chain clients and clutter logs.
Consider adding a check that reverts (or skips the update and event) when the value being set is identical to the existing one so that duplicate events are not emitted.
Update: Resolved in pull request #2.
NatSpec docstrings describe a contract’s public API for developers and tools. When parameters or return values are omitted, integrators and auditors have to infer behavior from the code.
Within EfixDIToken.sol, several public and external functions have incomplete docstrings: the decimals function does not document its return value; canMint, canBurn, grantMinter, revokeMinter, grantBurner, and revokeBurner do not document the account parameter or (where applicable) the return value; remainingMintableSupply does not document its return value.
Consider documenting all functions and events that form part of the contract’s public API, including every parameter and return value, and following the Ethereum Natural Specification Format (NatSpec).
Update: Resolved in pull request #2.
NatSpec docstrings help developers and tools understand a contract’s public API. When they are absent, integrators must rely on the code or external documentation to interpret parameters and behavior.
Within EfixDIToken.sol, docstrings are missing for the following: the MaxSupplyUpdated event, the TreasuryUpdated event, the EmergencyWithdraw event, and the transfer function (in the minimal interface used for emergency withdraw).
Consider documenting all functions and events that form part of the contract’s public API, including their parameters and return values where applicable. For functions that implement sensitive logic (including non-public ones), add clear docstrings as well. When writing docstrings, follow the Ethereum Natural Specification Format (NatSpec).
Update: Resolved in pull request #2.
emergencyWithdraw Accepts Zero AmountemergencyWithdraw recovers ERC-20 tokens sent to the contract by mistake. It takes a token address and an amount and sends the tokens to the treasury. The function does not enforce that amount is greater than zero. A call with amount == 0 still succeeds and emits EmergencyWithdraw, which can mislead off-chain monitoring and indexing and has no operational effect.
Consider adding a check such as require(amount > 0, "EfixDIToken: zero amount"); before performing the transfer.
Update: Resolved in pull request #2.
EfixDIToken exposes helpers (grantMinter, revokeMinter, grantBurner, and revokeBurner), each protected with onlyRole(DEFAULT_ADMIN_ROLE). These functions only forward to AccessControl's grantRole and revokeRole. OpenZeppelin's grantRole and revokeRole already enforce that the caller has the admin of the role being changed via onlyRole(getRoleAdmin(role)). For MINTER_ROLE and BURNER_ROLE, the admin is DEFAULT_ADMIN_ROLE, so the same authorization is applied inside the library. The modifier on the wrapper is redundant and performs a duplicate check.
Consider removing the onlyRole(DEFAULT_ADMIN_ROLE) modifier from grantMinter, revokeMinter, grantBurner, and revokeBurner for improved code clarity and to avoid redundant checks.
Update: Resolved in pull request #2.
mintmint is restricted to addresses with MINTER_ROLE and is used by the vault to mint efixDI when users deposit or leverage. All token movements go through the internal _update hook, which this contract overrides and guards with whenNotPaused in _update, so transfers and mints are already blocked when the contract is paused. mint is decorated with whenNotPaused even though it ultimately calls _mint, which uses _update. Since _update already enforces whenNotPaused, the modifier on mint is redundant and only duplicates the same check.
Consider removing the whenNotPaused modifier from mint so that pause behavior is only enforced in _update, avoiding duplicate logic and a saving small amount of gas.
Update: Resolved in pull request #2.
mintmint allows authorized minters to create new efixDI tokens for a given recipient and delegates to the inherited _mint(to, amount). The function includes require(to != address(0), "EfixDIToken: mint to zero address"); in line 76 before calling _mint. The OpenZeppelin ERC-20 implementation reverts in _mint when the recipient is the zero address (ERC20InvalidReceiver(address(0))), so the explicit check is redundant.
Consider removing the require(to != address(0), ...) check so that the zero-address condition is only enforced by the inherited _mint implementation and duplicate logic is avoided.
Update: Resolved in pull request #2.
Since Solidity 0.8.4, custom errors offer a clearer and more gas-efficient way to signal why an operation failed. They replace string-based require messages and are commonly used in modern contracts.
EfixDIToken.sol still uses require with string messages in several places (e.g., constructor, mint, setMaxSupply, setTreasury, emergencyWithdraw) instead of custom errors.
For conciseness and gas savings, consider replacing the aformentioned require/string messages with custom errors (e.g., error ZeroAddress();, error MaxSupplyExceeded();) and using revert ZeroAddress(); (and similar) instead of string-based revert.
Update: Resolved in pull request #2.
Indexed parameters in Solidity events are stored in topic hashes and allow off-chain services to filter and search for events by those values efficiently. Non-indexed parameters are only stored in the event data and are not filterable by value.
In EfixDIToken.sol, the MaxSupplyUpdated, TreasuryUpdated, and EmergencyWithdraw events have no indexed parameters.
Consider indexing event parameters where appropriate (e.g. address indexed for treasury or token, or uint256 indexed for amounts) to improve the ability of off-chain services to query and filter for specific events, following the Solidity documentation on events.
Update: Resolved in pull request #2.
Imports can be written as global (importing the whole file) or as named imports (importing specific symbols). Named imports make it explicit which symbols come from which file and reduce the risk of naming conflicts when a file defines multiple contracts or uses long inheritance chains.
EfixDIToken.sol uses global imports for OpenZeppelin contracts (e.g., ERC20, ERC20Burnable, ERC20Permit, AccessControl, Pausable) instead of named imports.
Consider switching to named import syntax and only importing what is used (e.g., import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; and similarly for other imports) so that each imported contract is explicitly declared and the code is clearer and easier to maintain.
Update: Resolved in pull request #2.
Consistent naming of constructor parameters (e.g., prefixing with _ to distinguish them from state variables) improves readability and aligns with common Solidity conventions.
In EfixDIToken, the constructor uses the underscore prefix for parameters _treasury and _maxSupply but not for admin. That breaks the project's convention of prefixing constructor (and function) parameters with _ to distinguish them from state and locals, and can make the code harder to read and maintain.
Consider renaming the first constructor parameter from admin to _admin (and updating the NatSpec and all uses of that parameter in the constructor) so that it matches _treasury and _maxSupply and the naming is kept consistent across the codebase.
Update: Resolved in pull request #2.
This audit of the EfixDIToken contract—a pausable ERC-20 token with role-based access controls—found a generally well-structured and secure implementation. One high-severity issue was identified: user transfers of efixDI are not reflected in the vault’s position accounting, which can block emergency withdrawals and keeper-driven auto-deleverage when the user’s on-chain balance is insufficient. A medium-severity issue was also noted: pausing the token could unintentionally block emergency withdrawals in the associated vault. Several low- and note-severity findings were reported in addition.
Core access controls and token mechanics are sound. The audit team’s recommendations focus on vault–token accounting alignment, adherence to smart contract best practices, and robust behaviour of system components, especially under emergency conditions.
This engagement was limited to EfixDIToken; its integrations, such as the collateral vault, were not in scope and have remained unreviewed. We recommend that they undergo a dedicated security assessment before production use.
All OpenZeppelin library files in use were confirmed to match release v5.4.0. The compiler and configuration (settings.json) were deemed reasonable and safe in this context.
We thank the Hausbank team for their cooperation, the context they provided, and their prompt responses to our questions, which made for a smooth and effective audit process.
OpenZeppelin classifies smart contract vulnerabilities on a 5-level scale:
This classification is applied when the issue’s impact is catastrophic, threatening extensive damage to the client's reputation and/or causing severe financial loss to the client or users. The likelihood of exploitation can be high, warranting a swift response. Critical issues typically involve significant risks such as the permanent loss or locking of a large volume of users' sensitive assets or the failure of core system functionalities without viable mitigations. These issues demand immediate attention due to their potential to compromise system integrity or user trust significantly.
These issues are characterized by the potential to substantially impact the client’s reputation and/or result in considerable financial losses. The likelihood of exploitation is significant, warranting a swift response. Such issues might include temporary loss or locking of a significant number of users' sensitive assets or disruptions to critical system functionalities, albeit with potential, yet limited, mitigations available. The emphasis is on the significant but not always catastrophic effects on system operation or asset security, necessitating prompt and effective remediation.
Issues classified as being of medium severity can lead to a noticeable negative impact on the client's reputation and/or moderate financial losses. Such issues, if left unattended, have a moderate likelihood of being exploited or may cause unwanted side effects in the system. These issues are typically confined to a smaller subset of users' sensitive assets or might involve deviations from the specified system design that, while not directly financial in nature, compromise system integrity or user experience. The focus here is on issues that pose a real but contained risk, warranting timely attention to prevent escalation.
Low-severity issues are those that have a low impact on the client's operations and/or reputation. These issues may represent minor risks or inefficiencies to the client's specific business model. They are identified as areas for improvement that, while not urgent, could enhance the security and quality of the codebase if addressed.
This category is reserved for issues that, despite having a minimal impact, are still important to resolve. Addressing these issues contributes to the overall security posture and code quality improvement but does not require immediate action. It reflects a commitment to maintaining high standards and continuous improvement, even in areas that do not pose immediate risks.