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 utilizesSSTORE
to persist the attestation in contract storage. - At the beginning of the same transaction, using the
saveAttestation()
function, which leverages the more gas-efficientTSTORE/TLOAD
opcodes. 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
CheckpointExecutor
contract 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
SecurityValidator
from theERC2771Context
contract - Can forward meta-transactions on behalf of the users to call
storeAttestation
in theSecurityValidator
contract
- 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
FirewallRoleManager
contract 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
AttesterControllers
contract - 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
AttesterControllers
contract. 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
SecurityValidator
contract 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.origin
is 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
quantize
function performs downward quantization. - When the
AlwaysActive
activation 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
delayFFR
function 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
delayFFR
reduces transparency, making it difficult to trace the rationale for transaction delays. - Redundant Identity Checks and Logs: The
logDetection
function callsdelayFFR
, which redundantly checks whether the caller is an attester, even though this validation has already been performed. Furthermore, the logging of both theFFRDelayed
andTxDelayed
events 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
FFRDelayed
event 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
ISecurityValidator
interface does not include thevalidateFinalState
andexecutionHashFrom
functions. - The
IFirewall
interface does not include theattestedCall
function.
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
ffrIds
parameter of theStoreBatch
event is not documented. - The
encryptedDetails
parameter of theDetection
event is not documented. - The
configuration
parameter of theregisterController
function is not documented. - The
logExceedsThreshold
anddelayExceedsThreshold
parameters of thelogDetection
function 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
, theaddToWhitelist
function - In
Inbox.sol
, theTxDelayed
event - In
AttesterControllers.sol
, thegrantSuperAdminRole
function - In
Firewall.sol
, theSecurityConfigUpdated
event - In
Firewall.sol
, theSupportsTrustedOrigin
event
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
firewallAccess
assignment operation within the contractFirewallPermissions
contract - The
newImplementation
assignment operation within theProxyFirewall
contract - The
roleManagerAddress
assignment operation within theAttesterControllers
contract - The
inboxAddress
assignment operation within theAttesterControllers
contract
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.value
as the reference value whenmsg.sig
or 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_ROLE
role since deployment until it is potentially renounced. - Enforces a two-step process to transfer the
DEFAULT_ADMIN_ROLE
role 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_ROLE
role.
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
AccessControlDefaultAdminRules
might 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.origin
is 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 sameUserOperation
bundle, 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
delete
keyword 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
_tryInitAttestationFromStorage
function should returnfalse
instead 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
checkpoints
state variable of theFirewall
contract - The
attestations
state variable of theSecurityValidator
contract - The
whitelist
state variable in theWhitelist
contract
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 thegetFirewallConfig
function are unused. - In
AttesterControllers.sol
, thecontrollerId
return variable of theregisterController
function 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
executionHashes
within the_initAttestation
function - The length of
controllerIds
within thelogDetection
function - The length of
targetContracts[entityId]
within theremoveTargetContract
function (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_targetContract
is 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 thecheckpoint
parameter in_checkpointActivated
and thecheckpoint
variables ingetCheckpoint
, and in both versions of_secureExecution
(line 309 and line 317) fromstorage
tomemory
. Neither function modifies the checkpoint, so they can cache and reference it from memory instead of accessing it from storage.Inbox.sol
: TheinboxReleaseTimestamps
mapping in thedelayFFR
function is being accessed twice for the same key.AttesterControllers.sol
: The_controllersIdCounter
state variable in theregisterController
function 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
getControllerById
function is redundant because the publiccontrollers
state variable already has an automatically included getter. - The
msgSender
local variable in thestoreAttestation
function is only used once, so it could be removed in favor of a direct call to the_msgSender()
function. - The
delayFFR
function is only used once, so it could be in-lined withinlogDetection
to reduce contract size and avoid a redundant call to theonlyAttester
modifier.
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
SecurityValidator
contract 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
upgradeToAndCall
and theUUPSUpgradeable
contract implementsupgradeNextAndCall
, when in fact, it is the other way around. - The
@notice
comment that contains the firewall description has an incomplete sentence. - This comment suggests that the
_updateFirewallConfig
function 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
delayFFR
function incorrectly describes it as an event. - The
ExternalFirewall
NatSpec is outdated: thewithCheckpoint
modifier no longer exists, and theexecuteCheckpoint
andsetCheckpoint
functions now receive additional arguments. - The
InternalFirewall
andProxyFirewall
NatSpecs are outdated: the_secureExecution
andsetCheckpoint
functions 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
@inheritdoc
but 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.activation
is not one of the expectedActivation
types. It is not specifically about an invalid threshold type, but rather an invalid or unexpected activation type. A more accurate name could beInvalidActivationType
orUnexpectedActivationType
.Checkpoint
: The term "checkpoint" is overloaded in the codebase, leading to potential confusion. While theCheckpoint
struct in theFirewall
contract represents configuration settings for security checks, theexecuteCheckpoint
function inSecurityValidator
performs runtime verification using a hash that is unrelated to theCheckpoint
struct. Renaming the struct toCheckpointConfig
may 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
ProxyFirewall
contract inProxyFirewall.sol
- The
AttesterControllers
contract inAttesterControllers.sol
- The
Registry
contract 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
, theAlreadyInitialized
error 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 aliasAttestation
inExternalFirewall.sol
. - The import
import {ISecurityValidator, Attestation} from "./SecurityValidator.sol";
imports unused aliasAttestation
inInternalFirewall.sol
. - The import
import {ISecurityValidator, Attestation} from "./SecurityValidator.sol";
imports unused aliasAttestation
inProxyFirewall.sol
. - The import
import "./Errors.sol";
imports unused errors inAttesterControllers.sol
. - The import
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
imports unused librarySignatureChecker
inSecurityValidator.sol
. - The import
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
imports unused interfaceIAccessControl
inFirewall
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.