Guardian's review of ERC-20 Native and Token for Aria, published November 2025. The report records 33 findings across 2 review rounds, including 1 medium and 19 low.
- Published
- Review window
- October 29 to November 5, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Story, BNB Chain
- Sector
- Real-world assets
- 0 Critical
- 0 High
- 1 Medium
- 19 Low
- 13 Informational
Scope
7 files in scope · 323 nSLOC
| File | nSLOC | Lines |
|---|---|---|
contracts/claim/erc20-native/ERC20NativeClaim.sol | 24 | 45 |
contracts/claim/erc20-native/vesting/BaseVesting.sol | 71 | 141 |
contracts/claim/erc20-native/vesting/CliffReleaseVesting.sol | 73 | 142 |
contracts/claim/erc20-native/vesting/SnapshotVesting.sol | 32 | 58 |
contracts/claim/erc20-native/shared/Claim.sol | 14 | 23 |
contracts/claim/erc20-native/shared/ClaimAdmin.sol | 95 | 142 |
contracts/claim/erc20-native/shared/MerkleWhitelist.sol | 14 | 18 |
Findings 33
Main Review
26 findings · October 29 to 30, 2025-
M-01 Medium Token Configuration DoS DoS Resolved
Description
The token defaults to address(0) (native). While token remains unset, anyone can call deposit() to fund the contract with ETH and, if whitelisted in the merkle tree and within the window, call claim() to withdraw ETH to themselves. Because setToken() is permanently disabled after totalClaimed > 0, a single successful ETH claim irreversibly locks the distribution to native token, preventing the admin from switching to the intended ERC20. An attacker can front-run by depositing ETH and claiming as soon as the window opens if the admin forgets to set the token first.
Recommendation
Require explicit token configuration before
startTimeor use a sentinel value instead ofaddress(0). -
L-01 Low Invalid ClaimAdmin__ZeroAddress Revert Best Practices Resolved
Description
When
token_ != token, the functionsreleasableandvestedAmountrevert withClaimAdmin__ZeroAddress(), which does not represent the true reason for the error.Recommendation
Change the revert name to match the cause of the error.
-
L-02 Low Underflow When Checking Releasable Warning Resolved
Description
Function
releasablerisks underflow if an admin were to change the cliff withsetCliffsuch thatvestedAmount(token_, uint64(block.timestamp))becomes zero andtotalClaimedis positive. This would block integrators from viewing the true amount of releasable funds.Recommendation
Do not move the cliff after claims have been made.
-
L-03 Low Fee-on-transfer And Rebase Tokens Not Supported Warning Resolved
Description
Fee-on-transfer and rebasing tokens are not currently supported by the system. For example,
Claim._claimdelivers the exact_amount. If the configured token is fee-on-transfer / rebasing / otherwise non-standard, the hook still creditsclaimedandtotalClaimedwith the full_amounteven though the user receives less (or more).Recommendation
Restrict tokens to plain ERC-20/native asset.
-
L-04 Low Zero Claims Allowed Events Resolved
Description
ERC20NativeClaim.claimallows a claim amount of zero, hence the event Claimed could be emitted repeatedly as_claimChecksonly validates thatif (claimed[msg.sender] != 0).Recommendation
Either block zero claim amounts or clearly document this behavior.
-
L-05 Low Missing whenNotPaused Modifiers Validation Acknowledged
Description
The
depositfunctions of theClaimAdmincontract are callable by everyone but are not pausable.Recommendation
Consider to add
whenNotPausedto all external functions to follow best practices. -
L-06 Low Reliance On Contract Balance Risks Warning Acknowledged
Description
If the admin withdraws tokens, the live balance shrinks. The recomputed
totalReleaseddrops as well, and for users who already claimed based on the previous total allocation the newuserTotalReleasedcan fall below their historical claimed amount, which makes_toClaimrevert withBaseVesting__AllocationDecreased.Also note that with direct contract donations or whenever contract balance exceeds the total allocated, the contract immediately treats the higher balance as though it had been part of the original allocation. The linear curve
_releasetherefore jumps up, so every user can instantly claim their proportional share of the donation instead of the donation vesting over the remaining duration. Users will also pull more than their recorded allocation, and total claims will exceed total allocation/total snapshotted.Recommendation
Use a fixed ledger of “total allocated” tokens that is only ever updated by explicit funding events instead of reliance on
.balanceorbalanceOf. Then_toClaimwill not ignore the passed parameters. -
L-07 Low Start and End Time Movement DoS Claims DoS Partially resolved
Description
setStartTimeandsetEndTimeremain callable by the admin after launch. Because every claim runs_checkStartTime() / _checkEndTime(), pushingstartTimeforward or pullingendTimeback once users have begun claiming immediately bricks the flow for everyone else.There is no guard comparable to
totalClaimed > 0, so a routine timetable adjustment moves the eligible window outside the current block timestamp and all claim calls revertRecommendation
Consider preventing start and end time updates after claims begun.
-
L-08 Low Cliff Release Percentage Change Leads To DoS DoS Resolved
Description
The admin may lower
cliffReleasePercentageat any time. After claims have started, the new curve in_releasepushes the expected total release at the current timestamp below what has already been paid out. When_toClaimrecomputes each user’s,userTotalReleasedbecomes smaller thanclaimed[msg.sender]so every subsequent claim reverts withBaseVesting__AllocationDecreased. This permanently DoSes all claimants.Recommendation
Forbid percentage decreases after
totalClaimed > 0 -
L-09 Low Allocation Update Leads To DoS DoS Resolved
Description
Updating allocations after any user has claimed can also DoS existing claimants. Function
updateAllocationsadjuststotalAllocated, which is later used as the denominator in_toClaim. If an admin onboards a new wallet or increases some other wallet once another user has already claimed, the recalculated shareuserTotalReleased = userAmount * totalReleased / totalAmountshrinks for the existing claimer, making it less thanclaimed[msg.sender]and triggeringBaseVesting__AllocationDecreasedon every future claim. In practice, a single late allocation can freeze all earlier participants.Furthermore, any allocation decreases after other users have already claimed risk an ERC20InsufficientBalance revert since old
totalClaimedhistory is maintained even if the allocations have changed or the admin withdrew tokens.Recommendation
Disallow allocation changes once totalClaimed > 0 or vesting begins.
-
L-10 Low Updating Start Time Will DoS DoS Resolved
Description
Raising startTime once vesting has begun makes
_releaserecompute with a later start, decreasing totalReleased. The next claim hits the same AllocationDecreased revert, freezing everyone until the original start time is restored.Recommendation
Prevent startTime from moving forward once vesting begins.
-
L-11 Low Linear Duration Can Underflow DoS Resolved
Description
If an admin shortens
endTimebelow the current cliff, the subtraction inlinearDurationunderflows:endTime - cliff;Consequently, any
_releasefunction call reverts until the admin fixes the timestamps.Recommendation
Modify
setEndTimeto ensure the newendTimedoesn't fall below the current cliff. -
L-12 Low DoS On Merkle Root Change DoS Resolved
Description
Updating the Merkle root after anyone has claimed reruns
_toClaimwith the new (snapshot,total) pair. If the refreshed leaf lowers either value,userTotalReleaseddrops belowclaimed[msg.sender], so every subsequent claim reverts withBaseVesting__AllocationDecreased. This bricks all prior claimants even when the admin is just trying to admit new wallets or fix a typo in a snapshot amount.Recommendation
Lock the root once
totalClaimed > 0or clearly document this risk. -
L-13 Low Native Claims To Smart Contract May Fail Warning Partially resolved
Description
Claims always transfer to msg.sender. If the whitelisted address is a smart contract without a payable receive/fallback, native token transfers will revert and the user cannot ever claim. There is no option to specify an alternative recipient address.
Recommendation
Consider adding a claim variant to allow a different recipient. Otherwise, clearly document this behavior.
-
L-14 Low depositedRewards Unused Documentation Resolved
Description
The
depositedRewardswhich tracks deposits is never used and is not an accurate representation of how much funds are available for vesting. Also note that the passed_tokendoes not have to match the token being vested out.Recommendation
Clearly document this behavior.
-
L-15 Low Total Allocated Not Used For Claim Calculation Warning Resolved
Description
The
_toClaimfunction relies onbalance + totalClaimedfor calculation rather thantotalAllocated. Consequently, users can pull more funds out than their intended allocation.Recommendation
Clearly document this is intended behavior.
-
L-16 Low Snapshot Leaves Must Be Consistent Warning Resolved
Description
Each Merkle leaf encodes both the user’s snapshotAmount and a caller-supplied
totalSnapshoted. During claim, the contract takes that same pair and computes the payout as snapshotAmount /totalSnapshoted. If whoever prepares the snapshot accidentally (or intentionally) gives one address a smallertotalSnapshotedthan everyone else, the proof will still verify and that address will receive a disproportionately large share of the vested tokens.Recommendation
Ensure all leaves have the same
totalSnapshotted. -
I-01 Informational Unecessary Calculation Gas Optimization Resolved
Description
In function
_vestingSchedule, whenstartTimeequals the timestamp,_timePoints()returns -1 (requires calculation) but_release()simply calculates: (totalAllocation * 0) / duration() = 0This case could be shortcutted in
_timePoints().Recommendation
If
timestamp <= startTimereturn 0 timePoints. -
I-02 Informational Temporary Negative Allocation Reverts Warning Resolved
Description
The
updateAllocations()function validates that each user's allocation is non-negative immediately after each iteration, rather than after all updates are processed. Consequently, multi-step allocation adjustments where a user's allocation temporarily goes negative before being corrected to be positive will revert.Recommendation
Clearly document this behavior.
-
I-03 Informational Lack Of Zero Address Check On Admin Validation Resolved
Description
The initializer does not validate that _admin is non-zero. If initialized with address(0), no one will hold DEFAULT_ADMIN_ROLE, which disables all core functionality.
Recommendation
Add a zero-address check for _admin and revert with
ClaimAdmin__ZeroAddress()if_admin == address(0). -
I-04 Informational Unused Import Superfluous Code Resolved
Description
The
IERC20interface is imported in theSnapshotVesting.solfile but not used.Recommendation
Consider to remove unused code.
-
I-05 Informational Allocations Made For Zero Address Warning Resolved
Description
Function
updateAllocationsacceptswallet = address(0)in the Allocation struct, which will strand a portion oftotalAllocated.Recommendation
Ensure allocations are appropriately passed by the
DEFAULT_ADMIN_ROLEor add contract-level validation. -
I-06 Informational 100% Vesting At Cliff Not Possible Best Practices Resolved
Description
Function
_checkCliffReleasePercentageforbids setting the percentage of the cliff allocation unlock to 100%. If the product requirement includes “full unlock at cliff", it will not be supported.Recommendation
Consider if this is expected behavior and clearly document if so.
ERC20NativeClaimcan be used for 100% claims but a merkle root needs to be created. -
I-07 Informational Potential Overflow On Released Calculation Warning Resolved
Description
The calculation
uint256 userTotalReleased = (userAmount * totalReleased) / totalAmount;can potentially overflow when performing(userAmount * totalReleased)on large values before being able to reach the division.Recommendation
Consider using
mulDivor bound allocation amounts appropriately. -
L-17 Low Grant/Revoke Role Always Emits Event Events Resolved
Description
Function
_grantRoleignores the boolean returned byEnumerableSet.add, so re‑granting a role that an account already holds still emitsRoleGranted. Similarly,_revokeRolehas the same issue on removal — a failed remove (account never had the role) still emitsRoleRevoked. This may confuse off-chain indexers relying on events to reflect actual membership changes.Recommendation
Clearly document this behavior.
-
I-08 Informational Streamlining Storage Warning Resolved
Description
Both ERC7201 namespaced storage is used and plain storage is used, so care must be taken with upgrades.
Recommendation
Consider sticking with just ERC7201 namespaced storage.
Remediation Review
7 findings · November 5, 2025-
L-01 Low Invalid ClaimAdmin__ZeroAddress Revert Best Practices Resolved
Description
The function
vestedAmountreverts whentoken_ != $.setup.tokenwithClaimAdmin__ZeroAddress(), which does not represent the true reason for the error.Recommendation
Change the revert name to
BaseVesting__TokenMismatch. -
L-02 Low Arbitrary Token Can Be Depositted Validation Resolved
Description
As noted in , a
_tokenthat is not the token being vested can be deposited by users which will credit towardsdepositedRewardsand emit aDepositedevent. This may lead to issues where a user deposits the valid vesting token, the admin changes the vesting token, but users continue to deposit the old vesting token.Recommendation
Consider restricting
function deposit(address _token, uint256 _amount)so that the_tokenmust match the$.setup.token. -
I-01 Informational startTime After cliff Is Possible Validation Resolved
Description
It is possible to set the
startTimeafter thecliffwith thesetStartTimefunction which does not make sense.Recommendation
Consider to revert in that case.
-
I-02 Informational PausableUpgradable Not Used Best Practices Resolved
Description
Currently the ClaimAdmin uses
Pausablefrom solidstate instead ofPausableUpgradeablefrom OZ which is inconsistent with the other inherited contracts:AccessControlEnumerableUpgradeableandUUPSUpgradeable.Recommendation
Consider using OZ's PausableUpgradeable or clearly document this.
-
I-03 Informational Small Amounts Rounds Down To 0 Warning Resolved
Description
BaseVesting floors both the stream and per-user share. Consequently, tiny allocations therefore vest to zero and never cross the BaseVesting__ZeroReleased guard, leaving some users unable to claim and producing stranded dust.
Recommendation
Clearly document this behavior.
-
I-04 Informational Redundant Check Best Practices Resolved
Description
Function
setMerkleRootvalidates that the_merkleRootis not empty bytes withif (_merkleRoot == bytes32(0)) revert MerkleWhitelist__ZeroMerkleRoot();. However,_setMerkleRootperforms the exact same check:if (_merkleRoot == bytes32(0)) return;Recommendation
Consider removing the redundant check.
-
I-05 Informational Invalid Comment For Native Token Documentation Resolved
Description
"The token to be claimed -
address(0)for native token, otherwise ERC20." is invalid since the native token is represented by theNATIVE_SENTINEL_ADDR = 0x000000000000000000000000000000000000dEaDRecommendation
Update the comment to appropriately reflect the native token.
No findings match.
More from Aria
Put 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.
