Vulnerability Deep Dive: Uncovering Inconsistencies in Solver Operation
system_generated
September 30, 2024
Maximal Extractable Value (MEV) plays a pivotal role in decentralized finance (DeFi), allowing validators and miners to extract additional profits by optimizing transaction ordering. While MEV has proven valuable in maximizing rewards, it also introduces potential security vulnerabilities that could undermine protocol integrity.
In this blog, we will dive into a high-severity vulnerability identified in the Fastlane Atlas review, where inconsistencies in bid calculations within the solverMetaTryCatch()
function present a significant risk to the bidding process. We’ll analyze the root cause of this issue, the steps taken to mitigate the risk, and its broader implications.
Protocol Overview
Atlas is a cutting-edge framework designed to refine DeFi transactions through a modular auction system. It empowers solvers to compete for optimal solutions to user requests and MEV redistribution, ensuring efficient and profitable transactions.
Solver Operations and Bidding: In Atlas, Solvers play a pivotal role by placing bids to execute transactions. Here’s a high-level view of how it works:
- Bidding Process: Solvers place bids on operations, which are assessed based on their bid value. The process involves multiple stages:
- PreOps: Runs before the User’s operation, setting up the context for Solvers.
- PreSolver: Executes after the User’s operation but before the Solver’s task. If errors occur here, the current Solver’s bid is reconsidered.
- PostSolver: Takes place after both the User’s and Solver’s operations. Any failures at this stage affect the entire transaction.
- PostOps: Occurs after a Solver’s task and value allocation. Failures here can revert the User’s operation.
The efficiency of the Solver, along with the accuracy of the bidding, directly impacts the transaction's success and the protocol’s overall security. Atlas ensures bids are carefully managed to maintain integrity and optimize outcomes. To read the review in full, visit this link.
Understanding the solverMetaTryCatch()
Function
The solverMetaTryCatch( )
function manages the execution of solver operation within the Atlas Contract. Its primary role is to ensure that solver processes the bids correctly, handling errors and maintaining the integrity of the transaction.
Additionally, solverMetaTryCatch()
ensures that the contract's execution state is preserved and verifies token balances after the completion of a successful bid, maintaining accuracy and reliability in transaction results.
Now, let’s deep dive into the execution logic of solverMetaTryCatch
function:
Function Setup: The function accepts parameters including bidAmount
, gasLimit
, a SolverOperation
struct, and returnData
. These parameters are essential for the solver to perform its operation. Due to strict access controls, this function can only be executed by the Atlas Contract ensuring secure and authorized handling of solver operations.
Pre-Execution Checks: During the pre-execution phase, two critical checks are performed:
- Verify Balance: This step ensures that contract balance is always initiated with
solverOp.value
before execution of the solve operation. - Bid Token Type: The contract checks the type of token being used for bidding before executing the solver operation. There are two types of tokens: ETH and ERC20.
Safety Checks: The Safety Checks consists of four parts before execution of solver operation: Solver Safety Check, Pre-Solver Hook, DelegateCall, Error Handling. This operation provides secure setup and prevent any unauthorized changes before execution of solver operation:
- Solver Safety Check: The contract verifies if an operation’s control address (
solverOp.control
) matches the actual control address located in the configuration of the smart contract. This guarantees that the control logic has not been tampered with in any way. If the addresses do not match the revert function is invoked and the operation ceases. - Pre-Solver Hook: The Function initially evaluates whether the existing configuration requires a pre-solver hook (for instance: there are some operations that might require validation or preliminary preparation). If so, the hook is executed.
- DelegateCall Execution: Through delegate call, the function turns on the
preSolverCall
in theDAppControl
contract, but the function is executed within the context of this contract. Hence, whatever state or transfers occur pertain to the current contract and not the peripheralDAppControl
contract. - Error Handling: After executing the hook the contract looks for an error. In case there is a failure of the hook (because of an execution error or returns false), the operation is reverted, thus guaranteeing that any wrong pre-solver logic does not endanger the core operation.
Solver Execution: A solver is responsible for processing transactions in a decentralized system. It submits the bids to execute specific tasks, such as transferring tokens or performing calculations, and ensures that the operation is completed correctly while paying the required bid amount. Within the Atlas Contract, the solver has the following tasks:
- Data Preparation: The contract bundles the essential information such as the sender's address, the bid token, the bid amount, and any specific data needed for the operation. If necessary, return data from previous steps is also included.
- Execute Solver Call: The contract sends the prepared data to the execution contract using a call function. This step uses the specified gas limit and value (e.g., Ether) needed for the transaction.
- Check Success: Once the call is executed, the contract checks if it was successful. If the call fails, the entire transaction is reverted, preventing any unintended behaviour.
PostSolver Hook Execution: This step runs additional logic after the solver operation to validate the outcome. If executed, it calls the postSolverCall
to ensure the task was completed correctly. If the check fails, it reverts the transaction, ensuring the operation isn’t left in an incomplete state.
New endBalance: After the bid operation, the endBalance is updated to reflect the total ETH or ERC20 tokens used in the transaction. This ensures any changes to the contract's balance are accurately tracked for proper reconciliation. Once the solver completes the operation and submits their bid, the endBalance is adjusted to the endBalance
for both the solverOp.value
and the bidAmount
, effectively calculated as (solverOp.value + bidAmount)
.
Bid Find Mode: The _bidFind()
is a function within the contract that checks if a valid bid has been found. Its primary role is to determine whether the conditions for processing a bid have been met. In the context of the provided code, it acts as a gatekeeper, ensuring that the contract only proceeds with calculating and adjusting balances if a valid bid is detected.
Surplus Handling: If there’s any leftover balance after the operation, the surplus is returned to the system, preventing unnecessary holding of excess funds. In the event of validation failure, the transaction is promptly reversed with a BalanceNotReconciled
error, ensuring the contract’s financial state remains accurate and properly aligned.
Bid Verification: This section ensures that the solver has paid the correct bid amount based on whether the configuration prefers higher or lower bids. It compares the start, end balances and bidAmount to determine if the bid amount was fully paid, and if not, the transaction is reverted. Any remaining balance is then contributed back to the system.
Vulnerability: Inconsistent Bid Handling (ETH and ERC20)
In the Fastlane Atlas protocol, inconsistent bid handling introduces a vulnerability where endBalances
are miscalculated after a solver executes a bid. This issue arises from incorrect accounting of the solverOp.value
and bidAmount
, leading to inaccuracies in balance tracking after the bidding process.
These miscalculations can lead to:
- Failed bid validation, resulting in unintended transaction reverts.
- Disruption in the bidding process, affecting the reliability of MEV extraction.
- Misaligned balances, which affect the protocol’s expected behaviour post-bid.
Now, we will explain the different cases for ETH and ERC20 tokens, and how inconsistent bid calculation occurs in different scenarios.
Case 1: _bidFind == False
and invertsBidValue == False
(ETH):
Initial Contract State:
- The contract starts with zero balance. There is no pre-existing ETH in the contract.
Start Balance Calculation:
- The
startBalance
is calculated assolverOp.value
, which is the amount of ETH that the solver sends for the while initiating the contract.
startBalance = solverOp.value
- This reflects the initial ETH that the solver provides, which will be used as the base for balance calculations.
Solver Submits Bid:
- The solver submits two key values:
solverOp.value
: The ETH sent by the solver to initiate the contract balance.bidAmount
: The amount the solver is bidding to perform the task.
End Balance Calculation (After Solver Execution):
- Once the solver’s operation is executed, the
endBalance
is calculated by summingsolverOp.value
andbidAmount
.
endBalance = solverOp.value + bidAmount
- This new balance reflects the total ETH in the contract, including both the solver’s original contribution and the bid amount.
Miscalculated Condition:
- The function checks if
endBalance < startBalance + bidAmount
, but this check is flawed. endBalance
already contains bothsolverOp.value
andbidAmount
, so the function is checking an inflated value, leading to an incorrect comparison. This causes the condition to fail, resulting in incorrect bid validation.
Incorrect Final End Balance Calculation:
- The contract then subtracts the
bidAmount
from theendBalance
to calculate the remaining balance.
Incorrect Final endBalance = endBalance - bidAmount
- Since
endBalance
already includessolverOp.value + bidAmount
, subtracting only thebidAmount
leaves thesolverOp.value
as extra ETH in the balance, leading to an incorrect result.
Extra ETH Leftover (Miscalculation):
- As a result, the contract retains extra ETH, known as
ExtraEthEndBalance
, because thesolverOp.value
wasn’t fully accounted for in the subtraction. - The incorrect formula leaves an inflated balance, causing bid validation to fail and introducing an error in the system.
Correct vs. Incorrect Calculation
- Incorrect Calculation (
ExtraEthEndBalance
):
ExtraETH (ETH): endBalance - bidAmount
- This calculation is wrong because it leaves the
solverOp.value
in the balance, leading to extra ETH in the contract. - Correct Calculation (
ExtraEthBalance
):
ExtraETH (ETH): (endBalance - startBalance) - bidAmount
- This formula correctly subtracts both the
start balance (solverOp.value)
and thebidAmount
, resulting in an accurate remaining balance.
Case 2: _bidFind == False
and invertsBidValue == True
(ETH):
Initial Contract State:
- The contract balance starts at zero, with no pre-existing ETH in the contract before the solver's bid is processed.
Start Balance Calculation:
- The Start Balance is calculated as
solverOp.value
, which is the amount of ETH sent by the solver to initiate the contract balance. - This is the initial amount of ETH in the contract before the solver submits their bid.
startBalance = solverOp.value
Solver Submits Bid: The solver sends two values:
solverOp.value
: The ETH provided by the solver to initiate the contract balance.bidAmount
: The amount the solver is bidding for the operation.
End Balance Calculation (After Solver Execution):
- After the solver completes the operation, the
endBalance
is calculated. This balance now reflects the solver’s contribution minus the bid amount.
New endBalance = solverOp.value - bidAmount
- Since
invertsBidValue = true
, this calculation assumes that a lower bid is desirable. As a result,endBalance
reflects the subtraction of thebidAmount
fromsolverOp.value
.
Miscalculated Condition:
- The function checks if
endBalance < startBalance - bidAmount
, but this check is flawed. - The
endBalance
has already had thebidAmount
subtracted. This causes the condition to evaluate asfalse
, even afterendBalance
is already reduced. As a result, the condition fails, leading to incorrect bid handling. - The function incorrectly processes the Final endBalance based on the miscalculation.
Incorrect Final endBalance = endBalance
- The miscalculation leads to extra ETH being left in the contract, resulting in an inflated balance after the operation.
Extra ETH Leftover (Miscalculation):
- The issue arises because
endBalance
containssolverOp.value
minusbidAmount
. Subtracting only thebidAmount
leaves the remainingsolverOp.value
as extra ETH, leading to an inaccurateExtraEthEndBalance
. This incorrect calculation leaves excess ETH in the contract, resulting in an incorrect balance.
Correct vs. Incorrect Calculation
Incorrect Balance Calculation (ExtraEthEndBalance
):
- The system incorrectly leaves extra ETH because it doesn't account for the bid and balance correctly.
Final Balance: endBalance
- This formula incorrectly leaves the balance inflated after the operation.
Correct Balance Calculation (ExtraEthBalance
):
The correct formula for calculating the extra ETH is: ExtraEthEndBalance: (startBalance - endBalance) - bidAmount
- This calculation ensures that both the
startBalance
andendBalance
are properly accounted for, along with thebidAmount
, resulting in the correct final balance.
Case 3: _bidFind == True
and invertsBidValue == True/False
(ERC20 Token):
In the case of bidFind == true
(a bid is found) for ERC20 tokens:
- In the case where
bidFind == true
(a bid is found) for ERC20 tokens, the contract incorrectly usesaddress(this).balance
to check for extra tokens, regardless of whetherinvertsBidValue == true or false
. - This is problematic because
address(this).balance
only checks the ETH balance of the contract and doesn't reflect the balance of ERC20 tokens. As a result, any extra ERC20 tokens are not accounted for properly, and the contract may default to 0, leading to incorrect calculations.
Case 4: _bidFind == False
and invertsBidValue == True/False
(ERC20 Token):
When bidFind == false
, the contract checks its ETH balance using address(this).balance
. This is appropriate for ETH, as it correctly reflects the amount of native ETH held by the contract. However, this approach doesn't apply to ERC20 tokens, which are stored differently.
ERC20 token balances are managed by the token's contract, not by the native balance of the contract itself. Therefore, to correctly check the ERC20 token balance, the contract should use ERC20Token.balanceOf(address(this))
. This method directly queries the ERC20 token contract, ensuring that the token balance is properly accounted for. Failing to use balanceOf()
will cause the contract to miss the actual token balance, leading to incorrect calculations or assumptions when dealing with ERC20 tokens.
Impact
The vulnerability in the solverMetaTryCatch()
function of the Fastlane Atlas protocol had significant potential implications:
- Bid Calculation Errors: The bug in
solverMetaTryCatch()
led to inaccuracies in bid computations. This resulted in incorrect bid values, affecting the efficiency and fairness of the solver's operations. - Suboptimal Transaction Execution: Due to the bid calculation errors, the transactions may have been executed in a less optimal manner, reducing the effectiveness of the MEV redistribution and the overall performance of the protocol.
- Security Risks: The vulnerability created a potential for exploitation, where attackers could manipulate the bidding process to their advantage, undermining the protocol’s security.
- Protocol Reliability: The inconsistencies affected the reliable execution of transactions, potentially leading to execution failures or unintended results.
The issue has been addressed, enhancing both the security and reliability of the Fastlane Atlas protocol. This resolution demonstrates the protocol’s ongoing commitment to delivering robust and effective solutions in the DeFi space.
Remediation
In response to the identified issue with the solverMetaTryCatch()
function, the Fastlane Atlas team took several decisive actions to address and rectify the vulnerability. The remediation efforts focused on fixing the calculation discrepancies, reinforcing testing practices, and bolstering security measures to prevent similar issues in the future. Here’s a detailed overview of the steps taken to resolve the issue effectively:
- Account for Pre-existing ETH: Update the
solverMetaTryCatch()
function so it doesn't assume there’s no existing ETH in the contract. Always consider any pre-existing balances when calculating startBalance and endBalance to ensure accuracy. - Unique Variables for Unique Purposes: Make sure each variable is dedicated to a single purpose. For instance, don’t reuse startBalance or endBalance for different calculations or token types. Introduce new variables as needed to keep things clear and prevent mistakes caused by reusing variables.
- Standardize Bid Amount Handling: Instead of switching between solverOp.bidAmount and bidAmount, simplify things by always using bidAmount after it’s set, regardless of whether bidFind is true or false.
- Set Allowances Properly: Make sure ERC20 token allowances are explicitly set, limiting the solver’s ability to withdraw only the tokens necessary for the bid, preventing unauthorized or excessive token usage.
Conclusion
This blog dived into a significant vulnerability found within the Fastlane Atlas protocol as part of their review with Spearbit, specifically focusing on discrepancies in bid calculations within the solverMetaTryCatch()
function. Addressing this vulnerability was crucial not only for accurate bid evaluations but also ensuring trust across the entire Fastlane Atlas ecosystem.
This case highlights a key lesson: even minor vulnerabilities can have substantial effects on the security and performance of smart contracts. It’s imperative to take proactive security measures, as the Fastlane team did, such as undergoing security reviews as part of the development process.
Contact the Spearbit team today to learn more about our smart contract security reviews.