Hyperware
Cantina Security Report
Organization
- @Hyperware
Engagement Type
Cantina Reviews
Period
-
Repositories
Researchers
Findings
Medium Risk
5 findings
5 fixed
0 acknowledged
Low Risk
23 findings
21 fixed
2 acknowledged
Informational
23 findings
22 fixed
1 acknowledged
Gas Optimizations
11 findings
11 fixed
0 acknowledged
Medium Risk5 findings
vHYPR tokens received after vestingEndTimestamp might not be claimable
State
Severity
- Severity: Medium
Submitted by
Gerard Persoon
Description
Function
claim()removessHYPRtokens onevestingEndTimestamphas been reached.However after that time, new
sHYPRtokens can still be received viatransfer()or by claiming a merkledrop.In that situation
userTotalClaimed_[]still has its value which means the newsHYPRtokens can't be claimed.Recommendation
Consider resetting
userTotalClaimed_[]to 0 whensHYPRtokens are removed.Note: resetting
userTotalClaimed_[]to 0 could possibly enable transfers that were earlier considered not possible (because of userTotalClaimed_ > requiredClaimedAmount). However not an issues because at moment vesting has ended anyway.mint doesn't account for claimed tokens
State
Severity
- Severity: Medium
Submitted by
Gerard Persoon
Description
Function
claim()transfersHYPRtokens out (after the cliff). In that situation the test of L73 doesn't work as expected any more and too few tokens will be able to minted.Recommendation
Consider changing the code to:
-if (totalMinted + amount > _balance) {+if (totalMinted + amount > _balance + totalClaimed) { revert NotEnoughHyprSupplyToMint();}newDuration can be lower than lock.duration
State
Severity
- Severity: Medium
Submitted by
Gerard Persoon
Description
Function
manageLock()usesunchecked, however this can be abused to change thenewDurationto a value that is lower thanlock.duration. This would allow unlocked tokens earlier than expected.Proof of Concept
Here is POC to show the issue:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "hardhat/console.sol"; contract test { uint256 public constant MAX_LOCK_DURATION = 4 * 365 days; uint256 public constant MIN_LOCK_DURATION = 7 days; function _min(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { return a < b ? a : b; } } function _validateDuration(uint256 _duration) internal pure { if (_duration < MIN_LOCK_DURATION || _duration > MAX_LOCK_DURATION) { revert (); } } constructor() { uint _duration = type(uint256).max - MAX_LOCK_DURATION + MIN_LOCK_DURATION+1; uint lock_duration = MAX_LOCK_DURATION; if (_duration > 0) { // Increase lock duration uint256 newDuration; unchecked { newDuration = lock_duration + _duration; } _validateDuration(newDuration); console.log("orginal duration %d days",lock_duration / 1 days); // 1460 console.log("new duration %d days",newDuration / 1 days); // 7 } } }Recommendation
Remove the
uncheckedfor the calculation ofnewDuration.Entrypoint version mismatch
State
Severity
- Severity: Medium
Submitted by
Gerard Persoon
Description
Contract
AccountincludesBaseAccountfromentryPointversion 0.7, while the constant_entryPointuses version 0.6. As these are incompatible, the Account Abstraction logic will not work.Also a newer version of the
entryPointis released.Recommendation
Consider using entryPoint version 0.8 in both the include and the constant.
Several issues with the creation of the _DOMAINSEPARATOR
State
Severity
- Severity: Medium
Submitted by
Gerard Persoon
Description
There are several issues with the creation of the
_DOMAINSEPARATOR:- EIP712Domain contains an extra space rights before
bytes32 salt, this will lead to a different hash; - there is no
saltadded inabi.encode(); address(this)references the address of the template contract and not of the proxy.
Recommendation
Consider using EIP712Upgradeable.
This uses a
TYPE_HASHwithout asalt, but that is fine because thesaltisn't used.bytes32 private constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
Low Risk23 findings
Potential infinite loop in constructor of HyperToken
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
In the
constructorofHyperToken, acontinueis done if(to == address(0) || amount == 0). In that situation the loop counter isn't incremented and the loop would continue until it is out of gas.Recommendation
Move the
i++inside of theforstatement. Theuncheckedisn't necessary any more since solidity version 0.8.22.The functions transfer() and transferFrom() can return false which isn't best practice
State
- Fixed
PR #77
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
The functions
transfer()andtransferFrom()can returnfalse. However this isn't best practice because it can be overlooked by users of the contract.Although the use of
SafeERC20/safeTransfer()/safeTransferFrom()fixes this, it is safer to use the best practice.Also see weird-erc20.
For example function
claim()usestransfer()without usingsafeTransfer().Recommendation
Consider using
revert()on failure oftransfer()andtransferFrom().Function claim() uses transfer()
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
The function
claim()usestransfer()to transfer HyperTokens. However if that tokens would returnfalselike this contract does, then potentially errors would not be detected.Recommendation
Consider using
safeTransfer().Not all init functions are called
State
- Fixed
PR #77
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Several init functions of (underlying) upgradable contracts are not called. Currently they are all empty, so the risk is low. However future version might have code.
Recommendation
Consider also calling:
In
MerkleDistributor::initialize():- __ReentrancyGuard_init
- __ERC165_init
- __Context_init
In
TokenRegistry::initialize():- __ERC165_init()
- __Context_init()
In
Hypermap::initialize():- __UUPSUpgradeable_init()
- __ERC165_init()
- __Context_init()
Multiple the same merkleRoots could be added accidentally
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Function
create()allow to accidentally add the samemerkleRootmultiple times. This could be for the same token or for different tokens.It also allows adding multiple different
merkleRoots for the same token.Recommendation
Consider disallowing to add multiple the same
merkleRoots are added. Also consider only allowing onemerkleRootper token.Indexes larger than or equal to count can be used
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Functions claim() doen't check that
_dIndex < count. In theory this could allow claiming tokens in the case that the root is0. However its very unlikely that a root of 0 will be valid.Function
setAlias()also doesn't check that_dIndex < count. This could allow setting an alias for futureroots.For consistency it good to check for this.
Recommendation
Consider disallowing
_dIndex >= countin functionsclaim()andsetAlias().Incorrect use of _validateDuration() in transferRegistration()
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Function
transferRegistration()checks_durationfor validity but in the situation ofdstBind.amount ! = 0,_durationindicates a duration extention which is added todstBind.endTime.This way it is not possible to extend the
durationwith an amount smaller thanMIN_LOCK_DURATION(7 days)Recommendation
Consider changing the code to something like:
function transferRegistration(...) ... { ...- if (_duration > 0) {- _validateDuration(_duration);- } ...}function _transferRegistration( ... if (dstBind.amount == 0) { ...+ _validateDuration(_duration); ... } else { ... if (_duration > 0) {+ _validateDuration(dstBind.endTime + _duration - block.timestamp); unchecked { dstBind.endTime = _min(_lockEndTime, dstBind.endTime + _duration); } } ... }}Function withdraw() doesn't always remove processed entries
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Function
withdraw()processes the first 20namehashs and remove them from the list when processed so the next call towithdraw()will process the next 20.However in the edge case that
bind.amount == 0, the entries are not removed, which could potentially lead to not being able to process all entries.This situation shouldn't happen but it still safer to always remove the processed entries.
Recommendation
Consider always doing
_userBinds[].remove(...);.Function manageLock() doesn't emit BindCreated
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Function
_transferRegistration()emitsBindCreatedfor all destionations when_binds[][].amountis set.However function
manageLock()doesn't do this when_binds[][DEFAULT_REGISTRATION_NAMEHASH].amountis set.Recommendation
Consider doing an
emit BindCreated()in functionmanageLock().Difference between emit LockIncreased() and emit BindAmountIncreased()
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Function
manageLock()emits the final value (e.g.lock.amount) inemit LockIncreased(). However function_transferRegistration()emits the increase (e.g._maxAmount) inemit BindAmountIncreased(). This is inconsistent.Recommendation
Consider changing the code to:
-emit BindAmountIncreased(_user, _dstNamehash, _maxAmount, dstBind.endTime);+emit BindAmountIncreased(_user, _dstNamehash, dstBind.amount, dstBind.endTime);Not all upgradable contracts have _disableInitializers()
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Not all upgradable contracts have
_disableInitializers(). This is recommended to use on the template contracts to prevent aunautorized parties to initialize the contracts and potential selfdestruct them on chains where this is still possible.Recommendation
Consider adding the following to
HyperAccountMinterUpgradable,HyperAccountUpgradable,Hypermap.constructor() { _disableInitializers();}Check in function _isValidLabel() isn't robust
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
The check in function
_isValidLabel()isn't robust. Whenlen == 0andoffset == 1then thisifwon't trigger.In the current code this isn't an issue, but it potentially could be with future changes. It could be made more robust.
Recommendation
Consider changing the code to:
-if (len == offset) revert LabelTooShort();+if (offset >= len) revert LabelTooShort();Function supportsInterface() doesn't include functions of current contract
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Function
supportsInterface()ofHypermapdoesn't include the functions of the current contract.Recommendation
Consider changing the code to:
-return super.supportsInterface(interfaceId);+return super.supportsInterface(interfaceId) || interfaceId == type(IHypermap).interfaceId;msg.sender in CommitHash() and Account Abstraction / mintBySignature()
State
- Fixed
PR #77
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
The
_getCommitHash()could be guessed when using Account Abstraction because themsg.senderis always the same and the name could be a well known name. That way the commit could be front run.Additional in case of Account Abstraction and because the
msg.senderis always the same, an NFT can be minted from someone else.On the other hand when using
mintBySignature(), themsg.sendercould be any party so might not match the commit.Recommendation
For Account Abstractions consider storing
userOp.senderinAccount::_validateSignature()in transient storage and use that as thesenderwhen verifying the commit.For
mintBySignature()use thesigneras thesenderwhen verifying the commit.Hyperware
Solved for
_mintBySignature()and acknowledged for Account Abstraction.msg.sender of withdraw() might be unexpected
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
In function
withdraw()funds are send to themsg.sender, however themsg.sendercould have an unexpected value.In case of
account abstractionthemsg.senderis theEntryPoint.If the funds are send there then the orignal sender doesn't receive them. Although the funds can be retrieved via exec() and
EntryPoint::withdrawStake().Recommendation
Consider doing one of the following:
- specify a destination
- when
msg.sender == entrypoint: revert and retrieve if viaexec() - when
msg.sender == entrypoint: send it to the owner of the NFT - store
userOp.senderinAccount::_validateSignature()in transient storage and use that as destination
msg.sender of HyperAccountPermissionedMinter::_mint() might be unexpected
State
- Fixed
PR #77
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
In function
HyperAccountPermissionedMinter::_mint()actions are taken based on themsg.sender, however themsg.sendercould have an unexpected value.In case of Account Abstraction the
msg.senderis theentryPoint. In case of_mintBySignature()thenmsg.sendercan be any user.Recommendation
For Account Abstraction consider storing
userOp.senderinAccount::_validateSignature()in transient storage and use that as the parameter for the_allowances[].For
mintBySignature()use thesigneras the parameter for the_allowances[].Hyperware
Solved for
_mintBySignature()and acknowledged for Account Abstraction.HyperAccountPaidMinter::_mint() won't work with mintBySignature
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
HyperAccountPaidMinter::_mint()isn'tpayableso it won't work withmintBySignature.However its unlikely that someone else will pay, unless it is the
entrypoint.Recommendation
Doublecheck to usefulness of
mintBySignaturein combination withHyperAccountPaidMinter::_mint(). If this is relevant make the functionmintBySignaturepayable.Hyperware
Acknowledged. This combination will not be used.
_initialized is overwritten
State
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
The variable
_initializedofHyperAccountProxyis located onstoragelocation 0. So an implementation contract, which is used viadelegatecall, is very likely to overwrite this.For example
HyperAccountMinterUpgradablewill overwrite this with_nonce[][]that is also located onstoragelocation 0.Due to the other checks in
initialize()its unlikely this will cause issues, however if still safer to solve this.Recommendation
Consider doing one of the following:
- eliminate the use of
_initialized, this can be done because the checks withmsg.sender == HYPERMAPand_implementation() == address(0)are sufficient; - use a ERC-7201 namespace.
supportsInterface() not implemented everywhere
State
- Fixed
PR #77
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Several contracts don't extend the
supportsInterface()function with their owninterfaceId.Recommendation
Consider also extending
supportsInterface()in the attached contracts.Flawed encoding due to dynamic types allow an attacker to pass signature verification using malicious values
State
Severity
- Severity: Low
Submitted by
hash
Description
The message hash is currently constructed as
keccak256(abi.encodePacked(to, address(this), _nonce[signer], name, initialization, implementation, signer));Since both
nameandinitializationare of dynamic type, the same final encoded bytes can be attributed to multiple combinationsEg:
bytes name1 = "hello"; bytes initialization1 = " world"; bytes name2 = "he"; bytes initialization2 = "llo world"; constructor() { // both results in `hello world` console.logBytes32(keccak256(abi.encodePacked(name1, initialization1))); // 0x471732... console.logBytes32(keccak256(abi.encodePacked(name2, initialization2))); // 0x471732... }Hence this allows an attacker to pass the signature verification using values different from what was signed
Recommendation
Use hashes of
nameandinitializationinsteadkeccak256(abi.encodePacked(to, address(this), _nonce[signer], keccak256(name), keccak256(initialization), implementation, signer))Lack of protection against gene updation can cause some user's to pay unwillingly
State
- Acknowledged
Severity
- Severity: Low
Submitted by
hash
Description
In
HyperAccountPaidMinter, users are supposed to pay in-order to mint. And in case the parent doesn't have any gene set, they expect theTBAto be created with the passed inimplementation.function _mint(address to, bytes calldata name, bytes calldata initialization, address implementation) internal override returns (address tba) { if (msg.value != currentPrice) revert IncorrectPayment(); return _HYPERMAP.mint(to, name, initialization, implementation); }}But if the parent sets a gene in between an user viewing the current state and the execution of the user's transaction, it will cause the
TBAto be created with thegeneinstead of the passed inimplementationcausing users to unwillingly pay.function mint(address to, bytes calldata label, bytes calldata initialization, address implementation) external isName(label) returns (address tba) { ....... if (map[parent].gene != address(0)) { implementation = map[parent].gene; setGene = true; }Recommendation
In case the
implementationandgeneare both non-zero and is not the same, instead of overwriting the implementation revert the execution.Hyperware
Acknowledged. The timing is indeed an undesirable edge-case, but the behavior as it exists is clearly specified throughout our discussion of the feature (i.e. that an implementation given by the minter will be overridden).
Approved transfers can be DOS'd since 3rd party claiming is allowed
State
Severity
- Severity: Low
Submitted by
hash
Description
In order to perform an approved transfer, the user's total claimed amount must equal the approved
requiredClaimedAmountfunction transfer(address to, uint256 amount) public override(IERC20, ERC20) returns (bool) { => if (params.used || params.to != to || params.amount != amount || userTotalClaimed_[msg.sender] != params.requiredClaimedAmount) { return false; }This is flawed as any user can claim on behalf of another user increasing their
totalClaimed[]. This will make the user unable to perform the approved transfersfunction claim(address _user) external { uint256 _balance = balanceOf(_user); uint256 _claimed = userTotalClaimed_[_user]; uint256 _available = _availableToClaim(_balance, _claimed); if (_available != 0) {=> userTotalClaimed_[_user] += _available; totalClaimed += _available;Another way a transfer can get disabled (without the owner themselves making claims) is via an inbound transfer which increases the
userTotalClaimed_[]. But this should be rareRecommendation
Either disallow claiming on behalf of others or remove the
requiredClaimedAmountcondition for transfersRequired preconditions in _transferRegistration are not suitable for pure duration extensions
State
Severity
- Severity: Low
Submitted by
hash
Description
In order to extend the duration of a binding, the user has to make use of the
_transferRegistrationfunction. But the kept preconditions are that the source binding should be expired (or be DEFAULT_REGISTRATION_NAMEHASH) and have non-zero balancefunction _transferRegistration( address _user, uint256 _lockEndTime, bytes32 _srcNamehash, bytes32 _dstNamehash, uint256 _maxAmount, uint256 _duration ) internal { Bind storage srcBind = _binds[_user][_srcNamehash]; if (_srcNamehash != DEFAULT_REGISTRATION_NAMEHASH && srcBind.endTime > block.timestamp) { revert SourceNotExpired(_srcNamehash, srcBind.endTime); } uint256 availableBalance = srcBind.amount; if (availableBalance == 0) revert InsufficientBindAmount(_srcNamehash, 0, _maxAmount);This disallows extending the duration of a binding in case there are no other expired binding (or DEFAULT_REGISTRATION_NAMEHASH binding) with non-zero balance
Recommendation
Add a separate function that handles just the duration extension
Informational23 findings
_totalSupply could be replaced with totalSupply()
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The constructor of
HyperTokentracks the total number of tokens minted via_totalSupply. However the ERC20 contract also tracks this intotalSupply(), so this could be used instead. This would simplify the code.Recommendation
Consider replacing
_totalSupplywithtotalSupply(), in the following way:constructor(...) ... { ...- uint256 _totalSupply; ... for (uint256 i = 0; i < len; ) { ...- unchecked {- _totalSupply += amount;- } ... }- if (_totalSupply != MAX_SUPPLY) {+ if (totalSupply() != MAX_SUPPLY) { revert InvalidTotalSupply(); }}Limiting transfers doesn't fully stop selling vHYPR
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Function
transfer()tries to limit the the sell ofvHYPRtokens by limiting the transfer. However there are others ways to make thevHYPR tokensthat originate from an merkle drop liquid/transferable.The
vHYPRtokens can beclaim()ed into a seperate contract that is an ERC20/ERC4626/ERC6551 contract, and then ownership of these contracts, or shares can be transferred.Recommendation
Consider adding a comment to the code for some who reuses the code and isn't aware of this.
Entries in transferRoleParams[] can be deleted after use
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
HyperVestingTokenflag that a version oftransferRoleParams[]is used by settingparams.used = true. However the params are not used afterward so could also be deleted.Recommendation
Consider removing
transferRoleParams[]once it is used.Checks on transferRoleParams[] could be more thorough
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The values of
transferRoleParams[]are compared to the supplied parameters. However it isn't explicitly checked the enty isn't empty.Due to this a transfer with
to==0andamount==0could be accepted, alhough it doesn't do a meaningful action.Currently the check for
to==0in_transfer()prevents this, but its safer to an explicit check.Recommendation
Consider adding
|| to == 0to the check:-if (... || params.to != to || ... ) ...+if (... || params.to != to || to == 0 || ... ) ...Function transfer() could divide by 0
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
If the
balanceOf(msg.sender)would be 0, and then L115 would revert due to a divsion by zero without a clear error message. Although this situation is unlikely it could be detected and reverted with a better error message.Recommendation
Consider detecting the situation where
balanceOf(msg.sender) == 0.Two functions named claim()
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
There are two functions named
claim(), one inMerkleDistributorand one inHyperVestingToken.A user should first do
claim()on theMerkleDistributorand then lateronclaim()on theHyperVestingToken. This might be confusing to documentRecommendation
Consider changing the name in
HyperVestingTokento something likeconvert(). Also consider renaming all other references toclaimin that contract.Use of super not necessary
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The function
claim()usessuper, but it doesn'toverridethe function, sosuperisn't necessary.Recommendation
Consider changing the code to:
-super._burn(_user, _balance);+_burn(_user, _balance);Edge case when block.timestamp == vestingCliffTimestamp == vestingEndTimestamp
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Edge case: when
block.timestamp == vestingCliffTimestamp == vestingEndTimestampthen function_totalVestedTokens()will return 0. However in other situations whereblock.timestamp == vestingEndTimestampit will return_vestingTokenBalance.This will most likely be negligible because the full amount will be present in the next call, after one second.
Recommendation
If you do want to change this, consider checking for
vestingEndTimestampfirst.Error message in function claim() not accurate
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
An error message in function
claim()indicates all tokens are claimed. However the situation only indicates there are insufficient tokens.Recommendation
Consider changing the code to:
-revert AllClaimed(_dIndex, root.totalAmount);+revert NotEnoughTokensLeft(_dIndex, root.totalAmount);Not immediately clear why unchecked calculation is safe
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Function does an unchecked calculation on
_binds[][].amount. It is not immediately clear why this is safe.Recommendation
Consider add a comment that
_amountwon't be so large to induce an overflow because that would revert insafeTransferFrom().Solidity style guide not followed
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Functions in Hypermap are not ordered in the recommended way by the Solidity style guide.
This makes the code more difficult to read.
Recommendation
Consider changing the order of the functions.
Events don't show the hash
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Several events show the unhashed version of a variable twice, instead of showing both the hashed and the unhashed version.
Recommendation
Consider changing the events to:
-event <eventname>(..., bytes indexed labelhash, ...);+event <eventname>(..., bytes32 indexed namehash, ...);And hash the values that are put in the emit.
genes and upgradeable contracts
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Genes are meant to enforce the same implementation contract for the current TBA/leaf and all TBA/leafs below it.
However some contracts allow upgrades and thus should not be used in combination with genes:
- contracts that have
_authorizeUpgrade() - contracts that have
delegatecall
Recommendation
Document that contracts that enable upgrades are not used in combination with gene.
Duplicate code for _exec()
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The function
_exec()is duplicated only to have a differentmodifier. Code duplication makes the code more difficult to maintain, which is especially important for a core function like_exec().Recommendation
Consider having a function that is used in a
modifierthat can be overriden to prevent code duplicationSignature with v==0
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
In several other implementation of signature checks, they allow for values of 0, 1 of v as valid values. If such a value would be present then the signature would not be interpreted correctly.
Recommendation
Consider using a different signal value for
v. Check EIP 155 for possible values.Hyperware
Acknowledged. No action at this time.
Checks on _minCommitAge and _maxCommitAge
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The constructor of
HyperAccountCommitMinterdoesn't do any checks on_minCommitAgeand_maxCommitAge.However there should be a reasonable time between
_minCommitAgeand_maxCommitAgeto be able to mint at all.Recommendation
Consider checking something like:
_maxCommitAge - _minCommitAge > 1 days.Use storage variables from address 0 and use of __gaps
State
- Fixed
PR #77
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The upgradable contracts use
storagestarting from slot 0 and use__gaps. This is cumbersome and allows for storage collisions. Some contract are upgradeable but don't have__gaps, these are:HyperAccountCommitMinterandHyperAccountMinterUpgradable.Most recent contracts use the EIP-7201 storage pattern. This also removes the need for
__gaps.Recommendation
Consider using the EIP-7201 storage pattern for all the contracts the use upgradability, including the
HyperAccountProxy.mintBySignature() and reverting _mint()
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
In function
mintBySignature(), when_mint()reverts, the_nonce[]increase is also undone. If the_mint()always reverts, then no further_mint()s can be done until a newsignatureis made using the samenonce._mint()could revert in different ways, see the attached code.Recommendation
Consider documenting how a reverting
_mint()should be handled by the account usingmintBySignature().Unused imports
State
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The import of
IERC721isn't used inMech.Recommendation
Consider removing the unused imports.
No-op inside initialize function
State
Severity
- Severity: Informational
Submitted by
hash
Description
Since the
DEFAULT_ADMIN_ROLEis never granted to msg.sender,_revokeRole(DEFAULT_ADMIN_ROLE, msg.sender);is a no-op.Recommendation
Remove the redundant line
- _revokeRole(DEFAULT_ADMIN_ROLE, msg.sender);BindCreated/BindAmountIncreased events are not emitted inside manageLock
State
Severity
- Severity: Informational
Submitted by
hash
Description
The
BindCreated/BindAmountIncreasedevents are not emitted inside manageLock when the default binding is created for the first time or amount added to itRecommendation
Emit the events or be consistent and not emit events for default binding always
Bind removal doesn't emit event
State
Severity
- Severity: Informational
Submitted by
hash
Description
When a bind is removed, currently no event is emitted
Recommendation
Emit an event
Custom error doesn't have named parameters
State
Severity
- Severity: Informational
Submitted by
hash
Description
The
InsufficientLockAmounterror doesn't have named parametersRecommendation
Add named parameters
Gas Optimizations11 findings
More gas efficient function can be used in claim()
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
Function claim() uses
MerkleProof.verify()A more gas efficient function of this function is also present.Recommendation
Consider using
verifyCalldata():-if (!MerkleProof.verify(...)) { ... }+if (!MerkleProof.verifyCalldata(...)) { ... }A more gass efficient version of ReentrancyGuardUpgradeable can be used
State
- Fixed
PR #77
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
There is a more gass efficient version of
ReentrancyGuardUpgradeablewhich can be used.Recommendation
Consider using ReentrancyGuardTransientUpgradeable.
Redundant call to contains()
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
In function
manageLock()callscontains()before doingadd(). Howeveradd()also does this, so the check is redundant.Recommendation
Consider changing the code to:
- if (!_userBinds[msg.sender].contains(DEFAULT_REGISTRATION_NAMEHASH)) { _userBinds[msg.sender].add(DEFAULT_REGISTRATION_NAMEHASH);-}Function withdraw() updates the storage variable defaultBind.amount` in a loop
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
Function
withdraw() updates the storage variabledefaultBind.amount` in a loop, which is relatively expensive.Recommendation
Considure using a temporaty variable, for example in the following way.
function withdraw() external nonReentrant { ... Bind storage defaultBind = _binds[msg.sender][DEFAULT_REGISTRATION_NAMEHASH];+ uint256 amount = defaultBind.amount; ... for (uint256 i = 0; i < workingSize; ++i) { ... - defaultBind.amount += bindAmount;+ amount += bindAmount; ... }- uint256 amount = defaultBind.amount; ... // use amount defaultBind.amount = 0; ...}entry.data always empty in get() of a name
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
If the
namehashis anamethen the third return value (entry.data) is always empty. So this function could return an empty bytes array. This makes the code easier to understand and also saves some gas.Recommendation
Consider changing the code to:
-return (entry.tba, ownerOf(uint256(namehash)), entry.data);+return (entry.tba, ownerOf(uint256(namehash)), bytes(""));Assembly blocks don't use "memory-safe"
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
The assembly blocks don't use "memory-safe". Having these allows the compiler to generate more efficient code.
Recommendation
Consider using the following:
-assembly { ... }+assembly ("memory-safe") { ... }Function leaf() can be optimized
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
There are several ways to optimize the assembly code in
leaf():- the extra
add(..., 0x20)on L263 doesn't seem necessary; - the second
keccak256can use thescratch spaceof address0x00..0x39; - because
labelis limited to maximum 64 bytes, they can also fit in thescratch space.
Recommendation
The three optimizations can be implemented in the following way:
function leaf(bytes32 parenthash, bytes calldata label) public pure returns (bytes32 namehash) { if (label.length >= 64) revert(); // important to prevent overwrite of address 0x40 return _leaf(parenthash,label);) // internal version without the length check, which can be called from mint, fact, notefunction _leaf(bytes32 parenthash, bytes calldata label) internal pure returns (bytes32 namehash) { assembly ("memory-safe") { calldatacopy(0x00, label.offset, label.length) let labelhash := keccak256(0x00, label.length) mstore(0x00, parenthash) mstore(0x20, labelhash) namehash := keccak256(0x00, 0x40) }}Function _isOperatorOrDelegatee() can be simplified
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
Function
get()always returns an emptyownerfor anode, so function_isOperatorOrDelegatee()can be simplified.Recommendation
Consider changing the code to:
-(, address owner, bytes memory data) = _HYPERMAP.get(alLeaf);+(,, bytes memory data) = _HYPERMAP.get(alLeaf);...-if (owner == address(0)) { owner = IERC721(tokenContract).ownerOf(tokenId);-}Duplication of the last element can be done before the loop
State
- Fixed
PR #77
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
The duplication of the last element can also be done before the loop (although execution speed difference is probably neclectible)
Recommendation
Consider changing the code to something like:
while len(level) > 1: next_level = []+ # If there's an odd number of elements, duplicate the last one+ if len(level) % 2 == 1:+ level.append(level[-1]) for i in range(0, len(level), 2): left = level[i]- # If there's an odd number of elements, duplicate the last one- right = level[i + 1] if i + 1 < len(level) else left+ right = level[i + 1] parent = standard_node_hash(left, right) next_level.append(parent) level = next_levelAlso consider doing this in
generate_proof()Function generate_proof() can be simplified
State
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
Function
generate_proof()uses the arrayindexes[]which makes the code more difficult to understand.Also the calculation of
target_indexcan be simplified.Recommendation
Consider changing the code to something like:
while len(level) > 1: next_level = [] for i in range(0, len(level), 2): left = level[i] right = level[i + 1] if i + 1 < len(level) else left # Add to the next level next_level.append(standard_node_hash(left, right)) # If this is the target, add the sibling to the proof if i == target_index: proof.append(right) elif i + 1 < len(level) and i + 1 == target_index: proof.append(left) target_index //= 2 level = next_levelreturn proofExtra condition added to always allow default binding can be removed
State
Severity
- Severity: Gas optimization
Submitted by
hash
Description
Since
DEFAULT_REGISTRATION_NAMEHASHbinding will always have endTime == 0 (which will be < block.timestamp), the single condition ofsrcBind.endTime > block.timestampwill disallow expired bindings while always allowing the default binding. Hence the extra condition added to explicitly allow the default binding is unnecessaryRecommendation
Remove
_srcNamehash != DEFAULT_REGISTRATION_NAMEHASH