msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: _calculateClaim() does not distribute boost emissions correctly
Severity: high

Description:
The function `_calculateClaim()` is responsible for the calculations of the amount of emissions a specific veSatin is entitled to claim. The idea is to distribute emissions only to veSatin tokens locked for more than minLockDurationForReward and only for the extra time the veSatin is locked for on top of minLockDurationForReward. As an example, if minLockDurationForReward is set to 6 months a veSatin locked for 7 months would receive emissions for 1 month and a veSatin locked for 5 months would receive no emissions at all. To do so the following code is executed in a loop, where every loop calculates the amount of emissions the veSatin accumulated during a specific week, in chronological order:\\n```\\n      if ((lockEndTime - oldUserPoint.ts) > (minLockDurationForReward)) {\\n        toDistribute +=\\n          (balanceOf * tokensPerWeek[weekCursor]) / veSupply[weekCursor];\\n      weekCursor += WEEK;\\n      }\\n```\\n\\nThe code distributes the rewards if the elapsed time between lockEndTime (the locking end timestamp) and oldUserPoint.ts is bigger than minLockDurationForReward. However, oldUserPoint.ts is the timestamp of the last user action on a veSatin, for example depositing LP by calling `increaseAmount()`. As an example, a user that locks his veSatin and does nothing else will receive rewards for the whole locking duration. In contrast, a user that performs one action a week would only receive rewards for the locking duration minus minLockDurationForReward

Impact:

Recommended mitigation:
The variable weekCursor should be used instead of oldUserPoint.ts in the if condition:\\n```\\n      if ((lockEndTime - weekCursor) > (minLockDurationForReward)) {\\n```\\n
Vulnerability: Users will be unable to claim emissions from veSatin tokens if they withdraw it or merge it
Severity: high

Description:
The function `_calculateClaim()` uses the variable lockEndTime when checking if a veSatin is entitled to emissions for a particular week (code with mitigation from TRST-H-1):\\n```\\n           if ((lockEndTime - weekCursor) > (minLockDurationForReward)) {\\n            toDistribute +=\\n        (balanceOf * tokensPerWeek[weekCursor]) / veSupply[weekCursor];\\n        weekCursor += WEEK;\\n       }\\n```\\n\\nHowever lockEndTime is set to 0 whenever a user withdraws a veSatin by calling `withdraw()` or merges one by calling `merge()`. When this is the case the operation lockEndTime - weekCursor underflows, thus reverting. This results in users being unable to claim veSatin emissions if they withdraw or merge it first

Impact:

Recommended mitigation:
In the `withdraw()` and `merge()` functions, call `claim()` in VeDist.sol to claim emissions before setting the lock end timestamp to 0. In `merge()` this is only necessary for the veSatin passed as _from
Vulnerability: It's never possible to vote for new pools until setMaxVotesForPool() is called
Severity: high

Description:
The function `_vote()` allows voting on a pool only when the current amount of votes plus the new votes is lower or equal to the value returned by _calculateMaxVotePossible():\\n```\\n        require(_poolWeights <= _calculateMaxVotePossible(_pool), "Max votes exceeded");\\n```\\n\\nHowever, `_calculateMaxVotePossible()` returns 0 for every pool in which the variable maxVotesForPool has not been initialized, thus making `_vote()` revert:\\n```\\n          return ((totalVotingPower * maxVotesForPool[_pool]) / 100);\\n```\\n

Impact:

Recommended mitigation:
In `createGauge()` and `createGauge4Pool()` set maxVotesForPool for the pool the gauge is being created for to 100.
Vulnerability: The protocol might transfer extra SATIN emissions to veSatin holders potentially making SatinVoter.sol insolvent
Severity: high

Description:
The function `_distribute()` in SatinVoter.sol is generally responsible for distributing weekly emissions to a gauge based on the percentage of total votes the associated pool received. In particular, it's called by `updatePeriod()` (as per fix TRST-H-4) on the gauge associated with the Satin / $CASH pool. The variable veShare is set to be equal to the returned value of `calculateSatinCashLPVeShare()`, which is calculated as the percentage of Satin / $CASH LP times claimable[gauge] and represents the amount of SATIN that will be transferred to VeDist.sol when checkpointing emissions in checkpointEmissions():\\n```\\n      uint _claimable = claimable[_gauge];\\n      if (SATIN_CASH_LP_GAUGE == _gauge) {\\n            veShare = calculateSatinCashLPVeShare(_claimable);\\n      _claimable -= veShare;\\n      }\\n      if (_claimable > IMultiRewardsPool(_gauge).left(token) && _claimable / DURATION > 0) {\\n          claimable[_gauge] = 0;\\n      if (is4poolGauge[_gauge]) {\\n              IGauge(_gauge).notifyRewardAmount(token, _claimable, true);\\n      } else {\\n                IGauge(_gauge).notifyRewardAmount(token, _claimable, false);\\n           }\\n      emit DistributeReward(msg.sender, _gauge, _claimable);\\n        }\\n```\\n\\nHowever, when the if condition (_claimable > IMultiRewardsPool(_gauge).left(token) && _claimable / DURATION > 0) is false the variable claimable[_gauge] will not be set to 0, meaning the next time veShare will be calculated it will include emissions that have already been distributed, potentially making SatinVoter.sol insolvent

Impact:

Recommended mitigation:
Adjust claimable[gauge] after calculating veShare and calculate veShare only if the msg.sender is SatinMinter.sol to prevent potential attackers from manipulating the value by repeatedly calling _distribute():\\n```\\n    if (SATIN_CASH_LP_GAUGE == _gauge && msg.sender == minter) {\\n        veShare = calculateSatinCashLPVeShare(_claimable);\\n           claimable[_gauge] -= veShare;\\n              _claimable -= veShare;\\n        }\\n```\\n
Vulnerability: It's possible to drain all the funds from ExternalBribe
Severity: high

Description:
The function `earned()` is used to calculate the amount rewards owed to a tokenId, to do so it performs a series operations over a loop and then it always executes:\\n```\\n    Checkpoint memory cp = checkpoints[tokenId][_endIndex];\\n      uint _lastEpochStart = _bribeStart(cp.timestamp);\\n        uint _lastEpochEnd = _lastEpochStart + DURATION;\\n    if (block.timestamp > _lastEpochEnd) {\\n            reward += (cp.balanceOf * \\n         tokenRewardsPerEpoch[token][_lastEpochStart]) /\\n           supplyCheckpoints[getPriorSupplyIndex(_lastEpochEnd)].supply;\\n```\\n\\nwhich adds to reward the amount of rewards earned by the tokenId during the last epoch in which it was used to vote, but only if that happened at least a week prior (block.timestamp > _lastEpochEnd). Because of this, it's possible to call `earned()` multiple times in a row with a tokenId that voted more than a week before to drain the contract funds.

Impact:

Recommended mitigation:
The function `earned()` is taken from the Velodrome protocol and is known to have issues. Because it uses the convoluted logic of looping over votes to calculate the rewards per epoch instead of looping over epochs, we recommend using the Velodrome fixed implementation, which we reviewed:\\n```\\n      function earned(address token, uint tokenId) public view returns (uint) {\\n      if (numCheckpoints[tokenId] == 0) {\\n          return 0;\\n      }\\n        uint reward = 0;\\n          uint _ts = 0;\\n           uint _bal = 0;\\n             uint _supply = 1;\\n               uint _index = 0;\\n          uint _currTs = _bribeStart(lastEarn[token][tokenId]); // take epoch last claimed in as starting point\\n      _index = getPriorBalanceIndex(tokenId, _currTs);\\n          _ts = checkpoints[tokenId][_index].timestamp;\\n              _bal = checkpoints[tokenId][_index].balanceOf;\\n      // accounts for case where lastEarn is before first checkpoint\\n        _currTs = Math.max(_currTs, _bribeStart(_ts));\\n      // get epochs between current epoch and first checkpoint in same epoch as last claim\\n      uint numEpochs = (_bribeStart(block.timestamp) - _currTs) / DURATION;\\n      if (numEpochs > 0) {\\n      for (uint256 i = 0; i < numEpochs; i++) {\\n      // get index of last checkpoint in this epoch\\n      _index = getPriorBalanceIndex(tokenId, _currTs + DURATION);\\n      // get checkpoint in this epoch\\n      _ts = checkpoints[tokenId][_index].timestamp;\\n          _bal = checkpoints[tokenId][_index].balanceOf;\\n      // get supply of last checkpoint in this epoch\\n            _supply = supplyCheckpoints[getPriorSupplyIndex(_currTs + DURATION)].supply;\\n      reward += _bal * tokenRewardsPerEpoch[token][_currTs] / _supply;\\n            _currTs += DURATION;\\n             }\\n           }\\n      return reward;\\n        }\\n```\\n
Vulnerability: Division by 0 can freeze emissions claims for veSatin holders
Severity: medium

Description:
The function `_calculateClaim()` is responsible for the calculations of the amount of emissions a specific veSatin is entitled to claim. In doing so, this code is executed (code with mitigation from TRST-H-1):\\n```\\n    if ((lockEndTime - weekCursor) > (minLockDurationForReward)) {\\n         toDistribute +=\\n            (balanceOf * tokensPerWeek[weekCursor]) / veSupply[weekCursor];\\n    weekCursor += WEEK;\\n         }\\n```\\n\\nThe variable veSupply[weekCursor] is used as a denominator without checking if it's 0, which could make the function revert. If the protocol ever reaches a state where veSupply[weekCursor] is 0, all the claims for veSatin that were locked during that week would fail for both past and future claims. The same issue is present in the function `_calculateEmissionsClaim()`

Impact:

Recommended mitigation:
Ensure veSupply[weekCursor] is not 0 when performing the division.
Vulnerability: BaseV1Pair could break because of overflow
Severity: medium

Description:
In the function _update(), called internally by `mint()`, `burn()` and `swap()`, the following code is executed:\\n```\\n    uint256 timeElapsed = blockTimestamp - blockTimestampLast;\\n     // overflow is desired\\n    if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {\\n      reserve0CumulativeLast += _reserve0 * timeElapsed;\\n        reserve1CumulativeLast += _reserve1 * timeElapsed;\\n     }\\n```\\n\\nThis is forked from UniswapV2 source code, and it's meant and known to overflow. It works fine if solidity < 0.8.0 is used but reverts when solidity >= 0.8.0 is used. If this happens all the core functionalities of the pool would break, including `mint()`, `burn()`, and `swap()`.

Impact:

Recommended mitigation:
Wrap the operation around an unchecked{} block so that when the variable overflows it loops back to 0 instead of reverting.
Vulnerability: createGauge4Pool() lacks proper checks and/or access control
Severity: medium

Description:
The function createGauge4Pool() can be called by anybody at any time and is used to create a Gauge for a special pool, the 4pool. It takes 5 parameters as inputs:\\n```\\n    function createGauge4pool(\\n       address _4pool,\\n          address _dai,\\n            address _usdc,\\n               address _usdt,\\n             address _cash\\n       ) external returns (address) {\\n```\\n\\nNone of the parameters are properly sanitized, meaning _dai, _usdc, _usdt, _cash could be any whitelisted token and not necessarily DAI, USDC, USDT, and cash while _4pool could be any custom contract, including a malicious one. The function also sets the variable FOUR_POOL_GAUGE_ADDRESS to the newly created gauge, overwriting the previous value.

Impact:

Recommended mitigation:
Make the function only callable by an admin, and if it can be called multiple times, turn the variable FOUR_POOL_GAUGE_ADDRESS to a mapping from address to boolean to support multiple 4 pools.
Vulnerability: The logic in _calculateClaim() can leave some tokens locked and waste gas
Severity: low

Description:
The function `_calculateClaim()` is responsible for the calculations of the amount of emissions a specific veSatin is entitled to claim. To do so, this code is executed in a loop for each week from the current timestamp to the last claim (code with mitigation from TRST-H-1):\\n```\\n    if ((lockEndTime - weekCursor) > (minLockDurationForReward)) {\\n             toDistribute +=\\n         (balanceOf * tokensPerWeek[weekCursor]) / veSupply[weekCursor];\\n      weekCursor += WEEK;\\n    }\\n```\\n\\nWhen the if condition is not met two things happen:\\nAn amount of emissions that was supposed to be distributed ((balanceOf * tokensPerWeek[weekCursor]) / veSupply[weekCursor])) is skipped, meaning it will stay locked in the contract.\\nThe function `_calculateClaim()` will loop for the maximum number of times (50), because weekCursor is not increased, wasting users' gas.

Impact:

Recommended mitigation:
When the if condition is not met burn the tokens that were supposed to be distributed and exit the loop. Since the non-distributed tokens would stay locked it's not strictly necessary to burn them.
Vulnerability: More than one hat of the same hatId can be assigned to a user
Severity: high

Description:
Hats are minted internally using `_mintHat()`.\\n```\\n        /// @notice Internal call to mint a Hat token to a wearer\\n        /// @dev Unsafe if called when `_wearer` has a non-zero balance of `_hatId`\\n        /// @param _wearer The wearer of the Hat and the recipient of the  newly minted token\\n        /// @param _hatId The id of the Hat to mint\\n        function _mintHat(address _wearer, uint256 _hatId) internal {\\n            unchecked {\\n        // should not overflow since `mintHat` enforces max balance of 1\\n            _balanceOf[_wearer][_hatId] = 1;\\n        // increment Hat supply counter\\n        // should not overflow given AllHatsWorn check in `mintHat` ++_hats[_hatId].supply;\\n        }\\n        emit TransferSingle(msg.sender, address(0), _wearer, _hatId, 1);\\n        }\\n```\\n\\nAs documentation states, it is unsafe if _wearer already has the hatId. However, this could easily be the case when called from `mintHat()`.\\n```\\n        function mintHat(uint256 _hatId, address _wearer) public returns (bool) {\\n        Hat memory hat = _hats[_hatId];\\n            if (hat.maxSupply == 0) revert HatDoesNotExist(_hatId);\\n        // only the wearer of a hat's admin Hat can mint it\\n             _checkAdmin(_hatId);\\n            if (hat.supply >= hat.maxSupply) {\\n                 revert AllHatsWorn(_hatId);\\n                     }\\n        if (isWearerOfHat(_wearer, _hatId)) {\\n                revert AlreadyWearingHat(_wearer, _hatId);\\n                      }\\n        _mintHat(_wearer, _hatId);\\n             return true;\\n                  }\\n```\\n\\nThe function validates _wearer doesn't currently wear the hat, but its balance could still be over 0, if the hat is currently toggled off or the wearer is not eligible. The impact is that the hat supply is forever spent, while nobody actually received the hat. This could be used maliciously or occur by accident. When the hat is immutable, the max supply can never be corrected for this leak. It could be used to guarantee no additional, unfriendly hats can be minted to maintain permanent power.

Impact:

Recommended mitigation:
Instead of checking if user currently wears the hat, check if its balance is over 0.
Vulnerability: TXs can be executed by less than the minimum required signatures
Severity: high

Description:
In HatsSignerGateBase, `checkTransaction()` is the function called by the Gnosis safe to approve the transaction. Several checks are in place.\\n```\\n        uint256 safeOwnerCount = safe.getOwners().length;\\n             if (safeOwnerCount < minThreshold) {\\n                 revert BelowMinThreshold(minThreshold, safeOwnerCount);\\n        }\\n```\\n\\n```\\n           uint256 validSigCount = countValidSignatures(txHash, signatures, signatures.length / 65);\\n        // revert if there aren't enough valid signatures\\n             if (validSigCount < safe.getThreshold()) {\\n              revert InvalidSigners();\\n                  }\\n```\\n\\nThe first check is that the number of owners registered on the safe is at least minThreshold. The second check is that the number of valid signatures (wearers of relevant hats) is not below the safe's threshold. However, it turns out these requirements are not sufficient. A possible situation is that there are plenty of owners registered, but currently most do not wear a hat. `reconcileSignerCount()` could be called to reduce the safe's threshold to the current validSigCount, which can be below minThreshold. That would make both the first and second check succeed. However, minThreshold is defined to be the smallest number of signers that must come together to make a TX. The result is that a single signer could execute a TX on the safe, if the other signers are not wearers of hats (for example, their toggle has been temporarily set off in the case of multi-hat signer gate.

Impact:

Recommended mitigation:
Add another check in `checkTransaction()`, which states that validSigCount >= minThreshold.
Vulnerability: Target signature threshold can be bypassed leading to minority TXs
Severity: high

Description:
`checkTransaction()` is the enforcer of the HSG logic, making sure signers are wearers of hats and so on. The check below makes sure sufficient hat wearers signed the TX:\\n```\\n        uint256 validSigCount = countValidSignatures(txHash, signatures, signatures.length / 65);\\n                // revert if there aren't enough valid signatures\\n        if (validSigCount < safe.getThreshold()) {\\n                     revert InvalidSigners();\\n         }\\n```\\n\\nThe issue is that the safe's threshold is not guaranteed to be up to date. For example, initially there were 5 delegated signers. At some point, three lost eligibility. `reconcileSignerCount()` is called to update the safe's threshold to now have 2 signers. At a later point, the three signers which lost eligibility regained it. At this point, the threshold is still two, but there are 5 valid signers, so if targetThreshold is not below 5, they should all sign for a TX to be executed. That is not the case, as the old threshold is used. There are various scenarios which surface the lack of synchronization between the wearer status and safe's stored threshold.

Impact:

Recommended mitigation:
Call `reconcileSignerCount()` before the validation code in `checkTransaction()`.
Vulnerability: maxSigners can be bypassed
Severity: high

Description:
maxSigners is specified when creating an HSG and is left constant. It is enforced in two ways -targetThreshold may never be set above it, and new signers cannot register to the HSG when the signer count reached maxSigners. Below is the implementation code in HatsSignerGate.\\n```\\n        function claimSigner() public virtual {\\n             if (signerCount == maxSigners) {\\n                revert MaxSignersReached();\\n        }\\n        if (safe.isOwner(msg.sender)) {\\n                revert SignerAlreadyClaimed(msg.sender);\\n            }\\n        if (!isValidSigner(msg.sender)) {\\n             revert NotSignerHatWearer(msg.sender);\\n         }\\n         _grantSigner(msg.sender);\\n           }\\n```\\n\\nAn issue that arises is that this doesn't actually limit the number of registered signers. Indeed, signerCount is a variable that can fluctuate when wearers lose eligibility or a hat is inactive. At this point, `reconcileSignerCount()` can be called to update the signerCount to the current valid wearer count. A simple attack which achieves unlimited claims is as follows:\\nAssume maxSigners = 10\\n10 signers claim their spot, so signerCount is maxed out\\nA signer misbehaves, loses eligibility and the hat.\\nreconcile() is called, so signerCount is updated to 9\\nA new signer claims, making signerCount = 10\\nThe malicious signer behaves nicely and regains the hat.\\nreconcile() is called again, making signerCount = 11\\nAt this point, any eligible hat wearer can claim their hat, easily overrunning the maxSigners restriction.

Impact:

Recommended mitigation:
The root cause is that users which registered but lose their hat are still stored in the safe's owners array, meaning they can always get re-introduced and bump the signerCount. Instead of checking the signerCount, a better idea would be to compare with the list of owners saved on the safe. If there are owners that are no longer holders, `removeSigner()` can be called to vacate space for new signers.
Vulnerability: Attacker can DOS minting of new top hats in low-fee chains
Severity: medium

Description:
In Hats protocol, anyone can be assigned a top hat via the `mintTopHat()` function. The top hats are structured with top 32 bits acting as a domain ID, and the lower 224 bits are cleared. There are therefore up to 2^32 = ~ 4 billion top hats. Once they are all consumed, `mintTopHat()` will always fail:\\n```\\n          // uint32 lastTopHatId will overflow in brackets\\n             topHatId = uint256(++lastTopHatId) << 224;\\n```\\n\\nThis behavior exposes the project to a DOS vector, where an attacker can mint 4 billion top hats in a loop and make the function unusable, forcing a redeploy of Hats protocol. This is unrealistic on ETH mainnet due to gas consumption, but definitely achievable on the cheaper L2 networks. As the project will be deployed on a large variety of EVM blockchains, this poses a significant risk.

Impact:

Recommended mitigation:
Require a non-refundable deposit fee (paid in native token) when minting a top hat. Price it so that consuming the 32-bit space will be impossible. This can also serve as a revenue stream for the Hats project.
Vulnerability: Linking of hat trees can freeze hat operations
Severity: medium

Description:
Hats support tree-linking, where hats from one node link to the first level of a different domain. This way, the amount of levels for the linked-to tree increases by the linked-from level count. This is generally fine, however lack of checking of the new total level introduces severe risks.\\n```\\n        /// @notice Identifies the level a given hat in its hat tree\\n        /// @param _hatId the id of the hat in question\\n        /// @return level (0 to type(uint8).max)\\n     function getHatLevel(uint256 _hatId) public view returns (uint8) {\\n```\\n\\nThe `getHatLevel()` function can only return up to level 255. It is used by the `checkAdmin()` call used in many of the critical functions in the Hats contract. Therefore, if for example, 17 hat domains are joined together in the most stretched way possible, It would result in a correct hat level of 271, making this calculation revert:\\n```\\n        if (treeAdmin != 0) {\\n                 return 1 + uint8(i) + getHatLevel(treeAdmin);\\n            }\\n```\\n\\nThe impact is that intentional or accidental linking that creates too many levels would freeze the higher hat levels from any interaction with the contract.

Impact:

Recommended mitigation:
It is recommended to add a check in `_linkTopHatToTree()`, that the new accumulated level can fit in uint8. Another option would be to change the maximum level type to uint32.