Prepared by:
HALBORN
Last Updated 01/10/2025
Date of Engagement by: December 23rd, 2024 - January 3rd, 2025
100% of all REPORTED Findings have been addressed
All findings
5
Critical
0
High
0
Medium
0
Low
2
Informational
3
InFlux Technologies
engaged Halborn
to conduct a security assessment on their smart contracts revisions started on December 23th, 2024 and ending on January 3rd, 2025. The security assessment was scoped to the smart contracts provided to the Halborn
team.
Commit hashes and further details can be found in the Scope section of this report.
The team at Halborn was provided 1 week and 2 days for the engagement and assigned a security engineer to evaluate the security of the smart contract.
The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this assessment is to:
Ensure that smart contract functions operate as intended.
Identify potential security issues with the smart contracts.
In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were mostly acknowledged by the InFlux Technologies team
. The main ones were the following:
Add some checks for invalid time range.
Limit message length return.
Add some sanity checks.
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance code coverage and quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:
Research into architecture and purpose.
Smart contract manual code review and walkthrough.
Graphing out functionality and contract logic/connectivity/functions. (solgraph,draw.io
)
Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes.
Manual testing by custom scripts.
Static Analysis of security for scoped contract, and imported functions. (Slither
)
Testnet deployment. (Hardhat
,Foundry
)
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Attack Origin (AO) | Arbitrary (AO:A) Specific (AO:S) | 1 0.2 |
Attack Cost (AC) | Low (AC:L) Medium (AC:M) High (AC:H) | 1 0.67 0.33 |
Attack Complexity (AX) | Low (AX:L) Medium (AX:M) High (AX:H) | 1 0.67 0.33 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Confidentiality (C) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Integrity (I) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Availability (A) | None (A:N) Low (A:L) Medium (A:M) High (A:H) Critical (A:C) | 0 0.25 0.5 0.75 1 |
Deposit (D) | None (D:N) Low (D:L) Medium (D:M) High (D:H) Critical (D:C) | 0 0.25 0.5 0.75 1 |
Yield (Y) | None (Y:N) Low (Y:L) Medium (Y:M) High (Y:H) Critical (Y:C) | 0 0.25 0.5 0.75 1 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
2
Informational
3
Security analysis | Risk level | Remediation Date |
---|---|---|
Invalid Time Range can be returned by _intersectTimeRange | Low | Risk Accepted - 01/08/2025 |
Unbounded Error Message Length in Paymaster External Calls Creates Gas Unpredictability | Low | Not Applicable |
Missing sanity check for entryPoint | Informational | Acknowledged - 01/08/2025 |
Lack of Account Deployment Verification can lead to Unexpected Revert Behavior | Informational | Acknowledged - 01/08/2025 |
Overly Broad Unchecked Math Blocks in EntryPoint Contract | Informational | Acknowledged - 01/08/2025 |
// Low
The _intersectTimeRange
function in the Helper library contains a logical flaw in its time range validation mechanism. When intersecting time ranges between account and paymaster validations, the function fails to verify if the resulting time window is valid. The issue occurs in the following sequence:
The function takes the maximum value between account's and paymaster's validAfter
The function takes the minimum value between account's and paymaster's validUntil
No validation exists to ensure final validAfter <= validUntil
// intersect account and paymaster ranges.
function _intersectTimeRange(
uint256 validationData,
uint256 paymasterValidationData
) internal pure returns (ValidationData memory) {
ValidationData memory accountValidationData = _parseValidationData(validationData);
ValidationData memory pmValidationData = _parseValidationData(paymasterValidationData);
address aggregator = accountValidationData.aggregator;
if (aggregator == address(0)) {
aggregator = pmValidationData.aggregator;
}
uint48 validAfter = accountValidationData.validAfter;
uint48 validUntil = accountValidationData.validUntil;
uint48 pmValidAfter = pmValidationData.validAfter;
uint48 pmValidUntil = pmValidationData.validUntil;
if (validAfter < pmValidAfter) validAfter = pmValidAfter;
if (validUntil > pmValidUntil) validUntil = pmValidUntil;
// @tocheck missing validation to ensure validAfter <= validUntil
return ValidationData(aggregator, validAfter, validUntil);
}
It could result in :
Invalid time ranges being accepted by the system
Broken time-based validation mechanisms
This POC can be reproduced using this code :
import { expect } from "chai";
import { ethers } from "hardhat";
import { HelperTest } from "../contracts/HelperTest.sol"; // Adjust import path as needed
describe("Helper", () => {
let helperTest: HelperTest;
beforeEach(async () => {
const Helper = await ethers.getContractFactory("HelperTest");
helperTest = await Helper.deploy();
});
it("should demonstrate validAfter > validUntil in _intersectTimeRange", async () => {
// Create validation data where account is valid from t=100 to t=200
const accountValidationData = helperTest.packValidationData({
aggregator: ethers.ZeroAddress,
validAfter: 100n,
validUntil: 200n
});
// Create paymaster validation data where paymaster is valid from t=150 to t=120 (ERROR INTRODUCED)
const paymasterValidationData = helperTest.packValidationData({
aggregator: ethers.ZeroAddress,
validAfter: 150n,
validUntil: 120n
});
// Intersect the time ranges
const result = await helperTest.intersectTimeRange(
accountValidationData,
paymasterValidationData
);
const validAfter = Number(result.validAfter);
const validUntil = Number(result.validUntil);
// validAfter (150) is greater than validUntil (120)
expect(validAfter).to.be.greaterThan(validUntil);
expect(validAfter).to.equal(150);
expect(validUntil).to.equal(120);
console.log(`ValidAfter: ${validAfter}, ValidUntil: ${validUntil}`);
});
});
Result :
account-abstraction git:(master) ✗ npx hardhat test --grep "Helper"
Helper
ValidAfter: 150, ValidUntil: 120
✔ should demonstrate validAfter > validUntil in _intersectTimeRange
1 passing (979ms)
It is recommended to add another validation check after the time range intersection and mark the validation as failed when an invalid range is detected.
RISK ACCEPTED: An updated will be used for new chains. However, Influx will be using the current deployed on chain version for chains where is it possible.
// Low
The EntryPoint
contract handles error messages from paymaster calls without imposing size limits. This occurs in two critical locations where external calls to paymaster contracts are made: _validatePaymasterPrepayment
and _handlePostOp
. First instance in _validatePaymasterPrepayment
:
function _validatePaymasterPrepayment(
uint256 opIndex,
UserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPreFund,
uint256 gasUsedByValidateAccountPrepayment
) internal returns (bytes memory context, uint256 validationData) {
// .... //
try IPaymaster(paymaster).validatePaymasterUserOp{gas: gas}(op, opInfo.userOpHash, requiredPreFund)
returns (bytes memory _context, uint256 _validationData) {
context = _context;
validationData = _validationData;
} catch Error(string memory revertReason) {
revert FailedOp(opIndex, string.concat("AA33 reverted: ", revertReason));
}
}
Second instance in _handlePostOp
:
function _handlePostOp(
uint256 opIndex,
IPaymaster.PostOpMode mode,
UserOpInfo memory opInfo,
bytes memory context,
uint256 actualGas
) private returns (uint256 actualGasCost) {
// ... //
try IPaymaster(paymaster).postOp{gas: mUserOp.verificationGasLimit}(mode, context, actualGasCost)
{} catch Error(string memory reason) {
revert FailedOp(opIndex, string.concat("AA50 postOp reverted: ", reason));
}
}
While the contract defines REVERT_REASON_MAX_LEN = 2048
, this limit is not enforced when handling these paymaster error messages.
Report as informational as it's paymaster handling but there are several impacts here :
Gas consumption becomes unpredictable when copying large error messages to memory
Malicious paymasters can force excessive gas consumption through large error messages
Bundle execution can fail unexpectedly due to out-of-gas conditions
It is recommended to implement error message length limits using the existing REVERT_REASON_MAX_LEN
constant.
NOT APPLICABLE: Non concerning because the current deployed files does not use the affected code.
// Informational
The BasePaymaster
contract lacks interface validation for the _entryPoint parameter in its constructor. The current implementation directly sets the EntryPoint
address without verifying if it implements a matching IEntryPoint
interface.
constructor(IEntryPoint _entryPoint, address _owner) Ownable(_owner) {
//E @tocheck missing sanity check for _entryPoint
setEntryPoint(_entryPoint);
}
It is recommended to implement the same mechanism as it is done in eth-infinitism development branch repository which includes proper validation with an interface to be implemented:
constructor(IEntryPoint _entryPoint) Ownable(msg.sender) {
_validateEntryPointInterface(_entryPoint);
entryPoint = _entryPoint;
}
function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual {
require(IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), "IEntryPoint interface mismatch");
}
ACKNOWLEDGED: An on chain version of this file will be used instead of the one in the repository, but the project does not use the affected code.
// Informational
The _validateAccountPrepayment
function in EntryPoint.sol fails to verify the existence of code at the sender's address when initCode
is empty. While the function deploys new accounts when initCode
is provided, it assumes pre-existing accounts are already deployed without verification.
function _validateAccountPrepayment(
uint256 opIndex,
UserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPrefund
) internal returns (uint256 gasUsedByValidateAccountPrepayment, uint256 validationData) {
unchecked {
MemoryUserOp memory mUserOp = opInfo.mUserOp;
address sender = mUserOp.sender;
_createSenderIfNeeded(opIndex, opInfo, op.initCode);
// Missing verification of sender.code.length
try IAccount(sender).validateUserOp{gas: mUserOp.verificationGasLimit}(op,opInfo.userOpHash,missingAccountFunds)
// ... //
The issue occurs because the subsequent call to validateUserOp
will revert at the EVM level if no code exists at the sender's address. This revert happens before entering the try-catch
block, preventing the emission of the expected FailedOp
error.
The risk is that Bundlers
receive unclear error feedback when processing transactions for non-deployed accounts.
It is recommended to add an explicit deployment check after _createSenderIfNeeded
to ensure consistent error handling and provide clear feedback about account deployment status.
ACKNOWLEDGED: An on chain version of this file will be used instead of the one in the repository, but the project does not use the affected code.
// Informational
The EntryPoint contract contains multiple functions where entire function bodies are wrapped in unchecked blocks, rather than limiting these blocks to specific arithmetic operations that require them. This pattern is observed in critical functions including _validateAccountPrepayment
, _validatePaymasterPrepayment
, and _execute
instances:
function _validateAccountPrepayment(
uint256 opIndex,
UserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPrefund
) internal returns (uint256 gasUsedByValidateAccountPrepayment, uint256 validationData) {
unchecked {
// Entire function body (50+ lines) within unchecked block
uint256 preGas = gasleft();
// ... many operations that don't require unchecked math
gasUsedByValidateAccountPrepayment = preGas - gasleft();
}
}
function _validatePaymasterPrepayment(
uint256 opIndex,
UserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPreFund,
uint256 gasUsedByValidateAccountPrepayment
) internal returns (bytes memory context, uint256 validationData) {
unchecked {
// Entire function body within unchecked block
// ... including operations that don't require unchecked math
paymasterInfo.deposit = uint112(deposit - requiredPreFund);
}
}
Impacts:
Reduced code clarity and increased review complexity
Higher risk of arithmetic overflow/underflow vulnerabilities
It is recommended to restrict unchecked blocks to specific arithmetic operations where overflow/underflow checks are unnecessary.
ACKNOWLEDGED: An on chain version of this file will be used instead of the one in the repository, but the project does not use the affected code.
Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither
, a Solidity static analysis framework.
After Halborn verified the smart contracts in the repository and was able to compile them correctly into their abis and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.
All issues identified by Slither
were proved to be false positives or have been added to the issue list in this report.
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed