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)

Scope

OpenZeppelin performed an audit of the Develop-ltda/efixdi-backend repository at commit 8b9b217ae3630dcca37a43ad366f49432310be31.

In scope was the following file:

 contracts
└── EfixDIToken.sol

System Overview

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.

EfixDIToken

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.

  • Minting and Burning: The contract restricts the ability to mint and burn tokens to specific roles. The 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.
  • Supply Management: The total supply of the token can be capped. An administrative role has the authority to set and adjust this maximum supply.
  • Pausable: The contract can be paused by an administrator, which halts all token transfers, minting, and burning operations. This is a security feature to be used in case of an emergency.
  • ERC-20 Permit: The token supports gas-less approvals through the EIP-2612 permit function.
  • Emergency Functions: An administrator can withdraw any ERC-20 tokens accidentally sent to the contract address to a designated treasury address.

Security Model and Trust Assumptions

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.

  • Administrator Trust: A single address, holding the 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 and Burner Integrity: The 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.
  • Off-Chain Backing: The 1:1 backing of the efixDI token with BRL is not enforced on-chain. Users and integrators must trust the off-chain processes and the entities that manage the underlying DI Fund to maintain the peg and correctly report the yield.
  • Operational Security: The security of the system depends on the operational security of the off-chain environment where administrative keys are stored and managed. The contract itself does not implement multi-signature or timelock requirements for administrative actions.
  • Pause Management: The ability to pause the contract is a centralized power. As such, it is assumed that the administrator will use this power responsibly and only in genuine emergencies to protect the system and its users.

Privileged Roles

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.

High Severity

User Transfer of efixDI Blocks Emergency Withdraw and Auto-Deleverage

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.

Medium Severity

Paused Token Blocks Vault Emergency Withdrawal

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.

Low Severity

Unchecked ERC-20 Transfer in emergencyWithdraw Can Report Success While No Tokens Are Sent

EfixDIToken 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.

Floating Pragma

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.

Possible Duplicate Event Emissions

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.

Incomplete Docstrings

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.

Missing Docstrings

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.

Notes & Additional Information

emergencyWithdraw Accepts Zero Amount

emergencyWithdraw 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.

Redundant Access Modifier on Role Grant/Revoke Helpers

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.

Redundant whenNotPaused Modifier on mint

mint 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.

Redundant Zero-Address Check in mint

mint 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.

Use Custom Errors

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.

Lack of Indexed Event Parameters

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.

Non-Explicit Imports

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.

Inconsistent Constructor Parameter Naming

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.

Conclusion

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.

Appendix

Issue Classification

OpenZeppelin classifies smart contract vulnerabilities on a 5-level scale:

  • Critical
  • High
  • Medium
  • Low
  • Note/Information

Critical Severity

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.

High Severity

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.

Medium Severity

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

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.

Notes & Additional Information Severity

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.