Guardian's review of Merkle Vester for Magna, published August 2025. The report records 13 findings, including 1 medium and 6 low.
- Published
- Review window
- August 11 to 13, 2025
- Language
- Solidity
- Chains
- Ethereum, Base, Optimism, Polygon, Arbitrum, BNB Chain
- Sector
- Infrastructure
- 0 Critical
- 0 High
- 1 Medium
- 6 Low
- 6 Informational
Scope
Findings 13
-
M-01 Medium Withdraw Allows Unauthorized Post-Claim Actions Logical Error Acknowledged
Description
The
withdrawfunction can be executed by any caller on behalf of another user, using the target user’srootIndex,decodableArgs, andproof. However, thepostClaimHandlerandextraDataparams are not fixed by the user and can be arbitrarily set by the caller.If the
postClaimHandlerWhitelistcontains both direct claim options and contracts such as staking contracts, a caller could force a user’s withdrawal to be routed into staking—potentially with a lock-up period. Similarly, the caller could chooseextraDatavalues that, depending on the handler contract’s logic, could negatively affect the user’s funds without their consent.Recommendation
If the withdrawal is not a direct claim, restrict execution to the withdrawalAddress.
-
L-01 Low RevokeAll Allows Draining New Vesting Schedules Validation Acknowledged
Description
The
revokeAllfunction transfers the entire contract balance to thebenefactorwithout updating allocation states or setting any revocation flags:function revokeAll() external nonReentrant onlyRole(BENEFACTOR) { SafeERC20.safeTransfer(IERC20(token), msg.sender, IERC20(token).balanceOf(address(this))); }This leaves existing allocations'
DistributionStateunchanged (e.g.,terminatedTimestampremains 0), allowing beneficiaries of old allocations to continue computing vested amounts and withdrawing funds if the contract is later refunded for new allocations. Since funding is global (not per-allocation), old claims can drain the pool on a first-come-first-served basis, rendering new allocations insolvent despite valid vesting schedules. No events are emitted, reducing transparency and the function can be called repeatedly, exacerbating risks in underfunded or multi-root scenarios. This can lead to loss of funds for new beneficiaries.Recommendation
Update
revokeAllto set a global revocation flag (e.g., a boolallRevokedin storage) that blocks all future calls toaddAllocationRoot,setAllocationRootandfundfunctions. Alternatively, pair it with pausing the contract. Finally, consider emitting aRevokedAllevent with the transferred amount for auditability. -
L-02 Low Potential Global allocation.id Collisions Validation Acknowledged
Description
All mutable vesting state is stored under a single key: the free form string
allocation.id, shared across the entire contract lifetime irrespective of which Merkle root/batch a leaf belongs to. The contract never namespaces or fingerprints the state by rootIndex, root hash, or beneficiary. It also never enforces that a later leaf reusing the same id refers to the same logical allocation.Relevant storage and lookups:
// MerkleVester.sol mapping(string => DistributionState) public schedules; // mutable state (withdrawn, termination, withdrawalAddress) mapping(string => bool) private feeAlreadyPayed; // fee “pay once” flag // Everywhere state is read/written, the key is only allocation.id: schedules[allocation.id].withdrawalAddress schedules[allocation.id].terminatedTimestamp schedules[allocation.id].withdrawn feeAlreadyPayed[allocation.id]Operationally, it is very common for teams (or different admin wallets, see the following link) to build multiple Merkle trees over time (e.g., “Grants 2025”, “Airdrop Wave 2”) and to reuse human‑friendly IDs like "Distribution#2025". Because the contract treats id as a global primary key, any two leaves in different roots that share the same id will share one
DistributionStateand one fee flag. The first claimant to touch that id determines thewithdrawalAddressused for all future claims with that id, regardless of which root those future claims come from. Admin actions (cancel/revoke) and the “pay fee once” bit also bleed across batches.Impact:
- Funds can be misdirected: a claim under batch A sets
schedules[id].withdrawalAddressand claims under batch B (different person) will pay to that same address. - Future vesting can be capped/disabled across batches:
cancel/revokeof one leaf setsterminatedTimestampfor all leaves sharing that id. - Fees can be under‑collected: claiming once anywhere sets
feeAlreadyPayed[id]=truefor all other leaves with that id.
This is a correctness flaw that can permanently redirect funds or block rightful beneficiaries and it is realistic in production (distinct teams/batches often reuse friendly IDs).
Proof of Concept
Assume two completely separate Merkle roots are deployed:
- Root #0 (“HR Salaries 2025”, built by HR → admin wallet #1)
Allocation { id: "salaries#2025", originalBeneficiary: 0xA11c...ALEX, totalAllocation: ..., transferableByBeneficiary: true, ... // schedule = INTERVAL over 12 months }- Root #1 (“Growth Salaries 2025”, built by Growth → admin wallet #2)
Allocation { id: "salaries#2025", originalBeneficiary: 0xB0bB...B0B, totalAllocation: ..., transferableByBeneficiary: true, ... // schedule = CALENDAR single unlock after 30 days }Both leaves are valid for their respective roots. The id collision is accidental (common when using human‑friendly IDs).
T0 — Deploy roots
merkleRoots[0] = <root for HR Salaries 2025> merkleRoots[1] = <root for Growth Salaries 2025>On‑chain state is empty for this id:
schedules["salaries#2025"] = { withdrawalAddress: address(0), terminatedTimestamp: 0, withdrawn: 0, ... }T1 — Bob claims his airdrop first (rootIndex = 1)
withdraw( 0, // all available 1, // Growth salary root abi.encode("calendar", bobAlloc, bobSchedule), proofForRoot1, DIRECT_CLAIM_HANDLER, "" );Inside withdraw:
- The Merkle proof for root #1 validates.
_checkOrSetOriginalBeneficiary(bobAlloc)runs:
if (schedules[bobAlloc.id].withdrawalAddress == address(0)) { schedules[bobAlloc.id].withdrawalAddress = bobAlloc.originalBeneficiary; // 0xB0bB...B0B }- Tokens are transferred to
schedules["salaries#2025"].withdrawalAddress(now Bob).
Result: Bob is paid and the global
withdrawalAddressfor "salaries#2025" is pinned to Bob.T2 — Weeks later, Alice claims her salary (rootIndex = 0)
withdraw( 0, // all available 0, // HR salary root abi.encode("interval", aliceAlloc, aliceSchedule), proofForRoot0, DIRECT_CLAIM_HANDLER, "" );Inside withdraw:
- The Merkle proof for root #0 validates.
_checkOrSetOriginalBeneficiary(aliceAlloc)does nothing becauseschedules[id].withdrawalAddress != 0.- Payout destination is looked up only from global state:
address to = schedules["salaries#2025"].withdrawalAddress; // 0xB0bB...B0B SafeERC20.safeTransfer(token, to, aliceVestedNow);Alice’s salary is transferred to Bob’s address. Alice never needed to lose a key or sign anything, the only cause is that both leaves reused "
salaries#2025".Recommendation
Consider documenting this risk in the
addAllocationRootandsetAllocationRootfunctions. Implement safeguards at your backend that ensures that 2 ids are never reused. - Funds can be misdirected: a claim under batch A sets
-
L-03 Low Misleading Revert Reason Warning Resolved
Description
In
_transferBeneficiaryAddressthe code intends to block beneficiary‑initiated transfers while the contract is paused, allowing only admin‑initiated transfers in that state. The guard is functionally correct but the revert reason is inverted: it reverts withContractNotPausedwhenpaused() == true.Current code:
bool authorizedByAdmin = (AccessControl.hasRole(BENEFACTOR, msg.sender) && allocation.transferableByAdmin); bool authorizedByBeneficiary = (msg.sender == state.withdrawalAddress && allocation.transferableByBeneficiary); // authorizedByBeneficiary can only be true if not paused, error that should be used is ContractPaused() if (authorizedByBeneficiary && !authorizedByAdmin && paused()) revert ContractNotPaused();Elsewhere in the codebase,
ContractNotPaused()is used to enforce “this action requires the contract to be paused” (e.g.,setAllocationRoot), which makes the above usage inconsistent and confusing.Recommendation
Emit a revert that accurately reflects the state and keep semantics consistent across the codebase. Introduce a
ContractPaused()custom error and use it here, or reuse a consistent error/message used for “paused” across the project.Add the error in
AirlockTypes.sol:error ContractPaused();Use this new custom error in the
_transferBeneficiaryAddressfunction:function _transferBeneficiaryAddress( DistributionState storage state, Allocation memory allocation, address _newAddress ) internal { ... if (authorizedByBeneficiary && !authorizedByAdmin && paused()) revert ContractPaused(); ... } -
L-04 Low High‑level Transient Variables Warning Acknowledged
Description
The code uses high‑level transient storage variables whose semantics compile down to EIP‑1153 TSTORE/TLOAD and are only code‑generated by Solidity starting with 0.8.28. The files declare a floating pragma
^0.8.24, which allows any compiler in the range>=0.8.24 <0.9.0. If CI, verification, or a local build picks 0.8.24–0.8.27, high‑level transient variables are not supported for code generation and builds will fail or behave unexpectedly. Even when compiled with 0.8.28+, deploying to a network that has not activated Cancun (EIP‑1153) will cause runtime failures because the emitted opcodes are not available pre‑fork.Representative snippets:
// MerkleVester.sol pragma solidity ^0.8.24; bool transient insideMulticall; bool transient transientFeeInitialized; uint256 transient transientFeeReceived; uint256 transient transientFeeIncurred;These transient fields are central to fee accounting and the multicall lifecycle. Compiler drift here directly affects correctness: different minor compilers can accept or reject transient, generate different IR/Yul, or target an evmVersion that does not include Cancun.
Recommendation
Pin the compiler to a specific version that supports high‑level transient storage and set the EVM to Cancun in your toolchain. Replace floating pragmas with an exact version and enforce the same in your build config. For example, update all contracts and configuration as follows:
// In every Solidity file pragma solidity 0.8.28;# foundry.toml [profile.default] solc_version = "0.8.28" evm_version = "cancun" optimizer = true optimizer_runs = 200// hardhat.config.ts import { HardhatUserConfig } from "hardhat/config"; const config: HardhatUserConfig = { solidity: { version: "0.8.28", settings: { evmVersion: "cancun", optimizer: { enabled: true, runs: 200 } } } }; export default config; -
L-05 Low Direct Claim Handler Address Warning Acknowledged
Description
The
MerkleVestercontract supports direct claims (i.e., transferring tokens directly to the beneficiary without a post-claim handler) via theDIRECT_CLAIM_HANDLERconstant set toaddress(0). A convenience overload of thewithdrawfunction uses this handler implicitly. However, the internal_withdrawToBeneficiaryfunction requires all post-claim handlers, includingaddress(0),to be explicitly whitelisted in thepostClaimHandlerWhitelist. The constructor only whitelists handlers provided in the_postClaimHandlersarray and does not automatically addaddress(0). If deployment scripts omitaddress(0)from this array, attempts to use the defaultwithdrawoverload (or explicitly passaddress(0)) will revert withPostClaimHandlerNotWhitelisted(). This breaks the "plain" withdrawal functionality, which is intended as a user-friendly default.Users expecting simple, direct withdrawals (without custom handlers) will encounter unexpected reverts, leading to failed transactions.
Recommendation
Consider automatically whitelisting
address(0)in the constructor to enable direct claims by default:// In constructor, after adding provided handlers: postClaimHandlerWhitelist.add(address(0)); // Enable direct claims out-of-the-box -
L-06 Low Misleading Immutable Warning Resolved
Description
In the
MerkleVestercontract, an immutablefeeSetteraddress is set during construction and documented as the "address who can update the fee." If_feeSetteris non-zero, the constructor grants theFEE_SETTER_ROLEto this address. However, thesetClaimFeefunction enforces access solely viaonlyRole(FEE_SETTER_ROLE), without any direct reference to thefeeSetteraddress. This role-based access (inherited from OpenZeppelin'sAccessControl) allows the contract's admin (holdingDEFAULT_ADMIN_ROLE, initially the benefactor) to grant or revoke the role to/from any address, including multiple parties.The
feeSettervariable itself is never read or used after the constructor, rendering it dead code. This creates a discrepancy between the code's behavior (flexible, role-based fee setting) and its documentation/immutable storage (implying a fixed, address-specific privilege). In the outlined valid states for fees:- Case 3 (
feeCollector != address(0), feeSetter != address(0)): Fees are updatable, but not exclusively byfeeSetter, any role holder can update. This could mislead deployers, auditors or users into assumingfeeSetterhas exclusive, immutable control, leading to incorrect trust assumptions or overlooked admin privileges in fee management.
The flow is as follows:
- Constructor: Set
feeSetter = _feeSetter; if non-zero, grantFEE_SETTER_ROLEto it. - Fee updates: Checked only against the role, not the address.
- Role management: Admin can alter role holders at any time, bypassing the immutable
feeSetter.
No security vulnerability exists if roles are managed correctly, but the unused variable and docs inconsistency reduce clarity.
Recommendation
Remove the immutable
feeSettervariable entirely, as it serves no functional purpose post-construction. If retaining for logging/initial setup reference, rename it toinitialFeeSetterand update all documentation (e.g., constructor params, valid states) to explicitly state: "Fee updates are controlled by holders ofFEE_SETTER_ROLE, initially granted to the provided address if non-zero. The admin (benefactor) can grant/revoke this role to manage access flexibly." Add an event emission in the constructor for the initial grant to improve traceability. - Case 3 (
-
I-01 Informational Unused Imports Best Practices Acknowledged
Description
Several Solidity files pull in libraries or contracts they never reference. Keeping unused imports makes the code harder to review and marginally increases compile time. Below are the concrete instances found in the code you provided, with the exact lines and why they are unused.
In
MerkleVester.sol, the file imports OpenZeppelin’sMerkleProofbut never calls it here. Merkle verification is delegated toMerkleValidator. The import can be safely removed from this unit.// MerkleVester.sol import {MerkleProof} from "@openzeppelin/utils/cryptography/MerkleProof.sol"; // ← unused in this fileIn
IAirlockBase.sol, theMathlibrary is included but not used anywhere in the contract. All arithmetic is plain checked arithmetic or handled bySafeERC20/AccessControllogic, there are no calls toMath.*in this unit.// IAirlockBase.sol import "@openzeppelin/utils/math/Math.sol"; // ← unusedIn
IMerkleVester.sol, the interface does not invoke or referenceSafeERC20at all, so that import is unused.// IMerkleVester.sol import "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; // ← unusedIn
AirlockTypes.sol, the file defines only errors and structs, yet it imports four libraries/contracts it never uses. None ofSafeERC20,AccessControlorReentrancyGuardare referenced in this file and can be removed entirely.// interfaces/AirlockTypes.sol import "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; // ← unused import {AccessControl} from "@openzeppelin/access/AccessControl.sol"; // ← unused import {ReentrancyGuard} from "@openzeppelin/utils/ReentrancyGuard.sol"; // ← unusedRecommendation
Consider removing the unused imports described above.
-
I-02 Informational Missing License Declaration Best Practices Acknowledged
Description
The codebase lacks any form of license declaration. This includes:
- No SPDX-License-Identifier comments at the top of Solidity files (e.g., // SPDX-License-Identifier: MIT).
- No LICENSE file in the root directory or subdirectories.
- No mentions of licensing terms in the
README.mdor other documentation files.
This absence creates ambiguity regarding the legal terms under which the code can be used, modified, distributed, or contributed to. Without a license, the code defaults to "all rights reserved" under copyright law, potentially restricting adoption and collaboration.
Recommendation
Choose and declare a license for the project, then make it explicit in every source file and at the repository root.
- Add a
LICENSEfile at the repo root that matches your intended terms. Common options:- MIT (most compatible with OpenZeppelin and widely used in web3)
- Apache‑2.0 (permissive + patent grant)
- GPL‑3.0‑or‑later (copyleft)
- BUSL‑1.1 (source‑available, time‑delayed commercial use)
- Add an SPDX header to the top of every .sol file, before the pragma. For example, if you choose MIT:
// SPDX-License-Identifier: MIT pragma solidity 0.8.28; -
I-03 Informational Code Typos Documentation Acknowledged
Description
Multiple files contain spelling mistakes, grammar slips and a few poorly named symbols. While these don’t change runtime behavior, they can confuse reviewers and integrators and in a few places could mislead automation that keys off error names or comments. Below is a concrete inventory with suggested corrections:
MerkleVester.solTypo in comments and parameter docs; also “paid” vs “payed”:
/// @dev lazily store the mutable state as allocaitons are interacted with // ^^^^^^^^^^^ → allocations /** * @param benefactor inital administator and benefactor of the contract * ^^^^^^ → initial ^^^^^^^^^^^^^ → administrator * @param _shouldPayClaimFeeOnlyOnce true, if claim fee should only be payed for the first claim * ^^^^ → paid */ /// @dev mapping allocation id to a boolean to store allocation ids for which fee was already payed // ^^^^ → paid // In setAllocationRoot() comment: // "whith the new modified schedule" → "with the new modified schedule" /** * @notice Returns true, if the decodableArgs is calendar type * @param decodableArgs decodable args to checks whether it is calendar type or not * ^^^^^ → check */IAirlockBase.solConstructor docs, variable name casing and a misspelled word in a comment.
/** * @param _benefactor inital administator and benefactor of the contract * ^^^^^^ → initial * ^^^^^^^^^^^^^ → administrator */ uint256 _postCLaimHandlersLength = _postClaimHandlers.length; // ^^ Wrong casing → _postClaimHandlersLength (non‑functional, but tidy) ... // any error in the postClam handler will revert the entire transaction // ^^^^ → postClaimFunction naming is also awkward:
function removePostClaimHandlerToWhitelist(...) external { ... } // “ToWhitelist” reads like “add”. Prefer removePostClaimHandlerFromWhitelist(...) for clarityIMerkleVester.solDoc typos in returns and comments.
/** * @return the new lenght of the number of merkle roots array minus 1 * ^^^^^^ → length */ /** * @dev using defund can result in underfunding the total liabilies of the allocations * ^^^^^^^^^^ → liabilities */interfaces/AirlockTypes.solSeveral repeated slips in comments and error docs.
/// @dev solidity does not support immutablability outside of compile time // ^^^^^^^^^^^^^^^ → immutability (appears multiple times) /// @dev error thrown when to many timestamps are provided // ^^ → too /// @dev error thrown when the supplied beneficiary address is the same as an others // ^^^^^^^ → another's /// @dev error thrown when an an amount in an interval is invalid // ^^ → an /// @dev error thrown when the the claim fee handler is not yet whitelisted // ^^^ → the /// @param totalAllocation total amount of tokens to vest in the allocaiton // ^^^^^^^^^^ → allocation /// @param fundedAmount ... merkle vester does not support funding indivual allocations // ^^^^^^^^ → individual // same “indivual” typo repeats in terminatedWithdrawn/terminatedAmount commentsinterfaces/ICalendarVester.solHeader comment misspells “Calendar”.
/// @notice Abstract contract to define common behavior for Calender type vesters // ^^^^^^^ → CalendarMerkleValidator.solDuplicated word in doc comment.
/// @notice Verify verify a leaf is included in the Merkle tree // ^^^^^^ → (remove duplicate) “Verify a leaf …” or “Verifies that a leaf …”README.mdMultiple typos and a subject/verb agreement slip, plus a word choice that doesn’t match code semantics.
Vester contracts internally operates on allocations # ^^^^^^^ → operate Merkle based contracts can provide gas savings when the number of allocatoins are high # ^^^^^^^^^^^ → allocations Interval schedules can be ... subdiveded into a piece # ^^^^^^^^^^ → subdivided ## 1.4 Noteable features # ^^^^^^^ → Notable ... indivual ... # ^^^^^^^^ → individualThe
READMEalso uses “percents”/percentages in examples for calendar/interval, while the code uses absolute unlockAmounts and piece amount. That’s not a spelling error but a terminology mismatch worth fixing to avoid builder confusion.Recommendation
Standardize and clean up language typos across the repository.
-
I-04 Informational Outdated Documentation Documentation Acknowledged
Description
Docs show calendar/interval examples in percents, but the code uses absolute amounts:
// evm/src/distribution/README.md ## 1.2 Types of vesting schedules There are two types of schedules implemented. ### 1.2.1 Calendar schedules /** * @notice Immutable unlock schedule for calendar allocations * @dev solidity does not support immutablability outside of compile time, contracts must not implement mutability * * @param unlockScheduleId id of the allocation * @param unlockTimestamps sequence of timestamps when funds will unlock * @param unlockPercents sequence of percents that unlock at each timestamp, in 10,000ths */ struct CalendarUnlockSchedule { string unlockScheduleId; uint32[] unlockTimestamps; uint256[] unlockPercents; }vs actual code:
// AirlockTypes.sol /** * @notice Immutable unlock schedule for calendar allocations * @dev solidity does not support immutablability outside of compile time, contracts must not implement mutability * * @param unlockScheduleId id of the allocation * @param unlockTimestamps sequence of timestamps when funds will unlock * @param unlockAmounts sequence of amounts that unlock at each timestamp */ struct CalendarUnlockSchedule { string unlockScheduleId; // Workaround for Internal or recursive type is not allowed for public state variables uint32[] unlockTimestamps; uint256[] unlockAmounts; }Moreover interval docs show percent per piece, code has amount:
// evm/src/distribution/README.md ### 1.2.2 Interval schedules /** * @notice Immutable unlock schedule for interval allocations * @dev solidity does not support immutablability outside of compile time, contracts must not implement mutability * * @param unlockScheduleId id of the allocation * @param pieces sequence of pieces representing phases of the unlock schedule, percents of pieces must sum to 100% */ struct IntervalUnlockSchedule { string unlockScheduleId; // Workaround for Internal or recursive type is not allowed for public state variables Piece[] pieces; } /** * @notice Represents a phase of an interval unlock schedule * @dev solidity does not support immutablability outside of compile time, contracts must not implement mutability * * @param startDate start timestamp of the piece * @param periodLength time length of the piece * @param numberOfPeriods how many periods for this piece * @param percent the total percent, in 10,000ths that will unlock over the piece */ struct Piece { uint32 startDate; uint32 periodLength; uint32 numberOfPeriods; uint32 percent; }vs actual code:
// AirlockTypes.sol /** * @notice Represents a phase of an interval unlock schedule * @dev solidity does not support immutablability outside of compile time, contracts must not implement mutability * * @param startDate start timestamp of the piece * @param periodLength the length of each period in seconds * @param numberOfPeriods the number of periods in the piece * @param amount the amount of tokens that is released in each piece */ struct Piece { uint32 startDate; uint32 periodLength; uint32 numberOfPeriods; uint256 amount; }Recommendation
Consider updating examples and field names in docs to avoid confusion.
-
I-05 Informational Lack Of Sanity Checks Validation Resolved
Description
The
_getVestedAmountfunction in theCalendarVesterabstract contract does not enforce sanity checks on the input arrays for calendar schedules. Specifically, there are no validations to ensure thatunlockTimestamps.lengthequalsunlockAmounts.length, or that theunlockTimestampsarray is strictly increasing (sorted in ascending order). Since schedule data originates from off-chain Merkle leaves (validated only by proof inclusion), malformed inputs could lead to incorrect vesting calculations, such as under-vesting, over-vesting, or transaction reverts due to out-of-bounds array access. While the benefactor controls Merkle roots and is trusted to provide valid data, the absence of on-chain checks increases the risk of errors from backend misconfigurations or adversarial leaves.Recommendation
- Add explicit validations in
_getVestedAmount(or at the point of decoding ingetCalendarLeafAllocationData):
require(_unlockTimestamps.length == _unlockAmounts.length, "Array length mismatch"); for (uint256 i = 1; i < _unlockTimestamps.length; i++) { require(_unlockTimestamps[i-1] < _unlockTimestamps[i], "Timestamps not strictly increasing"); }- Since data is from trusted Merkle roots, these checks could be optional or emit events on failure for monitoring. Alternatively, enforce sanity off-chain during Merkle tree construction, but document the assumption clearly.
- Update backend tools/scripts to validate schedules before root generation.
- Add explicit validations in
-
I-06 Informational Unused Variable Computation Best Practices Resolved
Description
In the
_getVestedAmountfunction of theIntervalVesterabstract contract, the lineuint256 vestingEndTimestamp = _getPieceEndTime(schedule.pieces[schedule.pieces.length - 1]);calculates the end timestamp of the last piece in the unlock schedule by invoking_getPieceEndTimeon the final element of thepiecesarray. This computation converts the result (auint32) to auint256and assigns it to a local variablevestingEndTimestamp. However, this variable is never referenced or utilized anywhere else in the function body, the return statement, or subsequent logic.This constitutes dead code, which may be a remnant from an earlier implementation where the end timestamp was used (e.g., to cap the
finalTimestampor perform additional checks). While harmless in terms of correctness (the function still accurately sums vested amounts across pieces), it incurs unnecessary computational overhead and increased gas costs.Recommendation
Remove the line entirely to eliminate dead code and associated overhead. If the computation was intended for a purpose (e.g., validating the schedule's overall end time or capping vesting), reintroduce it with explicit usage, such as
require(finalTimestamp <= vestingEndTimestamp, "InvalidTimestamp");or integrating it into the return cap.
No findings match.
More from Magna
All 9 reportsPut your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.