Open hats-bug-reporter[bot] opened 4 months ago
Thank you for the submission.
After the tests described in the view, there is indeed a certain logic for displaying tokens in both user1 and user2 if the transfer took place within the same block
But does it affect the votes?
We can see that the vote power is counted correctly.
and do not take into account these issues of duplicates, etc.
The principle of operation looks like with vote counting, when you take votes for a certain period, yes, there are indeed votes, but if there was a change of token owner or something else in a certain block, when you take votes using methods that are designed to take actual votes, it returns the correct value
I would like to request additional clarification from the submitter, namely, a specific case that will lead to a critical case within the protocol.
At the moment, this logic was inherited from Thena, as it is Medium, so OOS
Regarding "transfer but will return the last/previous checkpoint in the second transfer. This will cause the move token delegate" - It will not return the previous checkpoint, since the timestamp is never initialized (other bug)
Thank you for reviewing my submission!
Due to the bug of "timestamp variable is never initialized", the _findWhatCheckpointToWrite
function will always create and return a new checkpoint, which make this issue not happening as described. Please view this issue supposing the timestamp variable is correctly intitialized. In the mean time, kindly refer to the mitigation which velodrome use to fix this issue. And to mention about a critical case within the protocol, as the title says, it definitely about manipulating voting power within the protocol.
In the severity of this submission, I am more inclined to medium. Taking into account the way VotingEscrow is used and what methods are taken into account to obtain votes, I came to the conclusion (tests) that although this issue is valid, it does not pose risks to the methods used to obtain the value of the strength of the vote (balanceOfNFT) in the Voter.
It does not jeopardize the funds or manipulation of votes that will allow them to be used in the state in which the protocol is now.
Since the actual calculations and state variables of some methods that are present are broken, which makes their work incorrect, these methods are not called anywhere, which does not pose a threat to funds in the state of the protocol as it is (Medium).
Since there is a lack of criticality (direct impact on funds), OOS remains due to inheritance from Thena/Chronos from the beginning
Would like to see how this problem is used to extract/receive additional funds in the system in specific case
Thank you for replying!
After looking carefully at the code, it seems like this inflating issue may only affect the DAO Governance voting. As I observed from Fenix Finance docs, You guys state:
And from Thena:
Since the code will be inherited from Thena, there is possibly a chance that you will set up a governance for the protocol. If that is the case, then you may have to re-consider this issue if you don't want the governance to be manipulated by some malicious users with inflated voting power from their $veFNX.
When implementing the DAO, these functions such as VotingEscrowUpgradeable.getVotes() and VotingEscrowUpgradeable.getPastVotes() will be used to calculated the votes count of a proposal. This is where the issue happens.
With the ability to inflate voting balance, I want to change the severity of this issue to Medium instead of ~High~:
Impact: High, as it breaks the current votes of a particular account
Likelihood: Low, as the protocol may or may not implement a DAO governance within the protocol although malicious users can cast extra votes at any time
It remains OOS due to the lack of sufficient impact.
As we can see, at the moment there are only theoretical problems / in the future or in connection with the integration of these methods.
And the impact is the incorrect return voting power/list of the above methods, but since it is not used anywhere in contracts at the moment, it does not cause an impact on real funds and sufficient criticality, so OOS
Github username: -- Twitter username: -- Submission hash (on-chain): 0x38af4af798c80712ca428f4d6bd7ad29d16c29c34a102ceb271080a582951667 Severity: high
Description: Description
Attack Scenario\ A checkpoint can contain duplicated veNFTs (tokenIDs) under certain circumstances leading to double counting of voting balance. Malicious users could exploit this vulnerability to inflate the voting balance of their accounts and participate in governance and gauge weight voting, potentially causing loss of assets or rewards for other users if the inflated voting balance is used in a malicious manner (e.g. redirect rewards to gauges where attackers have a vested interest). Following is the high-level pseudo-code of the existing _moveTokenDelegates function, which is crucial for understanding the issue.
However, the problem is that the _findWhatCheckpointToWrite function does not always return the index of a new checkpoint. It will return the last checkpoint if it has already been written once within the same block.
If someone triggers the _moveTokenDelegates more than once within the same block (e.g. perform NFT transfer twice to the same person), the _findWhatCheckpointToWrite function will return a new checkpoint in the first transfer but will return the last/previous checkpoint in the second transfer. This will cause the move token delegate logic to be off during the second transfer.
First Transfer at Block 1000
Assume the following states:
To move tokenID=2 from Alice to Bob, the _moveTokenDelegates(Alice, Bob, n2) function will be triggered. The _findWhatCheckpointToWrite will return the index of 1 which points to a new array. The end states of the first transfer will be as follows:
Everything is working fine at this point in time.
Second Transfer at Block 1000 (same block)
To move tokenID=1 from Alice to Bob, the _moveTokenDelegates(Alice, Bob, n1) function will be triggered. This time round since the last checkpoint block is the same as the current block, the _findWhatCheckpointToWrite function will return the last checkpoint instead of a new checkpoint. The srcRepNew and dstRepNew will end up referencing the old checkpoint instead of a new checkpoint. As such, the srcRepNew and dstRepNew array will reference back to the old checkpoint _checkpoints[Alice][1].tokenIds and _checkpoints[Bob][1].tokenIds respectively. The end state of the second transfer will be as follows:
Four (4) problems could be observed from the end state:
Since the token IDs within the checkpoint will be used to determine the voting power, the voting power will be inflated in this case as there will be a double count of certain NFTs
Additional Comment about nextSrcRepNum variable and _findWhatCheckpointToWrite function
the code wrongly assumes that the _findWhatCheckpointToWrite function will always return the index of the next new checkpoint. The _findWhatCheckpointToWrite function will return the index of the latest checkpoint instead of a new one if block.number == checkpoint.fromBlock.
Additional Comment about numCheckpoints
the function computes the new number of checkpoints by incrementing the srcRepNum by one. However, this is incorrect because if block.number == checkpoint.fromBlock, then the number of checkpoints remains the same and does not increment.
Attachments
Proof of Concept (PoC) File
Recommendation
Update the move token delegate logic within the affected functions (VotingEscrow._moveTokenDelegates and VotingEscrow._moveAllDelegates) to ensure that the latest checkpoint is overwritten correctly when the functions are triggered more than once within a single block.
Further, ensure that the following invariants hold in the new code:
• No duplicated veNFTs (tokenIDs) within a checkpoint
• When moving a tokenID, it must be deleted from the source tokenIds list and added to the destination tokenIds list
• No more than one checkpoint within the same block for an account. Otherwise, the binary search within the VotingEscrow.getPastVotesIndex will return an incorrect number of votes