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: 8/60
Findings: 2
Award: $2,110.85
🌟 Selected for report: 0
🚀 Solo Findings: 0
312.7392 USDC - $312.74
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L182-L212
The value of voting power for a delegate can be incorrect because it uses the old latestVotingPower value.
Refer to the following code
function setMultiplier(address tokenAddress, uint128 tokenId, uint128 multiplierValue) public override onlyManager { if (multiplierValue > MAX_MULTIPLIER) revert NBV_MultiplierLimit(); NFTBoostVaultStorage.AddressUintUint storage multiplierData = _getMultipliers()[tokenAddress][tokenId]; // set multiplier value multiplierData.multiplier = multiplierValue; emit MultiplierSet(tokenAddress, tokenId, multiplierValue); }
function delegate(address to) external override { if (to == address(0)) revert NBV_ZeroAddress("to"); NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[msg.sender]; // If to address is already the delegate, don't send the tx if (to == registration.delegatee) revert NBV_AlreadyDelegated(); History.HistoricalBalances memory votingPower = _votingPower(); uint256 oldDelegateeVotes = votingPower.loadTop(registration.delegatee); // Remove voting power from old delegatee and emit event votingPower.push(registration.delegatee, oldDelegateeVotes - registration.latestVotingPower); emit VoteChange(msg.sender, registration.delegatee, -1 * int256(uint256(registration.latestVotingPower))); // Note - It is important that this is loaded here and not before the previous state change because if // to == registration.delegatee and re-delegation was allowed we could be working with out of date state uint256 newDelegateeVotes = votingPower.loadTop(to); // return the current voting power of the Registration. Varies based on the multiplier associated with the // user's ERC1155 token at the time of txn uint256 addedVotingPower = _currentVotingPower(registration); // add voting power to the target delegatee and emit event votingPower.push(to, newDelegateeVotes + addedVotingPower); // update registration properties registration.latestVotingPower = uint128(addedVotingPower); registration.delegatee = to; emit VoteChange(msg.sender, to, int256(addedVotingPower)); }
Manager may update multiplier by calling NFTBoostVault#setMultiplier from time to time and it does not update all the latestVotingPower that belongs to every registration. When user calls NFTBoostVault#delegate to remove an existing delegate, it still uses the old latestVotingPower that use the old multiplier.
See the following code snippet from NFTBoostVault#delegate
History.HistoricalBalances memory votingPower = _votingPower(); uint256 oldDelegateeVotes = votingPower.loadTop(registration.delegatee); // Remove voting power from old delegatee and emit event votingPower.push(registration.delegatee, oldDelegateeVotes - registration.latestVotingPower);
Manual and wrote a simple hardhat test
We need to use the new latestVotingPower
Change NFTBoostVault#delegate
function delegate(address to) external override { if (to == address(0)) revert NBV_ZeroAddress("to"); NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[msg.sender]; // If to address is already the delegate, don't send the tx if (to == registration.delegatee) revert NBV_AlreadyDelegated(); History.HistoricalBalances memory votingPower = _votingPower(); uint256 oldDelegateeVotes = votingPower.loadTop(registration.delegatee); +++ _syncVotingPower(msg.sender, registration); // @audit // Remove voting power from old delegatee and emit event votingPower.push(registration.delegatee, oldDelegateeVotes - registration.latestVotingPower); emit VoteChange(msg.sender, registration.delegatee, -1 * int256(uint256(registration.latestVotingPower))); // Note - It is important that this is loaded here and not before the previous state change because if // to == registration.delegatee and re-delegation was allowed we could be working with out of date state uint256 newDelegateeVotes = votingPower.loadTop(to); // return the current voting power of the Registration. Varies based on the multiplier associated with the // user's ERC1155 token at the time of txn uint256 addedVotingPower = _currentVotingPower(registration); // add voting power to the target delegatee and emit event votingPower.push(to, newDelegateeVotes + addedVotingPower); // update registration properties registration.latestVotingPower = uint128(addedVotingPower); registration.delegatee = to; emit VoteChange(msg.sender, to, int256(addedVotingPower)); }
Math
#0 - c4-pre-sort
2023-07-30T13:39:50Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2023-08-01T09:15:37Z
141345 marked the issue as duplicate of #431
#2 - c4-judge
2023-08-11T16:05:47Z
0xean marked the issue as satisfactory
🌟 Selected for report: bin2chen
Also found by: 0xWaitress, caventa, ladboy233
1798.1148 USDC - $1,798.11
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L189-L201 https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L384-L394
Calling ArcadeTreasury#gscApprove more than 1 times should not reset approval
Refer to the following code
function gscApprove( address token, address spender, uint256 amount ) external onlyRole(GSC_CORE_VOTING_ROLE) nonReentrant { if (spender == address(0)) revert T_ZeroAddress("spender"); if (amount == 0) revert T_ZeroAmount(); // Will underflow if amount is greater than remaining allowance gscAllowance[token] -= amount; // @audit1 see the deduction here _approve(token, spender, amount, spendThresholds[token].small); }
function _approve(address token, address spender, uint256 amount, uint256 limit) internal { // check that after processing this we will not have spent more than the block limit uint256 spentThisBlock = blockExpenditure[block.number]; if (amount + spentThisBlock > limit) revert T_BlockSpendLimit(); blockExpenditure[block.number] = amount + spentThisBlock; // approve tokens IERC20(token).approve(spender, amount); // @audit2 always use the latest amount emit TreasuryApproval(token, spender, amount); }
Admins with GSC_CORE_VOTING_ROLE role can call ArcadeTreasury#gscApprove for many times and gscAllowance should approve the ACCUMULATED ammount like how gscAllowance[token] is calculated
(See @audit1)
gscAllowance[token] -= amount;
However, right now, system always overwrites the approval amount with the final amount and not with the ACCUMULATED amount.
(See @audit2)
IERC20(token).approve(spender, amount);
Manual and wrote a simple hardhat test
Create a new function named _approveAccumulate which is modified from the existing _approve function. Approve the new amount together with the old allowance value. See @audit.
function _approveAccumulate(address token, address spender, uint256 amount, uint256 limit) internal { // check that after processing this we will not have spent more than the block limit uint256 spentThisBlock = blockExpenditure[block.number]; if (amount + spentThisBlock > limit) revert T_BlockSpendLimit(); blockExpenditure[block.number] = amount + spentThisBlock; uint256 allowance = IERC20(token).allowance(address(this), spender); // @audit // approve tokens IERC20(token).approve(spender, amount + allowance); // @audit emit TreasuryApproval(token, spender, amount); }
Change ArcadeTreasury#gscApprove to use the new _approveAccumulate function
function gscApprove( address token, address spender, uint256 amount ) external onlyRole(GSC_CORE_VOTING_ROLE) nonReentrant { if (spender == address(0)) revert T_ZeroAddress("spender"); if (amount == 0) revert T_ZeroAmount(); // Will underflow if amount is greater than remaining allowance gscAllowance[token] -= amount; --- _approve(token, spender, amount, spendThresholds[token].small); +++ _approveAccumulate(token, spender, amount, spendThresholds[token].small); }
Other
#0 - c4-pre-sort
2023-07-29T13:57:59Z
141345 marked the issue as duplicate of #85
#1 - c4-judge
2023-08-11T16:30:55Z
0xean marked the issue as satisfactory