Platform: Code4rena
Start Date: 21/07/2023
Pot Size: $90,500 USDC
Total HM: 8
Participants: 60
Period: 7 days
Judge: 0xean
Total Solo HM: 2
Id: 264
League: ETH
Rank: 55/60
Findings: 1
Award: $21.60
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: LaScaloneta
Also found by: 0xComfyCat, 0xDING99YA, 0xnev, ABA, BugBusters, DadeKuma, MohammedRizwan, QiuhaoLi, Sathish9098, Udsen, ak1, bart1e, immeas, koxuan, ladboy233, matrix_0wl, oakcobalt, squeaky_cactus, zhaojie
21.5977 USDC - $21.60
User's votes will be nullified after updating the new NFT. This will be possible if that NFT is yet to get assigned with multiplier value.
Though the NFT can be updated with new multiplier value, but, at times it would be serious risk to the governance based mechanism. In certain situations, there might be malicious proposal gonna be happen, but it should be stopped by counter voting. Since some of users voting power is nullified temporarily, they can not oppose the malicious proposal.
NFTBoostVault
has multiplier based vote allocation. By default the multiplier value is 1e3. This value would be more for NFT deposit.
Incase of freshly adding the NFT , the function addNftAndDelegate is called and NFT is added with registration by calling the function..
When we look at the function
function _registerAndDelegate( address user, uint128 _amount, uint128 _tokenId, address _tokenAddress, address _delegatee ) internal { uint128 multiplier = 1e3; // confirm that the user is a holder of the tokenId and that a multiplier is set for this token if (_tokenAddress != address(0) && _tokenId != 0) { if (IERC1155(_tokenAddress).balanceOf(user, _tokenId) == 0) revert NBV_DoesNotOwn(); multiplier = getMultiplier(_tokenAddress, _tokenId); if (multiplier == 0) revert NBV_NoMultiplierSet(); ----->> @@audit : revert happen for zero multiplier.
The above revert is done to ensure that user will get base voting power with respect to their deposited token amounts.
Now, lets look at the update NFT case.
Contract has the function updateNft function to update new NFT. the voting power is updated based on the NFT multiplier value.
Lets look at the code flow,
function updateNft(uint128 newTokenId, address newTokenAddress) external override nonReentrant { if (newTokenAddress == address(0) || newTokenId == 0) revert NBV_InvalidNft(newTokenAddress, newTokenId); if (IERC1155(newTokenAddress).balanceOf(msg.sender, newTokenId) == 0) revert NBV_DoesNotOwn(); NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[msg.sender]; // If the registration does not have a delegatee, revert because the Registration // is not initialized if (registration.delegatee == address(0)) revert NBV_NoRegistration(); // if the user already has an ERC1155 registered, withdraw it if (registration.tokenAddress != address(0) && registration.tokenId != 0) { // withdraw the current ERC1155 from the registration _withdrawNft(); } // set the new ERC1155 values in the registration and lock the new ERC1155 registration.tokenAddress = newTokenAddress; registration.tokenId = newTokenId; _lockNft(msg.sender, newTokenAddress, newTokenId, 1); // update the delegatee's voting power based on new ERC1155 nft's multiplier _syncVotingPower(msg.sender, registration); --------------------------------->> after adding new NFT, vote update will be done here. }
at the end of the updateNft
function, vote will be updated by calling the function _syncVotingPower. The _syncVotingPower
function calls the _currentVotingPower
function to call the current voting power in order to update the voting power of user and their delegatee.
function _syncVotingPower(address who, NFTBoostVaultStorage.Registration storage registration) internal { History.HistoricalBalances memory votingPower = _votingPower(); uint256 delegateeVotes = votingPower.loadTop(registration.delegatee); uint256 newVotingPower = _currentVotingPower(registration); // get the change in voting power. Negative if the voting power is reduced
now lets check the _currentVotingPower
function, it uses the getMultiplier
function to get the multiplier value for the NFT. Here the issue comes, if the new NFT is not assigned with any multiplier value, this funtion would return the current voting power has zero.
function _currentVotingPower( NFTBoostVaultStorage.Registration memory registration ) internal view virtual returns (uint256) { uint128 locked = registration.amount - registration.withdrawn; if (registration.tokenAddress != address(0) && registration.tokenId != 0) { return (locked * getMultiplier(registration.tokenAddress, registration.tokenId)) / MULTIPLIER_DENOMINATOR; } return locked; }
This way the updateNft
would nullify the voting power of an user by calling the _syncVotingPower
Manual review.
Update the getMultiplier
functioin as shown below.
function getMultiplier(address tokenAddress, uint128 tokenId) public view override returns (uint128) { NFTBoostVaultStorage.AddressUintUint storage multiplierData = _getMultipliers()[tokenAddress][tokenId]; // if a user does not specify a ERC1155 nft, their multiplier is set to 1 if (tokenAddress == address(0) || tokenId == 0) { return 1e3; } if(multiplierData.multiplier == 0) --------->> updated return 1e3; return multiplierData.multiplier; }
Governance
#0 - c4-pre-sort
2023-07-29T10:46:44Z
141345 marked the issue as duplicate of #214
#1 - c4-judge
2023-08-10T21:32:51Z
0xean changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-08-10T23:26:06Z
0xean marked the issue as grade-b