Forta Firewall Audit
Table of Contents
- Table of Contents
- Summary
- Scope
- System Overview
- Firewall
- SecurityValidator
- Forta Chain Contracts
- Privileged Roles
- Security Model and Trust Assumptions
- Critical Severity
- High Severity
- Medium Severity
- Limited Composability Due to Front-End Dependent Attestation Process
- Incomplete Validation of Attestations in SecurityValidator
- Attested Values May Significantly Differ From On-Chain Execution
- Lack of Challenge Mechanism for Incorrect Transaction Delays
- Inconsistent Delay Mechanisms in Inbox and AttesterControllers Contracts
- Potential for Transaction Censorship via Repeated Delay
- Low Severity
- Insufficient Code Coverage
- SecurityValidator Does Not Inherit From ISecurityValidator
- Stored Attestations Cannot Be Loaded After Previous Attestations Have Been Used
- Implemented Methods Are Not Part of the Interface
- Missing Error Messages in require Statements
- Incomplete Docstrings
- Missing Docstrings
- Missing Zero Address Checks
- Locked ETH in Contract
- Unsafe Use of DEFAULT_ADMIN_ROLE
- Events Emitted Regardless of Whitelist Status Change
- Lack of Maximum Delay for Suspicious Transactions
- Multicall Functionality in Some Firewalls Is Inefficient and Adds Limited Value
- Stored Attestations Cannot Be Used By Smart Contract Accounts
- Notes & Additional Information
- Redundant Functionality Increases Contract Size and Complexity
- Code Clarity
- Missing Named Parameters in Mapping
- Non-Explicit Imports Are Used
- State Variable Visibility Not Explicitly Declared
- Unused Named Return Variables
- Gas Optimizations
- Redundant Code
- Misleading Documentation
- Typographical Error
- File and Contract Names Mismatch
- Floating Pragma
- Lack of Security Contact
- Interfaces Should Be Defined in Separate Files
- Naming Issues
- Lack of License Specification
- Unnecessary Use of AccessControl in the Whitelist Contract
- Inconsistent Order Within Contracts
- Lack of Indexed Event Parameters
- Unused Errors
- Unused Imports
- Firewall Does Not Need To Be Initializable
- Client Reported
- Conclusion
Summary
- Type
- Security Infrastructure
- Timeline
- From 2024-09-09
- To 2024-09-23
- Languages
- Solidity
- Total Issues
- 47 (31 resolved, 4 partially resolved)
- Critical Severity Issues
- 1 (1 resolved)
- High Severity Issues
- 1 (0 resolved, 1 partially resolved)
- Medium Severity Issues
- 6 (3 resolved, 1 partially resolved)
- Low Severity Issues
- 14 (11 resolved)
- Notes & Additional Information
- 22 (16 resolved, 2 partially resolved)
- Client Reported Issues
- 3 (0 resolved)
Scope
We audited the forta-network/forta-firewall-contracts repository at commit edf03cd and the forta-network/forta-attester-controller-contracts repository at commit 8685148.
In scope were the following files:
forta-firewall-contracts
└── src
├── AttestationForwarder.sol
├── CheckpointExecutor.sol
├── ExternalFirewall.sol
├── FirewallAccess.sol
├── FirewallPermissions.sol
├── Firewall.sol
├── InternalFirewall.sol
├── ProxyFirewall.sol
├── Quantization.sol
└── SecurityValidator.sol
forta-attester-controller-contracts
└── src
├── AttesterControllers.sol
├── Errors.sol
├── Inbox.sol
├── Registry.sol
├── RoleManager.sol
└── Whitelist.sol
System Overview
The Forta Firewall is an innovative security solution designed to enhance the protection of DeFi protocols operating on EVM networks. It integrates off-chain exploit detection capabilities with on-chain smart contract security measures. The system works by intercepting and screening transactions before they are executed on the blockchain, allowing for the detection and prevention of potential exploits or malicious activities.
At its core, the Forta Firewall utilizes a system of checkpoints within smart contracts. These checkpoints act as configurable security gates that can activate under specific conditions, such as when a certain transaction parameter exceeds a predefined threshold. When a checkpoint is triggered, it communicates with a validator contract to verify if the transaction has been attested as safe by off-chain security services. The transactions which do not receive attestations revert by default.
Attestations can be stored in two ways:
- Before the transaction execution, using the
storeAttestation()function, which utilizesSSTOREto persist the attestation in contract storage. - At the beginning of the same transaction, using the
saveAttestation()function, which leverages the more gas-efficientTSTORE/TLOADopcodes. This method stores the attestation in transient storage, making it available throughout the transaction's execution without incurring the higher gas costs associated with permanent storage writes.
This architecture allows for real-time security checks without significantly impacting the performance or gas costs of normal operations. By combining off-chain analysis with on-chain enforcement, the Forta Firewall provides a robust and flexible security layer for DeFi protocols.
Firewall
The Firewall smart contract is the core component of the Forta Firewall system. It implements the security checkpoint logic, receives attestations, and controls the execution flow based on security validations. The contract includes functions for setting and managing checkpoints, saving attestations, and executing security checks.
Key features of the Firewall contract include:
- Checkpoint configuration and management
- Secure execution flow control
- Integration with external security validators to save attestations and execute checkpoints
Quantization
Quantization is a crucial feature implemented in the Firewall to address value fluctuations between attestation creation and on-chain execution. The Quantization library provides the quantize() function which rounds input values to predetermined scales before use in checkpoint calculations, ensuring consistency between off-chain attestations and on-chain execution, even with slight value changes. This is particularly important in DeFi contexts where asset values can change rapidly.
Firewall Variations
The Forta Firewall system offers three main implementation variations to accommodate different integration needs and constraints of DeFi protocols. Each variation provides the core firewall functionality while offering unique characteristics for deployment and interaction. The choice between these variations depends on factors such as the existing contract structure, upgrade mechanisms, and the desired level of integration.
Proxy Firewall
The Proxy Firewall is an approach that leverages the proxy pattern common in upgradeable smart contracts. Its characteristics include:
- It acts as an intermediary contract sitting between a proxy contract and the logic contract.
- The proxy firewall intercepts all calls to the logic contract, executes necessary security checks, and then delegates the call to the actual logic contract.
- This variation requires no modifications to the existing logic contract, making it ideal for protocols using upgradeable proxy patterns.
- It adds a small gas overhead to each transaction but offers seamless integration of security features.
Internal Firewall
The Internal Firewall is suitable for protocols that can directly incorporate firewall logic into their contracts. Key features include:
- Firewall functionality is inherited directly by the target contract.
- Checkpoint configurations are stored within the contract itself.
- This variation offers tighter integration and potentially lower gas costs for checkpoint executions.
- It requires more significant modifications to the existing contract but provides greater control over the firewall's operation.
External Firewall
The External Firewall is designed for scenarios where direct modification of the target contract is not feasible or desired. In this variation:
- The firewall logic is implemented in a separate contract.
- The target contract inherits from a
CheckpointExecutorcontract which provides methods to interact with the external firewall. - Checkpoint configurations and executions are managed by the external firewall contract.
- This approach allows for minimal changes to the existing contract while still providing robust security features.
SecurityValidator
The SecurityValidator is a singleton contract where users save attestations for their transactions. These attestations include a deadline for validity and an ordered list of "execution hashes", ensuring that users execute checkpoints in the exact order approved by the attester. In other words, users must perform actions in the transaction that trigger security checks (checkpoints) in the same sequence as those considered by the attester during off-chain simulation.
Furthermore, the SecurityValidator is used by contracts implementing a Firewall to ensure that users provide valid attestations before executing actions deemed sensitive by the protocol (i.e., actions that trigger a checkpoint during execution).
To enable attesters to simulate transactions off-chain without needing attestations, the contract implements a bypass mechanism. This mechanism involves setting non-empty code at a specific address, which is easily achieved using cheat codes on a local fork of the chain, but impossible to do in actual on-chain execution.
AttestationForwarder
The AttestationForwarder contract is a forwarder compatible with ERC-2771 contracts, built on top of OpenZeppelin's ERC2771Forwarder. Its sole purpose is to sponsor calls that save attestations in persistent storage within the SecurityValidator contract, allowing users to load these attestations in subsequent transactions.
Forta Chain Contracts
The Forta Attester architecture consists of a suite of contracts deployed on the Forta Chain, designed to manage roles, registries, controllers, and other components to ensure the system's decentralization and censorship resistance. The primary objectives of these contracts are to:
- Act as a public, immutable ledger for attester detections.
- Store encrypted per-protocol configurations for the attester client.
- Serve as a global database for transactions that have been deemed insecure and delayed.
AttesterControllers
The AttesterControllers contract manages controllers within the system, each containing the encrypted configuration used by attesters and the delay duration for transactions flagged as high-risk. To foster decentralization, each protocol integrating with the Forta Firewall can deploy its own instance of this contract as the administrator role holds privileges over how attesters operate and are configured.
In addition, attesters use this contract to log detections as events. These events include a unique detection ID, a risk score, and the encrypted details of the detection. Detections surpassing a risk threshold are delayed and stored in the Inbox contract, a singleton that records release times for suspicious transactions. To ensure censorship resistance, once the release time passes, attesters are required to sign attestations for these transactions, even if initially flagged as high-risk.
Privileged Roles
As the Forta Network is divided into two main parts, the on-chain and the off-chain, each one has its own set of privileged roles.
On-Chain Firewall System Roles
DEFAULT_ADMIN_ROLE- Can grant and revoke the Firewall Admin role
- Assigned at construction time
FIREWALL_ADMIN_ROLE- Highest level of access in the firewall system
- Can manage Protocol Admins
- Responsible for overall firewall configuration and management
PROTOCOL_ADMIN_ROLE- Can manage Checkpoint Managers, Logic Upgraders, Checkpoint Executors, and Attester Managers
CHECKPOINT_MANAGER_ROLE- Can set and modify checkpoints in the firewall system
LOGIC_UPGRADER_ROLE- Can upgrade the logic of a specific proxy firewall
CHECKPOINT_EXECUTOR_ROLE- Can execute checkpoints in external firewall architectures
- Typically the smart contract that is going to be protected by the firewall
ATTESTER_MANAGER_ROLE- Can manage Trusted Attesters
TRUSTED_ATTESTER_ROLE- Can provide security attestations for transactions
- Responsible for validating and approving transactions based on security criteria
- Trusted Forwarder
- Inherited by the
SecurityValidatorfrom theERC2771Contextcontract - Can forward meta-transactions on behalf of the users to call
storeAttestationin theSecurityValidatorcontract
- Inherited by the
Off-Chain Roles (Forta Chain)
DEFAULT_ADMIN_ROLE- Can grant and revoke roles like Attesters, Authenticators, and Super Admins
- Can register controllers
- Assigned to the
FirewallRoleManagercontract deployer initially
CONTROLLER_SUPER_ADMIN_ROLE- Can manage Controller Config Admin and Controller Viewer roles
- Can update Controllers fields
CONTROLLER_CONFIG_ADMIN_ROLE- Can update Controllers fields
CONTROLLER_VIEWER_ROLE- Have read-only access to controller information
UPGRADE_ADMIN_ROLE- Can upgrade the
AttesterControllerscontract - Responsible for managing contract upgrades
- Can upgrade the
ATTESTER_ROLE- Can log detections, store batches, and delay transactions
AUTHENTICATOR_ROLE- Can add or remove target contracts from controller entities
Security Model and Trust Assumptions
The Forta Network's firewall and attestation system operates under several trust assumptions and security considerations:
- Role Integrity: All addresses with privileged roles are assumed to be trustworthy and not compromised. If compromised, these roles could significantly impact the system's security and potentially users' funds.
- Attestation Validity: The system relies on Trusted Attesters to provide valid and accurate security attestations for transactions. It is assumed that these attesters are not compromised and act honestly.
- Attestation Censorship: Transactions that have been delayed must be executable once the release time passes. It is assumed that attesters will generate attestations for these transactions immediately, without further analysis.
- Controller Management: Controller Super Admins and Config Admins are trusted to manage their controllers responsibly. Mismanagement on configurations and delay durations as well as incorrect public keys could lead to security vulnerabilities or system malfunctions.
- Upgrade Security: Upgrade Admins are assumed to only authorize legitimate and secure upgrades to the
AttesterControllerscontract. Malicious upgrades could compromise the entire system. - Checkpoint Integrity: Checkpoint Managers are trusted to set appropriate and secure checkpoints. Incorrectly configured checkpoints could lead to security vulnerabilities like denial of services.
- Firewall Configuration: Firewall Admins and Protocol Admins are trusted to maintain secure and appropriate configurations. Misconfiguration could lead to system-wide vulnerabilities.
- Bypass Mechanism: The system includes a bypass mechanism for attestation requirements. It is assumed that this bypass mechanism will only work off-chain.
- Attestation Timing: The off-chain attestation process takes time which can be crucial for certain protocol operations, particularly those involving integrations or token exchanges. It is expected that protocols will consider this delay, assess the associated risks, and implement measures to mitigate them as much as possible.
Users and developers interacting with the Forta Network's firewall and attestation system should be aware of the above trust assumptions and the potential consequences if they are violated. The security of the system heavily relies on the integrity of the privileged roles and the correct implementation of the various security mechanisms.
Critical Severity
Attestation Validation Bypass in SecurityValidator
Users interacting with contracts utilizing a Forta Firewall must provide an attestation that serves as proof that the transaction has been simulated and validated for non-malicious intent. These attestations can only be created by trusted attesters. The SecurityValidator contract allows these attesters to store attestations via the storeAttestation and saveAttestation functions. The former persists the attestation in storage until it is consumed, while the latter uses transient storage to conduct the attestation and execution within the same transaction.
During checkpoint executions, the getCurrentAttester function retrieves the current attester by checking transient storage and validating if the attester is trusted using _isTrustedAttester. However, if the getCurrentAttester function returns the zero address, the transaction does not revert, and the attester is then queried from the contract's storage in the executeCheckpoint function. The critical issue here is that the attester retrieved from storage is not validated as trusted, potentially allowing any entity to use storeAttestation to create fraudulent attestations that could enable malicious transactions to pass as valid.
Importantly, this vulnerability only manifests when the attestation contains a single executionHash. If the attestation includes multiple execution hashes, the transaction reverts. Despite this limitation, the issue remains critical as it still provides a potential avenue for bypassing the security checks which is the purpose of the firewall.
Consider implementing a validation check to ensure that any attester retrieved from storage is indeed trusted, preventing unauthorized entities from bypassing the security mechanism.
Update: Resolved in pull request #9.
High Severity
Attestations Lack Sufficient Information for Accurate Verification
Attestations include an ordered list of execution hashes representing the sequence of checkpoints to be executed. These execution hashes are derived from checkpoint hashes, constructed by hashing the sender's address, the firewall's address, the function selector, and the quantized reference argument. However, this information alone is insufficient to determine whether a function call poses a risk to the protocol. Specifically:
- Attestations lack the context of what the other arguments to the function are and only consider the reference argument.
- Attestations do not consider additional function calls that might modify the state without triggering checkpoints.
For the reasons mentioned above, an attacker could request an attestation for harmless function calls that do not represent their true intent for on-chain execution. After obtaining the signed attestation, they could craft a malicious transaction by modifying any non-reference arguments and adding additional function calls that change the state without triggering checkpoints. This allows them to use the original signed attestation to execute the malicious transaction.
Consider providing additional context within attestations, allowing attesters to perform analyses in an environment that more accurately mimics actual on-chain execution. This approach would help prevent attackers from submitting modified transactions that produce outcomes significantly different from those attested.
Update: Partially resolved in pull request #10 and pull request #30. Attestations still do not account for additional function calls that could modify the state without triggering checkpoints. The Forta team stated:
Added support for byte range configuration larger than 32 to enable usage of more bytes from call data as the checkpoint hash input. Added more support for large call data ranges with a second PR. We will not support stateful checkpoint hash computation in the library since it can be very challenging to generalize but it will be possible for the user/developer to support that easily by, e.g., getting the caller's asset balance from a mapping and using it in the checkpoint hash computation.
Medium Severity
Limited Composability Due to Front-End Dependent Attestation Process
Contracts using Firewall-based contracts require attestations from trusted entities (Attesters) to ensure that the transactions are not malicious. If an attestation is not present, the transaction will revert. Currently, users can only obtain these attestations through the Dapp's front-end, which forwards the intended transaction to the attester before constructing a transaction for the user to sign, either with or without an embedded attestation, depending on how it is stored.
This design restricts composability as users cannot perform complex interactions outside the front-end’s capabilities, such as multi-contract operations (e.g., arbitrage), due to the inability to obtain the necessary attestation. This limitation may deter users from using the Dapp and prevent its integration with other Dapps.
Consider implementing a mechanism that allows users to obtain attestations without relying solely on the Dapp's front-end.
Update: Acknowledged, not resolved. The Forta team stated:
The
SecurityValidatorcontract and Firewall implementations do not suggest how attestations can be created and delivered off the chain. While dApp integration is one way to deliver them, there are other ways to deliver such as writing a JSON-RPC proxy to do front running, building a custom API to do transaction forwarding, or constructing EIP-7702 transactions in the near future.Any contract that depends on off-chain services needs to have this kind of flow if the contract does not implement a request-response base like Chainlink Functions, which we have not been in favor of and consider to be more complex from other angles. Multi-contract operations are feasible since an attestation contains all checkpoint execution hashes that will be produced by the
executeCheckpoint()call of any integrator in the same transaction. Delivering the attestation in the same transaction brings some friction since it requires a function likeattestedCall()to first save the attestation in the first contract call. We have developed the two-transaction pattern to let a proxy front-run by saving an attestation first before forwarding the user transaction. And later, when introduced, EIP-7702 will allow enabling the trusted origin pattern implemented in the Firewall, which bypasses the checkpoint execution in case thetx.originis trusted - saving attestations might not be necessary in most cases if this approach gets adopted.However, regardless of the above, it is hard to deny that contract-to-contract integrations can be impacted from the contract's side which does not integrate Forta Firewall. We have been aware of this issue since the initial phases of this design and are actively thinking of ways to reduce this impact. This is unfortunately not a kind of problem we can solve as part of this audit.
Incomplete Validation of Attestations in SecurityValidator
The SecurityValidator contract relies on attestations, which are essentially ordered arrays of hashes generated during each checkpoint execution. These attestations should validate the entire execution path, guiding the attester through a specific sequence of actions.
However, the current implementation allows attestations to be partially executed which contradicts the intended purpose that they should only be valid if the entire execution path is followed. This flaw could be exploited by malicious users. For instance, an attacker could execute a transaction designed to exploit a protocol but include a final step that returns funds to the protocol, misleading Forta validation mechanisms into believing that the transaction is non-malicious. Once users have the signed attestation, they can submit the transaction on-chain while omitting the final return step, allowing them to steal funds by bypassing the security check.
Consider enforcing stricter validation of the attestation process to ensure that all checkpoints are properly executed. For example, consider incorporating a mandatory call to the validateFinalState method to confirm that the entire attestation process has been completed as expected.
Update: Acknowledged, not resolved. The Forta team stated:
We acknowledge that it is possible to save an attestation that contains hashes A, B, and C, then just to A and B, and exit the transaction successfully. Euler's EVC can schedule a final
validateFinalState()call since it sits in front of every call to every vault in the same transaction if the user uses an attestation in theEVC.batch()call. In the case of a different consumer:
- The integrated contract needs to receive the user call through a trusted contract and do something similar to
_msgSender()to deduce the actual account to operate on. This brings more integration friction and reduces the chances of Forta Firewall adoption.- We can put
validateFinalState()at the end ofattestedCall()but if any contract uses OpenZeppelin Multicall, then a user can elect to send an attested call that does not finally validate.- If the attestation is delivered from a different integrator, then another integrator that is in the middle of the call stack can have no guarantees that the first contract in the call stack will do
validateFinalState(). Currently, this seems hard to solve but we are not too worried about this issue.For state and code at a given time/block range, we expect C to be consumed if A and B are consumed. If the state or code changes then the checkpoint hashes should also change. We are doing our best to ensure this in the Firewall base implementation. We will try to deliver improvements to the Firewall implementation in the next iterations and are giving every user the freedom to write their own firewall and checkpoints, with or without the Forta Firewall contract library.
Attested Values May Significantly Differ From On-Chain Execution
Attestations include an ordered list of execution hashes, representing the sequence of checkpoints to be executed. For each checkpoint, a hash is constructed by hashing the sender's address, the firewall's address, the function selector, and the quantized reference argument. Attesters rely on these checkpoint hashes, meaning their analyses only consider the quantized reference argument instead of the actual value that will be used on-chain. There are two scenarios where the attested value of the reference argument may differ significantly from the actual value provided by the user during on-chain execution:
- When the
quantizefunction performs downward quantization. - When the
AlwaysActiveactivation mode is used for a checkpoint.
On the one hand, the quantize function in the Quantization library zeroes out the least significant bits of a given value. In the worst case, it converts values of the form 2 * 256^n - 1 into 256^n, resulting in a precision loss of nearly a factor of two. An attacker could exploit this behavior by obtaining valid attestations for transactions that are dangerous to the protocol, as the attester is only evaluating half of the real reference value that will be sent on-chain. For instance, if the reference argument represents a withdrawal amount, an attacker could request an attestation to withdraw their entire balance, which the attester would validate. However, when submitting the actual on-chain transaction, the attacker could double the withdrawal amount, exceeding their balance while still using the original attestation due to the quantized values being checked.
On the other hand, when the AlwaysActive activation mode is set for a checkpoint, the reference value is replaced with the constant 1 to construct attestations. Consequently, attestations for this checkpoint will permit execution with any reference argument provided by the user when interacting with the protocol, as the attester always sees the constant value 1, regardless of the actual value supplied. As a result, attackers have even more freedom than in the previous case to alter the reference argument with respect to the attested value.
Consider performing upward quantization instead of downward to ensure that attesters' analyses favor the protocol. In addition, consider using the actual reference argument instead of replacing it with a constant when the checkpoint activation mode is set to AlwaysActive.
Update: Resolved in pull request #14.
Lack of Challenge Mechanism for Incorrect Transaction Delays
Currently, the system does not offer users a method to challenge delays imposed on transactions. If a valid transaction is mistakenly flagged as harmful, and a long delay is set by an attester, users are subjected to extended waiting times with no recourse to dispute the delay. Moreover, a malicious attester could use the logDetection function to arbitrarily delay transactions, potentially causing actions such as forcing liquidations in lending protocols. Legitimate attesters have no mechanism to revoke such delays, increasing the risk of exploitation.
Consider introducing a process to allow users to contest transaction delays and enabling legitimate attesters. This could involve a proof submission system for valid transactions or a more structured review process for disputed delays. Doing this will help remove unjustified delays and mitigate the risks
Update: Resolved in pull request #21. Admins can now update the release time for transactions that were incorrectly delayed.
Inconsistent Delay Mechanisms in Inbox and AttesterControllers Contracts
Both the Inbox and AttesterControllers contracts allow for delaying transactions. However, they do so in inconsistent ways which could lead to security, auditability, and efficiency issues.
In the Inbox contract, the delayFFR function allows attesters to delay transactions with minimal validation. In contrast, the logDetection function within AttesterControllers enforces stricter checks, such as risk logging and the application of predefined delay durations.
This inconsistency carries several issues:
- Fragmented Logic: The different delay mechanisms create fragmented logic, leading to inconsistent behavior between contracts.
- Potential Transaction Censorship: The
delayFFRfunction allows arbitrary future timestamps which may override controller settings and result in transactions being delayed indefinitely. - Bypass of Controls: Attesters can bypass the stricter delay controls enforced by
logDetection, undermining the integrity of the delay system. - Auditability Concerns: The lack of logging in
delayFFRreduces transparency, making it difficult to trace the rationale for transaction delays. - Redundant Identity Checks and Logs: The
logDetectionfunction callsdelayFFR, which redundantly checks whether the caller is an attester, even though this validation has already been performed. Furthermore, the logging of both theFFRDelayedandTxDelayedevents in the same transaction could be redundant.
To address the aforementioned issues, consider making the logDetection function the sole entry point for delaying transactions to ensure consistent handling of delays. In addition, removing redundant identity checks and enhancing logging would improve the system’s security, efficiency, and auditability.
Update: Partially resolved in pull request #21. Compared to using logDetection, delaying transactions via delayFFR directly still omits logging detection details and bypasses the delay durations configured for each controller in the AttesterControllers contract. The team stated:
The logging of both the FFRDelayed and TxDelayed events in the same transaction is not redundant, because delayFFR on inbox can be called by different AttesterControllers instances. So
FFRDelayedevent on AttesterControllers is required to extend data about which controller ids was affected by this delay. Inbox doesn't care about controllers just about FFRs and their delays.
Potential for Transaction Censorship via Repeated Delay
The system allows extending the release time of a transaction through multiple invocations but prevents shortening the delay. Specifically, by repeatedly calling the delayFFR function in the Inbox contract or the logDetection function in the AttesterControllers contract with the same data, an attester can continually push back the release time. This creates a risk of transaction censorship, as an attester could indefinitely delay the processing of a transaction.
To mitigate this risk, consider limiting the number of times the release time for a given transaction can be updated. Alternatively, consider imposing a maximum delay period to ensure that the release time cannot be extended indefinitely.
Update: Resolved in pull request #21. Admins can now update the release time for transactions that were incorrectly delayed, and a maximum delay duration has been introduced.
Low Severity
Insufficient Code Coverage
The (Forta Firewall and Forta Attester Controller) codebases exhibit insufficient test coverage for branches and functions (currently under 60%). This level of coverage may leave critical portions of the code untested, potentially leading to undetected vulnerabilities.
Consider adding more unit tests to increase the test coverage to above 95%, adhering to the best practices for software security and reliability. In addition, consider integrating test coverage tracking into the repository's Continuous Integration process for ongoing quality assurance.
Update: Acknowledged, not resolved.
SecurityValidator Does Not Inherit From ISecurityValidator
The SecurityValidator contract does not inherit from the ISecurityValidator interface, which could lead to the interface and its implementation going out of sync.
Consider making the SecurityValidator contract inherit from the ISecurityValidator interface.
Update: Resolved in pull request #4.
Stored Attestations Cannot Be Loaded After Previous Attestations Have Been Used
To execute checkpoints in the SecurityValidator contract, attestations can either be saved directly to transient storage for use within the same transaction or saved into persistent storage for use in future transactions. However, while the first method allows new attestations to be saved after previous attestations have been used, the second method does not. As a result, if a user wanted to start a transaction by loading attestations with saveAttestation, then consume those attestations, and then load more attestations from storage to keep executing checkpoints (which must have been loaded in a previous transaction using storeAttestation), they would not be able to do so.
Consider allowing attestations to be loaded from persistent storage even after previous attestations have been used, ensuring consistency between both methods of saving attestations.
Update: Resolved in pull request #3. The Forta team stated:
Applied a fix to support stored attestations after saved attestations are consumed.
Implemented Methods Are Not Part of the Interface
Several contracts in the codebase implement public functions that are not part of their interface. As a result, third parties interacting with these contracts via their interface will be unable to call those functions. In particular:
- The
ISecurityValidatorinterface does not include thevalidateFinalStateandexecutionHashFromfunctions. - The
IFirewallinterface does not include theattestedCallfunction.
Consider adding the missing functions to the interfaces to make it easier for third parties to interact with the contracts.
Update: Resolved in pull request #4.
Missing Error Messages in require Statements
In FirewallPermissions.sol, all modifiers are missing error messages in their require statements.
Consider including specific, informative error messages in require statements to improve overall code clarity and facilitate troubleshooting whenever a requirement is not satisfied.
Update: Resolved in pull request #15.
Incomplete Docstrings
In AttesterControllers.sol, multiple instances of incomplete docstrings were identified:
- The
ffrIdsparameter of theStoreBatchevent is not documented. - The
encryptedDetailsparameter of theDetectionevent is not documented. - The
configurationparameter of theregisterControllerfunction is not documented. - The
logExceedsThresholdanddelayExceedsThresholdparameters of thelogDetectionfunction are not documented.
Consider thoroughly documenting all functions/events (and their parameters or return values) that are part of a contract's public API. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).
Update: Resolved at commit 717626f.
Missing Docstrings
Throughout the codebase, multiple instances of missing docstrings were identified. Some notable examples include:
- In
Whitelist.sol, theaddToWhitelistfunction - In
Inbox.sol, theTxDelayedevent - In
AttesterControllers.sol, thegrantSuperAdminRolefunction - In
Firewall.sol, theSecurityConfigUpdatedevent - In
Firewall.sol, theSupportsTrustedOriginevent
Consider thoroughly documenting all functions (and their parameters) that are part of any contract's public API. Functions implementing sensitive functionality, even if not public, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).
Update: Resolved at commit 717626f.
Missing Zero Address Checks
When assigning an address from a user-provided parameter, it is crucial to ensure that the address is not set to zero. Setting an address to zero is problematic because this action has special renounce semantics.
Throughout the codebase, multiple instances of assignment operations missing zero address checks were identified:
- The
firewallAccessassignment operation within the contractFirewallPermissionscontract - The
newImplementationassignment operation within theProxyFirewallcontract - The
roleManagerAddressassignment operation within theAttesterControllerscontract - The
inboxAddressassignment operation within theAttesterControllerscontract
Consider adding a zero address check before assigning a value to a state variable.
Update: Resolved in pull requests #18 and #16.
Locked ETH in Contract
The ProxyFirewall contract contains a receive fallback that allows users to send ETH to the contract. However, it lacks a withdrawal mechanism, creating a potential risk of permanently locking any ETH sent to the contract. While accidental ETH transfers to this contract are unlikely, the possibility still exists.
Consider removing the receive fallback unless the functionality of sending ETH to the ProxyFirewall contract is needed. Alternatively, implement a mechanism to allow the withdrawal of the received ETH.
Update: Resolved in pull request #18. The Forta team stated:
We want the proxy firewall to be plugged in as an intermediary logic contract between, e.g., an ERC1967 Proxy and a logic contract. We added receive function to suppress Solidity warnings before and now corrected that to use the fallback logic. However, we realized that the checkpoint configuration does not know how to work with ether amounts in
msg.value. So, we adapted the configuration logic to takemsg.valueas the reference value whenmsg.sigor ref range is zero.
Unsafe Use of DEFAULT_ADMIN_ROLE
The FirewallAccess contract implements access control. The DEFAULT_ADMIN_ROLE role is given to the default admin address when deploying the contract. The admin role of the FIREWALL_ADMIN_ROLE role is the DEFAULT_ADMIN_ROLE role as it is not explicitly assigned in the code. Since this role has special privileges associated with it, such as the ability to grant or revoke the FIREWALL_ADMIN_ROLE role or the DEFAULT_ADMIN_ROLE role many times, further security measures should be taken.
According to the comments in OpenZeppelin's AccessControl.sol file as well as the official documentation, the recommendation is to use the AccessControlDefaultAdminRules contract. This contract implements the following risk mitigations on top of AccessControl:
- Only one account holds the
DEFAULT_ADMIN_ROLErole since deployment until it is potentially renounced. - Enforces a two-step process to transfer the
DEFAULT_ADMIN_ROLErole to another account. - Enforces a configurable delay between the two steps, with the ability to cancel before the transfer is accepted. The delay can be changed by scheduling.
- It is not possible to use another role to manage the
DEFAULT_ADMIN_ROLErole.
Consider using AccessControlDefaultAdminRules in order to safely manage the DEFAULT_ADMIN_ROLE role.
Update: Acknowledged, not resolved. The Forta team stated:
We do not feel that granting or revoking top-level roles many times is a problem. So, it feels like
AccessControlDefaultAdminRulesmight add needless complexity to the overall firewall management experience.
Events Emitted Regardless of Whitelist Status Change
The addToWhitelist and removeFromWhitelist functions of the Whitelist contract emit events even when there is no actual change to the whitelist. For instance, if removeFromWhitelist is called with a value that is not in the whitelist, it still emits the WhitelistReduced event, which is misleading because the whitelist remains unchanged. Similarly, addToWhitelist emits the WhitelistExtended event even when the value is already whitelisted.
Consider adding a condition to only emit events when the whitelist is effectively modified. This will ensure that the WhitelistReduced and WhitelistExtended events are only triggered when an address is truly added to or removed from the whitelist, avoiding potential confusion in event logs and monitoring.
Update: Resolved at commit 0d5c3d0.
Lack of Maximum Delay for Suspicious Transactions
The system currently does not impose a maximum limit on the delay duration for transactions flagged as suspicious. This lack of a constraint allows any actor with the DEFAULT_ADMIN_ROLE, controller admins, or an attester calling delayFFR directly, to set arbitrary delay values without restriction.
Consider introducing a maximum delay (e.g., MAX_DELAY). This would help mitigate potential abuses, such as censorship or DoS scenarios, by limiting the extent to which delays can be manipulated.
Update: Resolved at commit c944625.
Multicall Functionality in Some Firewalls Is Inefficient and Adds Limited Value
Multicall functionality is added to all proxy variants to enable saving attestations and executing protocol logic in the same transaction. While effective for the InternalFirewall, this approach is less suited for the ExternalFirewall and ProxyFirewall contracts.
Considering the ExternalFirewall contract, since it does not contain protocol logic, the multicall feature will not serve its main purpose. In this case, the primary benefit of this feature is the ability to configure multiple checkpoints in one transaction. However, this introduces inefficiencies due to unnecessary delegate calls. A more optimized solution would be a custom batch function to set checkpoints, reducing redundant calls and gas costs.
In the case of the ProxyFirewall, using multicall leads to significant gas inefficiencies as delegatecalls from the ProxyFirewall target the front proxy instead of the ProxyFirewall itself. This results in extra delegatecalls being triggered, which is inefficient and introduces potential risks. A better approach would be to implement the multicall functionality in the front proxy, avoiding unnecessary calls and maintaining the intended security model.
Consider removing the multicall feature from the ExternalFirewall and ProxyFirewall contracts and replacing it with a solution tailored to their specific use cases.
Update: Resolved in pull request #17.
Stored Attestations Cannot Be Used By Smart Contract Accounts
The storeAttestation function in the SecurityValidator contract allows users to save attestations in persistent storage for use in future transactions. These attestations are saved in a mapping, in which the key corresponds to the sender of the transaction given by the _msgSender function. However, when attestations are finally loaded from persistent storage to transient storage, the tx.origin address is used to access the mapping.
When users interact with the protocol using any type of smart contract account (such as Safe, ERC-4337 smart accounts, etc.) instead of EOAs, and call the storeAttestation function, the attestations are saved in the mapping under the smart account's address. However, when the users submit their transactions and the executeCheckpoint function is called, the protocol searches for the necessary attestations in the mapping under the tx.origin address, which corresponds to the EOA that initiated the transaction, not the users' smart contract accounts. As a result, the attestations are not found and the transaction reverts.
Consider avoiding the use of tx.origin to access attestations in the mapping, thereby allowing smart contract account users to interact with the protocol in the same way as EOA users.
Update: Acknowledged, not resolved. The Forta team stated:
The reason for using
tx.originis to avoid weird collisions across users when their EOAs need to use attestations that start with the same execution hash. ERC-4337 supports batching thanks to bundlers so any attestation should ideally be provided as part of the sameUserOperationbundle, in the same tx. The use of dual transaction (attestation tx + user tx) is not considered as a use case for ERC-4337 smart accounts and we consider this issue to be safe to ignore.
Notes & Additional Information
Redundant Functionality Increases Contract Size and Complexity
The inclusion of additional functions in the Firewall contract to handle function signatures aims to enhance the developer experience. However, it significantly increases the contract's size without providing substantial value. This increase in size may also reduce the margin available for contracts that inherit from it, potentially bringing the contract closer to the 24576 B limit imposed by EIP-170.
To maintain a more streamlined and elemental implementation, consider only retaining the methods that work with selectors directly.
Update: Resolved in pull request #11.
Code Clarity
Throughout the codebase, multiple opportunities for improving code quality were identified:
- The
deletekeyword is being used as if it were a function even though it is not. Consider removing the parentheses and adding a space between the keyword and the mapping. - The
_tryInitAttestationFromStoragefunction should returnfalseinstead of reverting when no stored attestation is found. This would be to maintain consistency with the rest of the function logic and its return type.
Consider incorporating these changes to enhance code clarity and readability.
Update: Resolved in pull request #5. The Forta team stated:
We addressed the second issue in the fix for L-03.
Missing Named Parameters in Mapping
Since Solidity 0.8.18, developers can utilize named parameters in mappings. This means mappings can take the form of mapping(KeyType KeyName? => ValueType ValueName?). This updated syntax provides a more transparent representation of a mapping's purpose.
Throughout the codebase, multiple instances of mappings without named parameters were identified:
- The
checkpointsstate variable of theFirewallcontract - The
attestationsstate variable of theSecurityValidatorcontract - The
whiteliststate variable in theWhitelistcontract
Consider adding named parameters to mappings in order to improve the readability and maintainability of the codebase.
Update: Resolved in pull request #6 and at commit 180a7cb.
Non-Explicit Imports Are Used
The use of non-explicit imports in the codebase can decrease code clarity and may create naming conflicts between locally-defined and imported variables. This is particularly relevant when multiple contracts exist within the same Solidity file or when inheritance chains are long.
Within the codebase, multiple instances of global imports were identified:
Following the principle that clearer code is better code, consider using the named import syntax (import {A, B, C} from "X") to explicitly declare which contracts are being imported.
Update: Resolved in pull request #7 and at commit 8c38c9d.
State Variable Visibility Not Explicitly Declared
Throughout the codebase, multiple instances of state variables lacking an explicitly declared visibility were identified:
-
In
Inbox.sol: -
In
Registry.sol: -
In
SecurityValidator.sol:
For improved code clarity, consider always explicitly declaring the visibility of state variables, even when the default visibility matches the intended visibility.
Update: Resolved in pull request #8 and at commit 95e2a94.
Unused Named Return Variables
Named return variables are a way to declare variables that are meant to be used within a function's body for the purpose of being returned as that function's output. They are an alternative to explicit in-line return statements.
- In
Firewall.sol, all the named return variables of thegetFirewallConfigfunction are unused. - In
AttesterControllers.sol, thecontrollerIdreturn variable of theregisterControllerfunction is unnecessarily returned.
Consider either using or removing any unused named return variables and eliminating unnecessary explicit return statements.
Update: Resolved in pull request #17. Line 185 was removed by mistake in the PR, but later reverted.
Gas Optimizations
Throughout the codebase, multiple opportunities for optimizing gas usage were identified:
-
Loop Optimizations:
- Replace the post-increment operator (
i++) with the pre-increment (++i) operator to save gas where the return value of the expression is not used: - Accessing the length of arrays within loop conditions or bodies results in unnecessary storage or memory reads during each iteration, increasing gas consumption. There are some loops within the codebase that could benefit from caching the length of the array being iterated over:
- The length of
executionHasheswithin the_initAttestationfunction - The length of
controllerIdswithin thelogDetectionfunction - The length of
targetContracts[entityId]within theremoveTargetContractfunction (also accessed within the loop body)
- The length of
- Replace the post-increment operator (
-
Custom Errors:
-
Hashing Optimizations:
- In line 99 of
Registry.sol, the_targetContractis rehashed on every iteration instead of reusing the already computed hash.
- In line 99 of
-
Optimizable State Reads:
- In several places, unnecessary storage reads can be optimized by caching values or changing storage references to memory:
Firewall.sol: Change thecheckpointparameter in_checkpointActivatedand thecheckpointvariables ingetCheckpoint, and in both versions of_secureExecution(line 309 and line 317) fromstoragetomemory. Neither function modifies the checkpoint, so they can cache and reference it from memory instead of accessing it from storage.Inbox.sol: TheinboxReleaseTimestampsmapping in thedelayFFRfunction is being accessed twice for the same key.AttesterControllers.sol: The_controllersIdCounterstate variable in theregisterControllerfunction is being read from storage twice: once implicitly, when its value is updated, and once for returning it.
Update: Partially resolved in pull request #27.
Redundant Code
Throughout the codebase, multiple instances of redundant code were identified:
- The
getControllerByIdfunction is redundant because the publiccontrollersstate variable already has an automatically included getter. - The
msgSenderlocal variable in thestoreAttestationfunction is only used once, so it could be removed in favor of a direct call to the_msgSender()function. - The
delayFFRfunction is only used once, so it could be in-lined withinlogDetectionto reduce contract size and avoid a redundant call to theonlyAttestermodifier.
Consider eliminating the redundant code to enhance readability and efficiency.
Update: Acknowledged, not resolved.
Misleading Documentation
Throughout the codebase, multiple instances of misleading documentation were identified:
- The
SecurityValidatorcontract is described as being intended for attesters and contracts. However, it is more accurate to say that it is meant for attesters and firewalls. - This comment suggests that the proxy firewall implements
upgradeToAndCalland theUUPSUpgradeablecontract implementsupgradeNextAndCall, when in fact, it is the other way around. - The
@noticecomment that contains the firewall description has an incomplete sentence. - This comment suggests that the
_updateFirewallConfigfunction initializes the firewall configuration for the first time. However, this function is called every time the configuration is updated, not just the first time. - This NatSpect comment for the
delayFFRfunction incorrectly describes it as an event. - The
ExternalFirewallNatSpec is outdated: thewithCheckpointmodifier no longer exists, and theexecuteCheckpointandsetCheckpointfunctions now receive additional arguments. - The
InternalFirewallandProxyFirewallNatSpecs are outdated: the_secureExecutionandsetCheckpointfunctions now receive additional arguments.
Consider correcting the aforementioned comments to improve the overall clarity and readability of the codebase.
Update: Resolved in pull request #26.
Typographical Error
Throughout the codebase, an instance of a typographical error was identified:
- At the end of line 33 of
ProxyFirewall.sol, the word "point" is repeated at the start of the next line.
Consider fixing the typographical error to improve the readability of the codebase.
Update: Resolved in pull request #23.
File and Contract Names Mismatch
The RoleManager.sol file name does not match the FirewallRoleManager contract name.
To make the codebase easier to understand for developers and reviewers, consider renaming the file names to match the contract names.
Update: Resolved at commit 99231f9.
Floating Pragma
Most contracts use Solidity ^0.8.25 floating pragma. Using a fixed pragma for other contracts ensures consistent compilation and behavior across environments, while maintaining flexibility where needed.
Consider using fixed pragma (e.g., pragma solidity 0.8.25;) for all contracts except InternalFirewall and CheckpointExecutor. These two should keep floating pragma as they have been designed to be inherited, enhancing integration flexibility and developer experience.
Update: Acknowledged, not resolved.
Lack of Security Contact
Providing a specific security contact (such as an email or ENS name) within a smart contract significantly simplifies the process for individuals to communicate if they identify a vulnerability in the code. This practice is quite beneficial as it permits the code owners to dictate the communication channel for vulnerability disclosure, eliminating the risk of miscommunication or failure to report due to a lack of knowledge on how to do so. In addition, if the contract incorporates third-party libraries and a bug surfaces in those, it becomes easier for their maintainers to contact the appropriate person about the problem and provide mitigation instructions.
Throughout the codebase, none of the contracts include a security contact.
Consider adding a NatSpec comment containing a security contact above each contract definition. Using the @custom:security-contact convention is recommended as it has been adopted by the OpenZeppelin Wizard and the ethereum-lists.
Update: Acknowledged, not resolved.
Interfaces Should Be Defined in Separate Files
Several files in the codebase, such as SecurityValidator.sol and Firewall.sol, contain both interfaces and contract definitions. This practice can reduce modularity and clarity as it mixes contract logic with interface definitions, which are conceptually distinct.
Consider moving each interface into its own dedicated file. This separation enhances maintainability, allows interfaces to be referenced more easily, and aligns with best practices for clean code architecture. Moreover, consider relocating the NatSpec comments from the functions in the contracts to the corresponding interfaces and using the @inheritdoc tag in the contracts to inherit these comments. NatSpec comments are more beneficial in interfaces since developers interacting with the contracts typically rely on them for reference.
Update: Resolved in pull request #29. The team stated:
I rearranged interfaces for easier and more granular import. I generally agree with the suggestion related to
@inheritdocbut I don't find it particularly useful in this case. I think that the current comments are more valuable when they sit next to the implementation. I think that this practice might be more useful for ERC interfaces that have wider reuse.
Naming Issues
Throughout the codebase, multiple instances of inaccurately named errors and state variables declarations were identified:
InvalidThresholdType: This error name is not entirely accurate and could be misleading. The error is thrown when thecheckpoint.activationis not one of the expectedActivationtypes. It is not specifically about an invalid threshold type, but rather an invalid or unexpected activation type. A more accurate name could beInvalidActivationTypeorUnexpectedActivationType.Checkpoint: The term "checkpoint" is overloaded in the codebase, leading to potential confusion. While theCheckpointstruct in theFirewallcontract represents configuration settings for security checks, theexecuteCheckpointfunction inSecurityValidatorperforms runtime verification using a hash that is unrelated to theCheckpointstruct. Renaming the struct toCheckpointConfigmay better distinguish it from the checkpoint hash used for verification.
Consider addressing the aforementioned naming issues to improve the readability of the codebase.
Update: Partially resolved in pull request #25. The team stated:
Agree about the first one.
Lack of License Specification
The use of // SPDX-License-Identifier: UNLICENSED in the code indicates that no explicit license has been specified. This has significant implications:
-
Restricted Usage by Default: Without a license, others are not granted permission to copy, modify, or distribute the code. Copyright laws automatically apply, protecting the code, and any use without permission could be considered infringement.
-
Enforceability Concerns: Even though the code is legally protected, the absence of a clear license might make it harder to enforce these rights. Some users may mistakenly believe that the code is open for use due to the lack of a license statement.
-
No Attribution Requirements: Since no license is provided, there are no conditions like attribution requirements. However, others are not permitted to use the code at all without explicit permission from the author.
To ensure control over how your code is used, consider specifying a license. A license provides legal protection by establishing clear usage terms which helps prevent misuse and encourages collaboration.
Update: Resolved in pull request #24.
Unnecessary Use of AccessControl in the Whitelist Contract
The Whitelist contract currently only uses the DEFAULT_ADMIN_ROLE role. If the contract intends to have only one role and one account managing this role, consider switching to Ownable to simplify the implementation. Using AccessControl for a single role can be unnecessarily complex and inefficient.
In addition, for enhanced security, consider implementing Ownable2Step. This would help prevent potential errors during ownership transfers, such as transferring ownership to an incorrect address or to contracts that cannot interact with the permission system.
Update: Acknowledged, not resolved.
Inconsistent Order Within Contracts
Throughout the codebase, multiple instances of contracts having inconsistent ordering of functions were identified:
- The
ProxyFirewallcontract inProxyFirewall.sol - The
AttesterControllerscontract inAttesterControllers.sol - The
Registrycontract inRegistry.sol
To improve the project's overall legibility, consider standardizing ordering throughout the codebase as recommended by the Solidity Style Guide (Order of Functions).
Update: Resolved at commit 0f7bb68.
Lack of Indexed Event Parameters
Throughout the codebase, multiple instances of events not having indexed parameters were identified:
In Firewall.sol:
In Registry.sol:
To improve the ability of off-chain services to search and filter for specific events, consider indexing event parameters.
Update: Resolved in pull request #22 and at commit 6879e15. The Forta team stated:
Fixed for
Registry.sol.
Unused Errors
Throughout the codebase, multiple instances of unused errors were identified:
- In
Firewall.sol, theAlreadyInitializederror is unused. - In
Errors.sol, none of the errors are used.
To improve the overall clarity, intentionality, and readability of the codebase, consider either using or removing any currently unused errors.
Update: Resolved in pull request #21, and commit dd9fd18. The Forta team stated:
Fixed for
Errors.sol. Removed unused errors.
Unused Imports
Throughout the codebase, multiple instances of unused imports were identified:
- The import
import {ISecurityValidator, Attestation} from "./SecurityValidator.sol";imports unused aliasAttestationinExternalFirewall.sol. - The import
import {ISecurityValidator, Attestation} from "./SecurityValidator.sol";imports unused aliasAttestationinInternalFirewall.sol. - The import
import {ISecurityValidator, Attestation} from "./SecurityValidator.sol";imports unused aliasAttestationinProxyFirewall.sol. - The import
import "./Errors.sol";imports unused errors inAttesterControllers.sol. - The import
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";imports unused librarySignatureCheckerinSecurityValidator.sol. - The import
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";imports unused interfaceIAccessControlinFirewall
Consider removing unused imports to improve the overall clarity and readability of the codebase.
Update: Resolved in pull request #20.
Firewall Does Not Need To Be Initializable
The Firewall contract inherits from the Initializable contract, despite not using any of its functionality. Furthermore, the only firewall variant that requires initialization is ProxyFirewall.
To avoid unnecessarily increasing contract size, consider limiting the Initializable inheritance exclusively to the ProxyFirewall contract.
Update: Resolved in pull request #19.
Client Reported
Composability Risk from Divergent Trusted Attester Sets
When integrating with the firewall, DeFi protocols are allowed to define their own set of trusted attesters. However, all integrators are expected to use the default attester set managed by the Forta team unless explicitly configured otherwise. If two protocols interact within the same transaction and rely on mutually exclusive attester sets, this may result in a composability issue.
Consider adding documentation to explain this situation, and ensure that all protocols integrating with the Firewall include Forta's attesters in their list of trusted attesters.
Update: Acknowledged, will resolve.
Reference Parsing Fails for Variable-Size Arguments
The Forta Firewall's checkpoint mechanism uses fixed refStart and refEnd fields to locate reference values in transaction calldata. This approach has limitations for functions with variable-length arguments (e.g., arrays), leading to several issues:
- An attacker could potentially craft transactions with specially structured calldata to bypass security checks. By manipulating the position of the reference value within the variable-length data, they might be able to:
- Push the actual reference value out of the expected range.
- Insert a benign value at the expected position to pass the threshold check.
- The system may trigger false positives or false negatives due to misreading the reference value.
- This limitation restricts the types of functions that can be effectively protected by the firewall, especially those with complex parameter structures involving arrays or strings.
Consider adding support for functions with variable-length arguments in later iterations of the firewall.
Update: Acknowledged, will resolve.
Attestations Can Be Replayed
To prevent replayability, attestations include a validity deadline. However, users can still replay a transaction using the same attestation repeatedly, as long as it occurs before the deadline.
This opens the door for a malicious user to break down an attack into smaller, seemingly harmless steps. Once they receive an attestation for executing a single step, they can reuse it to execute the entire attack by resending the transaction multiple times.
This issue is currently deemed out of scope to prioritize gas efficiency, but potential solutions are being explored. Consider adding a nonce to the attestations so that they can only be used once.
Update: Acknowledged, not resolved.
Conclusion
The Forta Firewall is an innovative security solution designed to enhance the protection of DeFi protocols. This system allows for the screening of transactions before they are sent to the blockchain, using a checkpoint mechanism and security attestations to prevent potentially malicious actions.
During our audit, we found one critical- and one high-severity issues. While these findings are concerning, the overall design of the Firewall is robust. We were really pleased with how the Forta team handled the audit process. They were quick to respond, eager to discuss our findings, and showed a genuine commitment to making their product as secure as possible. Despite the issues we found, we are optimistic about the Forta Firewall's potential as it could be a game-changer for DeFi security.