Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dettachFromManagedNFT might revert and temporarily prevent users from detaching in certain situation #5

Open
c4-bot-1 opened this issue Sep 24, 2024 · 3 comments
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working edited-by-warden M-05 🤖_primary AI based primary recommendation satisfactory satisfies C4 submission criteria; eligible for awards selected for report This submission will be included/highlighted in the audit report sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")

Comments

@c4-bot-1
Copy link
Contributor

c4-bot-1 commented Sep 24, 2024

Lines of code

https://github.com/code-423n4/2024-09-fenix-finance/blob/main/contracts/core/VoterUpgradeableV2.sol#L564

Vulnerability details

Impact

Users' veNFT might be temporarily undetachable, preventing users from performing action on their own veNFT.

Proof-of-Concept

When users invoke dettachFromManagedNFT to get their veNFT back from ManagedNFT, _poke is called at the end of the function to update voting power across gauges voted by this ManagedNFT.

function dettachFromManagedNFT(uint256 tokenId_) external nonReentrant onlyNftApprovedOrOwner(tokenId_) {
    _checkVoteDelay(tokenId_);
    _checkVoteWindow();
    IManagedNFTManager managedNFTManagerCache = IManagedNFTManager(managedNFTManager);
    uint256 managedTokenId = managedNFTManagerCache.getAttachedManagedTokenId(tokenId_);
    managedNFTManagerCache.onDettachFromManagedNFT(tokenId_);
    uint256 weight = IVotingEscrowV2(votingEscrow).balanceOfNftIgnoreOwnershipChange(managedTokenId);
    if (weight == 0) {
        _reset(managedTokenId);
        delete lastVotedTimestamps[managedTokenId];
    } else {
        _poke(managedTokenId);
    }
    emit DettachFromManagedNFT(tokenId_);
}

function _poke(uint256 tokenId_) internal {
    address[] memory _poolVote = poolVote[tokenId_];
    uint256[] memory _weights = new uint256[](_poolVote.length);

    for (uint256 i; i < _poolVote.length; ) {
        _weights[i] = votes[tokenId_][_poolVote[i]];
        unchecked {
            i++;
        }
    }
    _vote(tokenId_, _poolVote, _weights);
    _updateLastVotedTimestamp(tokenId_);
}

function _vote(uint256 tokenId_, address[] memory pools_, uint256[] memory weights_) internal {
    _reset(tokenId_);
    uint256 nftVotePower = IVotingEscrowV2(votingEscrow).balanceOfNFT(tokenId_);
    uint256 totalVotesWeight;
    uint256 totalVoterPower;
    for (uint256 i; i < pools_.length; i++) {
        GaugeState memory state = gaugesState[poolToGauge[pools_[i]]];
        if (!state.isAlive) {
            revert GaugeAlreadyKilled();
        }
        totalVotesWeight += weights_[i];
    }
    ...
    ... snipped ...
    ...
}

_poke loads a list of pools and weights voted by ManagedNFT then recast votes again to the same set of pools and weights via calling into _vote.

However, _vote reverts when one of the pool/gauge has already been killed.

Now consider this situation:

  1. Bob attaches his veNFT with ManagedNFT.
  2. ManagedNFT votes for [gaugeA, gaugeB].
  3. gaugeB is killed.
  4. Bob decides to detach his veNFT from ManagedNFT.
  5. Bob's transaction reverts because _poke will attempt to recast the vote on gaugeB.
  6. Bob can't detach his veNFT until ManagedNFT notices and recast the vote excluding gaugeB.

As a result, users' veNFT might be temporarily undetachable when the described scenario happens.

Recommended Mitigations

Users are expected to only include active pools in normal vote flow.
If one of the pool is inactive, we can safely set its weight to zero and skip over it (gracefully ignore it).

function _vote(uint256 tokenId_, address[] memory pools_, uint256[] memory weights_) internal {
    _reset(tokenId_);
    uint256 nftVotePower = IVotingEscrowV2(votingEscrow).balanceOfNFT(tokenId_);
    uint256 totalVotesWeight;
    uint256 totalVoterPower;
    for (uint256 i; i < pools_.length; i++) {
        GaugeState memory state = gaugesState[poolToGauge[pools_[i]]];
        if (!state.isAlive) {
            delete weights_[i];
            delete pools_[i];
            continue;
        }
        totalVotesWeight += weights_[i];
    }

    uint256 time = _epochTimestamp();
    for (uint256 i; i < pools_.length; i++) {
        address pool = pools_[i];
        if(pool == address(0)) continue;
        address gauge = poolToGauge[pools_[i]];

        uint256 votePowerForPool = (weights_[i] * nftVotePower) / totalVotesWeight;
        if (votePowerForPool == 0) {
            revert ZeroPowerForPool();
        }
        if (votes[tokenId_][pool] > 0) {
            revert NoResetBefore();
        }

        poolVote[tokenId_].push(pool);
        votes[tokenId_][pool] = votePowerForPool;
        weightsPerEpoch[time][pool] += votePowerForPool;
        totalVoterPower += votePowerForPool;
        IBribe(gaugesState[gauge].internalBribe).deposit(votePowerForPool, tokenId_);
        IBribe(gaugesState[gauge].externalBribe).deposit(votePowerForPool, tokenId_);
        emit Voted(_msgSender(), tokenId_, votePowerForPool);
    }
    if (totalVoterPower > 0) IVotingEscrowV2(votingEscrow).votingHook(tokenId_, true);
    totalWeightsPerEpoch[time] += totalVoterPower;
}

Assessed type

DoS

@c4-bot-1 c4-bot-1 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Sep 24, 2024
c4-bot-3 added a commit that referenced this issue Sep 24, 2024
@c4-bot-12 c4-bot-12 added the 🤖_primary AI based primary recommendation label Sep 25, 2024
@b-hrytsak
Copy link

Thank you for your submission.

Although there is a certain safe way to kill a gauge, etc., the described case is possible if the gauge is killed in the middle of an epoch for some reason, and as a result, the veNFT cannot be unhooked from the strategy for some time.

I am not sure that the recommended mitigation is optimal. Redistribution of votes between live pools decision is also not ideal

Thank you for participation

@b-hrytsak b-hrytsak added the sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") label Sep 30, 2024
@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Sep 30, 2024
@c4-judge
Copy link

alcueca marked the issue as satisfactory

@c4-judge
Copy link

alcueca marked the issue as selected for report

@c4-judge c4-judge added the selected for report This submission will be included/highlighted in the audit report label Sep 30, 2024
@C4-Staff C4-Staff added M-04 M-05 and removed M-04 labels Oct 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working edited-by-warden M-05 🤖_primary AI based primary recommendation satisfactory satisfies C4 submission criteria; eligible for awards selected for report This submission will be included/highlighted in the audit report sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")
Projects
None yet
Development

No branches or pull requests

6 participants