Farseen0/scar-pairs huggingface dataset:
HIGH severity: Malicious users can double their voting power.. **Description:** When calculating The `voteWeight` of a delegatee, its own `ownVoteWeight` is added. This makes it possible for some users to double their voting power by constructing a cycle. For example, Alice delegates Bob, Bob delegates Alice.

**Impact:** Malicious users can double their...

Vulnerable code:
function getDelegatedVoteWeight(address _delegate) external view returns (uint256) {
        // check address is delegate
        uint256 ownVoteWeight = _voteWeight(_delegate, _balances[_delegate]);
        address callerDelegate = userDelegates[_delegate];
        if (!delegates[_delegate] || callerDelegate == msg.sender) { return ownVoteWeight; }
@>      return ownVoteWeight + _voteWeight(_delegate, _delegateBalances[_delegate]);
    }

-   function unsetUserVoteDelegate() external override {
+   function unsetUserVoteDelegate() public override {

    function setSelfAsDelegate(bool _approve) external override {
            [...]
+       if (_approve && !prevStatusTrue) unsetUserVoteDelegate();
    }

function setUserVoteDelegate(address _delegate) external override {
        if (_delegate == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); }
        if (!delegates[_delegate]) {  revert InvalidDelegate(); }
+       if (userDelegates[msg.sender] == msg.sender) { revert(); }
            [...]
    }
LOW severity: In staking contract, resetting self delegation will reset vote weight to zero. **Description:** In `TempleGoldStaking` contract, when a delegate resets self delegation, the vote weights delegated to the delegate will be removed by calling `_updateAccountWeight`.
Since zero is passed as new balance parameter, the `stakingTime` of vote weight is reset to zero:

This means that.....

Vulnerable code:
// TempleGoldStaking.sol:L580
if (_newBalance == 0) {
    t = 0;
    _lastShares = 0;
}
MEDIUM severity: Incorrect end time setting for spice auction. **Description:** In `startAuction` function of `SpiceAuction` contract, the end time is set wrong.

Currently, `endTime` is set using `block.timestamp`, not `startTime` calculated above.

**Impact:** The auction end time becomes shorter than expected.

**Recommended Mitigation:** `endTime` should...

Vulnerable code:
uint128 startTime = info.startTime = uint128(block.timestamp) + config.startCooldown;
uint128 endTime = info.endTime = uint128(block.timestamp) + config.duration;

uint128 endTime = info.endTime = startTime + config.duration;
LOW severity: `TempleGoldStaking.getVoteWeight()` calculates vote weight of an account incorrectly. **Description:** The `TempleGoldStaking.sol` contract, the function is expected to return the vote power of account, but sometimes it is calculated incorrectly.
It uses current balance of account from L379.

However, in the `_voteWeight()` function, it uses previous vote weight when `week >...

Vulnerable code:
File: contracts\templegold\TempleGoldStaking.sol
378:     function getVoteWeight(address account) external view returns (uint256) {
379:         return _voteWeight(account, _balances[account]);
380:     }

File: contracts\templegold\TempleGoldStaking.sol
549:     function _voteWeight(address _account, uint256 _balance) private view returns (uint256) {
550:         /// @dev Vote weights are always evaluated at the end of last week
551:         /// Borrowed from st-yETH staking contract (https://etherscan.io/address/0x583019fF0f430721aDa9cfb4fac8F06cA104d0B4#code)
552:         uint256 currentWeek = (block.timestamp / WEEK_LENGTH) - 1;
553:         AccountWeightParams storage weight = _weights[_account];
554:         uint256 week = uint256(weight.weekNumber);
555:         if (week > currentWeek) {
556:             weight = _prevWeights[_account];
557:         }
558:         uint256 t = weight.stakeTime;
559:         uint256 updated = weight.updateTime;
560:         if (week > 0) {
561:             t += (block.timestamp / WEEK_LENGTH * WEEK_LENGTH) - updated;
562:         }
563:         return _balance * t / (t + halfTime);
564:     }
LOW severity: Rewards are calculated as distributed even if there are no stakers, locking the rewards forever. **Description:** The `TempleGoldStaking.sol` contract is a fork of the Synthetix rewards distribution contract, with slight modifications. The code special-cases the scenario where there are no users, by not updating the cumulative rate when the _totalSupply is zero, but it does not include such a.....

Vulnerable code:
File: contracts\templegold\TempleGoldStaking.sol
475:     function _rewardPerToken() internal view returns (uint256) {
476:         if (totalSupply == 0) {
477:             return rewardData.rewardPerTokenStored;
478:         }
479:
480:         return
481:             rewardData.rewardPerTokenStored +
482:             (((_lastTimeRewardApplicable(rewardData.periodFinish) -
483:                 rewardData.lastUpdateTime) *
484:                 rewardData.rewardRate * 1e18)
485:                 / totalSupply);
486:     }

File: contracts\templegold\TempleGoldStaking.sol
245:     function distributeRewards() updateReward(address(0)) external {
246:         if (distributionStarter != address(0) && msg.sender != distributionStarter)
247:             { revert CommonEventsAndErrors.InvalidAccess(); }
248:         // Mint and distribute TGLD if no cooldown set
249:         if (lastRewardNotificationTimestamp + rewardDistributionCoolDown > block.timestamp)
250:                 { revert CannotDistribute(); }
251:         _distributeGold();
252:         uint256 rewardAmount = nextRewardAmount;
253:         if (rewardAmount == 0 ) { revert CommonEventsAndErrors.ExpectedNonZero(); }
254:         nextRewardAmount = 0;
+            if (totalSupply == 0) { revert CommonEventsAndErrors.NoStaker(); }
255:         _notifyReward(rewardAmount);
256:     }

File: contracts\common\CommonEventsAndErrors.sol
06: library CommonEventsAndErrors {
07:     error InsufficientBalance(address token, uint256 required, uint256 balance);
08:     error InvalidParam();
09:     error InvalidAddress();
10:     error InvalidAccess();
11:     error InvalidAmount(address token, uint256 amount);
12:     error ExpectedNonZero();
13:     error Unimplemented();
+       error NoStaker();
14:     event TokenRecovered(address indexed to, address indexed token, uint256 amount);
15: }
LOW severity: While changing vote delegator from himself to another user, `_delegateBalances` should not be decreased. **Description:** In `setUserVoteDelegate` function, when a user set vote delegator to himeself, his `_delegateBalances` value doesn't contain his `userBalance` from L149.
When he changes vote delegator from himself to another user, it subtracts `userBalance` from `_delegateBalances` from L145. But.....

Vulnerable code:
**Impact:** `_delegateBalances` is not calculated correctly.

**Recommended Mitigation:** It is recommended to check if previous delegated user is not self.
HIGH severity: Math error in creator payment calculation. **Description:** The Tunnl protocol applies 3 kinds of fees.
- **Flat Fee**: The advertiser pays a flat fee and it is charged on acceptance of offers.
- **Advertiser Percentage Fee**: The advertiser percentage fee is the percentage fee that is charged to the advertiser. It is an extra amount added.....

Vulnerable code:
uint256 maxCreatorPayment = uint256((s_offers[offerId].maxValueUsdc - s_offers[offerId].flatFeeUsdc) * 10000)
            / uint256(10000 + s_config.creatorFeePercentageBP);//@audit-issue this should be 10000+s_config.advertiserFeePercentageBP

        console.log("maxCreatorPayment: %s", maxCreatorPayment);//@audit-info

        bytes[] memory bytesArgs = new bytes[](4);
        bytesArgs[0] = abi.encode(offerId);
        bytesArgs[1] = abi.encodePacked(s_offers[offerId].creationDate);
        bytesArgs[2] = abi.encodePacked(maxCreatorPayment);//@audit-info maximum offer amount relayed
        bytesArgs[3] = abi.encodePacked(s_offers[offerId].offerDurationSeconds);
        req.setBytesArgs(bytesArgs);

        bytes32 requestId = _sendRequest(
            req.encodeCBOR(),
            s_config.functionsSubscriptionId,
            s_config.functionsCallbackGasLimit,
            s_config.functionsDonId
        );

function test_PayOut() public {
        // Define the amount to be paid in USDC
        uint256 amountPaidUsdc = uint256(uint256(100e6));
        test_Verification_Success();
        // Warp time for payout and perform Chainlink automation
        vm.warp(block.timestamp + (1 weeks - 100));
        performUpkeep();
        // Fulfill request for payout  with PayOutamount
        mockFunctionsRouter.fulfill(
            address(tunnlTwitterOffers),
            functionsRequestIds[offerId],
            abi.encode(amountPaidUsdc),//@audit-info should be same to the maxCreatorPayout in the current implementation
            ""
        );

        // Assert balances after payout
        assertEq(mockUsdcToken.balanceOf(advertiser), 0);
        assertEq(mockUsdcToken.balanceOf(address(tunnlTwitterOffers)), (0));
        assertEq(
            mockUsdcToken.balanceOf(contentCreator),
            amountPaidUsdc - (amountPaidUsdc * tunnlTwitterOffers.getConfig().creatorFeePercentageBP) / 10000
        );
    }

console.log("maxCreatorPayment: %s", maxCreatorPayment);
MEDIUM severity: Incomplete implementation of state transition from Pending to Expired. **Description:** All offers follow a strict state transition defined by the protocol. We extracted all possible state transitions based on the conditionals that define the possible states before changing into a new state.

Looking at the implementation of the function `performUpkeep()`, we can see.....

Vulnerable code:
TunnlTwitterOffers.sol
322:     function performUpkeep(bytes calldata performData) external override {
323:         bytes32[] memory offersToUpkeep = abi.decode(performData, (bytes32[]));
324:
325:         for (uint256 i = 0; i < offersToUpkeep.length; i++) {//@audit-info checks only the offersToUpkeep
326:             bytes32 offerId = offersToUpkeep[i];
327:
328:             if (_needsVerificationRequest(s_offers[offerId].status, s_offers[offerId].dateToAttemptVerification)) {
329:                 s_offers[offerId].status = Status.VerificationInFlight;
330:                 sendFunctionsRequest(offerId, RequestType.Verification);
331:             } else if (_needsExpiration(s_offers[offerId].status, s_offers[offerId].payoutDate, s_offers[offerId].acceptanceExpirationDate)) {
332:                 // Update status before making the external call
333:                 Status previousStatus = s_offers[offerId].status;
334:                 s_offers[offerId].status = Status.Expired;
335:                 s_offersToUpkeep.remove(offerId);
336:                 if (previousStatus == Status.Pending) {//@audit-info Pending -> Expired
337:                     // If expired BEFORE offer is accepted & funds have been locked in escrow, "revoke" the allowance by sending the advertiser's funds back to themselves
338:                     _revokeAllowanceForPendingOffer(offerId);
339:                 } else {
340:                     // If expired AFTER offer is accepted & funds have been locked in escrow, release the funds in escrow back to the advertiser
341:                     uint256 amountToTransfer = s_offers[offerId].maxValueUsdc - s_offers[offerId].flatFeeUsdc;
342:                     usdcToken.safeTransfer(s_offers[offerId].advertiser, amountToTransfer);
343:                 }
344:                 emit OfferStatus(offerId, previousStatus, Status.Expired);
345:             } else if (_needsPayoutRequest(s_offers[offerId].status, s_offers[offerId].payoutDate)) {
346:                 s_offers[offerId].status = Status.PayoutInFlight;
347:                 sendFunctionsRequest(offerId, RequestType.Payout);
348:             }
349:         }
350:     }
MEDIUM severity: Attackers can prevent users creating an offer by frontrunning. **Description:** The protocol allows anyone to create offers with an offer ID provided by the user. While this design gives users the flexibility to choose their offer ID, it also introduces a problem.

The `createOffer` function checks if an offer with the provided ID already exists and reverts...

Vulnerable code:
TunnlTwitterOffers.sol
152:     function createOffer(bytes32 offerId, uint256 maxPaymentUsdc, uint32 acceptanceDurationSeconds, uint32 _offerDurationSeconds) external {
153:
154:         require(offerId != bytes32(0), "Invalid offerId");
155:         require(s_offers[offerId].creationDate == 0, "Offer already exists"); //@audit Can be abused to prevent offer creation
156:         require(acceptanceDurationSeconds >= s_config.minAcceptanceDurationSeconds, "AcceptanceDuration is too short");
157:         require(_offerDurationSeconds >= s_config.minOfferDurationSeconds, "OfferDuration is too short");

function test_FrontrunCreateOffer() public {
        address alice = address(0x10); // malicious actor
        address bob = address(0x20); // legitimate actor

        uint32 acceptanceDurationSeconds = tunnlTwitterOffers.getConfig().minAcceptanceDurationSeconds;
        uint offerAmount = 1e6;
        uint maxValueUsdc = tunnlTwitterOffers.getConfig().flatFeeUsdc + offerAmount;
        uint minAllowance = tunnlTwitterOffers.getConfig().flatFeeUsdc;

        // alice allows minimal amount of USDC to the contract
        vm.startPrank(alice);
        mockUsdcToken.mint(alice, minAllowance);
        mockUsdcToken.approve(address(tunnlTwitterOffers), minAllowance);
        vm.stopPrank();

        // bob intends to create an offer with id = 101010 (this is normally calculated offchain but here we just use an imaginary id)
        vm.startPrank(bob);
        mockUsdcToken.mint(bob, maxValueUsdc);
        mockUsdcToken.approve(address(tunnlTwitterOffers), maxValueUsdc);
        vm.stopPrank();

        offerId = bytes32(uint(0x101010));

        // alice front runs bob and creates an offer with the same id
        vm.startPrank(alice);
        tunnlTwitterOffers.createOffer(offerId, 0, acceptanceDurationSeconds, 4 days);
        vm.stopPrank();

        // now bob's offer creation reverts
        vm.startPrank(bob);
        vm.expectRevert("Offer already exists");
        tunnlTwitterOffers.createOffer(offerId, offerAmount, acceptanceDurationSeconds, 4 days);
        vm.stopPrank();
    }
LOW severity: Some variable and argument names are misleading. **Description:** The use of misleading variable and argument names may result in incorrect implementation. It is recommended that these names be revised to enhance clarity and maintain code integrity.

- `maxPaymentUsdc` -> `maxOfferedAmountUSDC`

- `Offer.maxValueUsdc` ->...

Vulnerable code:
L152: function createOffer(bytes32 offerId, uint256 maxPaymentUsdc, uint32 acceptanceDurationSeconds, uint32 _offerDurationSeconds)

L72: uint256 maxValueUsdc; // Percentage fee + flat fee + maximum payment the content creator can receive in USDC

L73: uint256 amountPaidUsdc; // The final amount paid to the content creator in USDC
HIGH severity: Attacker can cause a DOS during unstaking by intentionally reverting the transaction when receiving ETH. **Description:** The function `fulfillUnstake()` is used internally to fulfill unstake requests for users. It performs a low-level call to the `userAddress` to transfer ETH and reverses the transaction if the transfer fails. Moreover, the contract processes all unstake requests in a...

Vulnerable code:
function fulfillUnstake(address userAddress, uint256 amount) private {
    (bool success,) = userAddress.call{value: amount}(""); // @audit DOS by reverting on `receive()`
    if (!success) {
        revert TransferFailed();
    }
    emit UnstakeFulfilled(userAddress, amount);
}
HIGH severity: Function `claimEffectiveBalance()` may consistently revert, making it impossible to complete queue withdrawals. **Description:** The function attempts to remove the withdrawal at index `0`, while it uses the withdrawal at index `i` to call `completeQueuedWithdrawal()`. Since each withdrawal can only be completed once, the `delayedEffectiveBalanceQueue[]` list will eventually contain withdrawals that have...

Vulnerable code:
for (uint256 i; i < delayedEffectiveBalanceQueue.length; i++) {
    IDelegationManager.Withdrawal memory withdrawal = delayedEffectiveBalanceQueue[i];
    if (uint32(block.number) - withdrawal.startBlock > withdrawalDelay) {
        delayedEffectiveBalanceQueue.remove(0); // @audit Remove withdrawal at index 0
        claimedEffectiveBalance += withdrawal.shares[0];
        eigenDelegationManager.completeQueuedWithdrawal(withdrawal, tokens, 0, true); // @audit Complete withdrawal of index i
    } else {
        break;
    }
}
HIGH severity: Delayed rewards can be claimed without updating internal accounting. **Description:** The `claimRewards()` function is designed to claim delayed withdrawals from the EigenLayer Delayed Withdrawal Router and to update accounting variables such as `delayedRewards` and `reservedFeeBalance`.

However, this function can be bypassed by directly executing the claim on the.....

Vulnerable code:
function claimRewards() external {
    onlyReporter();

    uint256 initialWithdrawalsBalance = address(eigenWithdrawals).balance;
    eigenWithdrawals.claimDelayedWithdrawals(
        eigenWithdrawals.getClaimableUserDelayedWithdrawals(address(this)).length
    );
    uint256 claimedAmount = initialWithdrawalsBalance - address(eigenWithdrawals).balance;
    delayedRewards -= claimedAmount;

    uint256 rewardsAfterFee = subtractRewardFee(claimedAmount);
    reservedFeeBalance += claimedAmount - rewardsAfterFee;
    distributeStake(rewardsAfterFee);

    emit RewardsClaimed(rewardsAfterFee);
}
HIGH severity: Front-run withdrawValidator by submitting proofs can permanently DOS validator unstaking on EigenLayer. **Description:** An attacker can observe the mempool and front-run the `CasimirManager::withdrawValidator` transaction by submitting the same proofs directly on Eigen Layer.

Since the proof is already verified, the `CasimirManager::withdrawValidator` transaction will revert when it tries to...

Vulnerable code:
function withdrawValidator(
    uint256 stakedValidatorIndex,
    WithdrawalProofs memory proofs,
    ISSVClusters.Cluster memory cluster
) external {
    onlyReporter();

   // ..code...

    uint256 initialDelayedRewardsLength = eigenWithdrawals.userWithdrawalsLength(address(this)); //@note this holds the rewards
    uint64 initialDelayedEffectiveBalanceGwei = eigenPod.withdrawableRestakedExecutionLayerGwei(); //@note this has the current ETH balance

>   eigenPod.verifyAndProcessWithdrawals(
        proofs.oracleTimestamp,
        proofs.stateRootProof,
        proofs.withdrawalProofs,
        proofs.validatorFieldsProofs,
        proofs.validatorFields,
        proofs.withdrawalFields
    ); //@audit reverts if proof is already verified

    {
        uint256 updatedDelayedRewardsLength = eigenWithdrawals.userWithdrawalsLength(address(this));
        if (updatedDelayedRewardsLength > initialDelayedRewardsLength) {
            IDelayedWithdrawalRouter.DelayedWithdrawal memory withdrawal =
                eigenWithdrawals.userDelayedWithdrawalByIndex(address(this), updatedDelayedRewardsLength - 1);
            if (withdrawal.blockCreated == block.number) {
                delayedRewards += withdrawal.amount;
                emit RewardsDelayed(withdrawal.amount);
            }
        }
    }

    uint64 updatedDelayedEffectiveBalanceGwei = eigenPod.withdrawableRestakedExecutionLayerGwei();
>   uint256 finalEffectiveBalance =
        (updatedDelayedEffectiveBalanceGwei - initialDelayedEffectiveBalanceGwei) * GWEI_TO_WEI; //@audit if no proofs submitted, this will be 0
    delayedEffectiveBalance += finalEffectiveBalance;
    reportWithdrawnEffectiveBalance += finalEffectiveBalance;
}

//... code

event FullWithdrawalRedeemed(
    uint40 validatorIndex,
    uint64 withdrawalTimestamp,
    address indexed recipient,
    uint64 withdrawalAmountGwei
);
HIGH severity: Incorrect accounting of `tipBalance` can indefinitely stall report execution. **Description:** The `receive` fallback function in `CasimirManager` increases `tip` balance if the sender is not the `DelayedWithdrawalRouter`. The implicit assumption here is that all withdrawals, full or partial, are routed via the `DelayedWithdrawalRouter`. While this assumption is true incase.....

Vulnerable code:
receive() external payable {
        if (msg.sender != address(eigenWithdrawals)) {
            tipBalance += msg.value; //@audit tip balance increases even incase of full withdrawals

            emit TipsReceived(msg.value);
        }
    }

function claimTips() external {
        onlyReporter();

        uint256 tipsAfterFee = subtractRewardFee(tipBalance);
        reservedFeeBalance += tipBalance - tipsAfterFee;
        tipBalance = 0;
        distributeStake(tipsAfterFee);
        emit TipsClaimed(tipsAfterFee);
    }

function claimEffectiveBalance() external {
        onlyReporter();

        IStrategy[] memory strategies = new IStrategy[](1);
        strategies[0] = IDelegationManagerViews(address(eigenDelegationManager)).beaconChainETHStrategy();
        IERC20[] memory tokens = new IERC20[](1);
        tokens[0] = IERC20(address(0));

        uint256 withdrawalDelay = eigenDelegationManager.strategyWithdrawalDelayBlocks(strategies[0]);
        uint256 claimedEffectiveBalance;
        for (uint256 i; i < delayedEffectiveBalanceQueue.length; i++) {
            IDelegationManager.Withdrawal memory withdrawal = delayedEffectiveBalanceQueue[i];
            if (uint32(block.number) - withdrawal.startBlock > withdrawalDelay) {
                delayedEffectiveBalanceQueue.remove(0);
                claimedEffectiveBalance += withdrawal.shares[0];
                eigenDelegationManager.completeQueuedWithdrawal(withdrawal, tokens, 0, true);
            } else {
                break;
            }
        }

        delayedEffectiveBalance -= claimedEffectiveBalance;
        withdrawnEffectiveBalance += claimedEffectiveBalance; //@audit this accounting entry should be backed by ETH balance at all times
    }
HIGH severity: Function `getTotalStake()` fails to account for pending validators, leading to inaccurate accounting. **Description:** The `getTotalStake()` function is the core accounting function to calculate the total stake of the `CasimirManager`. It's used to compute the change for `rewardStakeRatioSum` within the `finalizeReport()` function.

This function aggregates the stakes from various sources,...

Vulnerable code:
function getTotalStake() public view returns (uint256 totalStake) {
  // @audit Validators in pending state is not accounted for
  totalStake = unassignedBalance + readyValidatorIds.length * VALIDATOR_CAPACITY + latestActiveBalanceAfterFee
      + delayedEffectiveBalance + withdrawnEffectiveBalance + subtractRewardFee(delayedRewards) - unstakeQueueAmount;
}

uint256 totalStake = getTotalStake();

rewardStakeRatioSum += Math.mulDiv(rewardStakeRatioSum, gainAfterFee, totalStake);

rewardStakeRatioSum += Math.mulDiv(rewardStakeRatioSum, gain, totalStake);

rewardStakeRatioSum -= Math.mulDiv(rewardStakeRatioSum, loss, totalStake);