Platform: Code4rena
Start Date: 22/05/2024
Pot Size: $20,000 USDC
Total HM: 6
Participants: 126
Period: 5 days
Judge: 0xsomeone
Total Solo HM: 1
Id: 379
League: ETH
Rank: 2/126
Findings: 4
Award: $1,695.86
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Circolors
Also found by: 0rpse, 0x175, 0xAadi, 0xHash, 0xMax1mus, 0xMosh, 0xblack_bird, 0xdice91, 0xfox, 0xhacksmithh, 0xloscar01, 0xrex, 4rdiii, Audinarey, AvantGard, Bigsam, DPS, Dots, Drynooo, Dudex_2004, Evo, Kaysoft, King_, Limbooo, MrPotatoMagic, PENGUN, Sabit, SovaSlava, SpicyMeatball, TheFabled, Utsav, Varun_05, Walter, adam-idarrha, araj, aslanbek, ayden, bctester, biakia, bigtone, brgltd, carrotsmuggler, cats, crypticdefense, dd0x7e8, dhank, fandonov, fyamf, grearlake, iamandreiski, ilchovski, jasonxiale, joaovwfreire, lanrebayode77, m4ttm, merlinboii, niser93, nnez, octeezy, oxchsyston, pamprikrumplikas, rouhsamad, tedox, trachev, turvy_fuzz, twcctop, yotov721, zhaojohnson
0.0056 USDC - $0.01
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L275
The lock private function increases the amount of certain token locked by an address at the LockManager contract by transferring a corresponding amount of tokens into it. As a side-effect, it increases the unlockTime by the current lock's lock duration. This opens up the possibility of attackers to lock someone's tokens forever at the contract.
The reason for that is the lock private function has two entry points: via the lock and via the lockOnBehalf functions. The latter allows anyone to target any address as the tokenRecipient - as it offers no access control from callers to lock recipients. If any address can be targeted, anyone can increase a lock's unlock time, as can be seen in the following code snippet:
function _lock( address _tokenContract, uint256 _quantity, address _tokenOwner, address _lockRecipient ) private { ... lockedToken.unlockTime = uint32(block.timestamp) + uint32(_lockDuration); ... }
Malicious actors can freely increase the unlock time of users' locks. If the gas cost is low at any point, then maintaining one's lock forever is relatively cheap.
Manual review
Make sure to create some sort of access control for locking on behalf of someone, as it has the side-effect of increasing the unlock time. An interesting possibility would be to create a mapping of allowed addresses to lock on behalf of an address then check if the lock on behalf caller is allowed to do such an action to its lock recipient. This access control logic will mitigate the aforementioned attack vector.
DoS
#0 - c4-judge
2024-06-05T12:58:13Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: SpicyMeatball
Also found by: 0rpse, 0xMosh, 0xblack_bird, 0xdice91, 0xhacksmithh, 0xleadwizard, 0xmystery, Audinarey, AvantGard, Bigsam, Dots, EPSec, Eeyore, Janio, Limbooo, LinKenji, Mahmud, MrPotatoMagic, Myd, Oxsadeeq, Sabit, SovaSlava, Stefanov, Tychai0s, Utsav, Varun_05, Walter, adam-idarrha, ahmedaghadi, araj, aslanbek, ayden, bigtone, c0pp3rscr3w3r, carrotsmuggler, crypticdefense, dhank, fyamf, gajiknownnothing, gavfu, itsabinashb, jasonxiale, joaovwfreire, ke1caM, leegh, merlinboii, mitko1111, n4nika, pfapostol, prapandey031, rouhsamad, sandy, snakeeaterr, stakog, steadyman, swizz, tedox, th3l1ghtd3m0n, trachev, turvy_fuzz, xyz, yashgoel72, zhaojohnson
0.014 USDC - $0.01
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L260 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L266
The setLockDuration function does a couple of checks to ensure the new lock duration does not decrease the previously existing one. However, its current logic allows this requirement to be bypassed. This happens due to the following reasons: The function does not check if the uint256 duration argument is smaller than the minimum duration at the contract.
The LockDurationReducedError only checks for the current timestamp + duration in comparision to the last unlockTime:
if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); }
The new unlockTime is not based on the previously used, but on the lastLockTime - that is, the time the previous lock was created.
uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] .lastLockTime; lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration);
This enables malicious parties to reduce the lock duration by doing the following steps:
Locks are able to be manipulated to last half of its original duration.
Manual review
Make sure to review the LockDurationReducedError checks. If the intention is to never allow lock duration reductions, the check should evaluate duration + last lock time instead of block.timestamp + duration, as can be seen in the code snippet bellow:
if ( lockedTokens[msg.sender][tokenContract] .lastLockTime + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); }
This will ensure the new unlock time fits the bounds defined by the initial lock timestamp.
Timing
#0 - c4-judge
2024-06-04T12:40:58Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:53:37Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: robertodf99
Also found by: 0xAadi, 0xAkira, 0xdice91, 0xhacksmithh, 0xleadwizard, AgileJune, Bauchibred, Bbash, Beosin, Bigsam, Dots, EPSec, EaglesSecurity, Eeyore, Evo, John_Femi, Mahmud, MrPotatoMagic, RotiTelur, Rushkov_Boyan, Sabit, Sentryx, Stormreckson, Topmark, Tychai0s, Utsav, Walter, ZanyBonzy, ZdravkoHr, adam-idarrha, araj, aslanbek, avoloder, bigtone, brevis, brgltd, carrotsmuggler, crypticdefense, dd0x7e8, dhank, djanerch, falconhoof, iamandreiski, joaovwfreire, leegh, merlinboii, mitko1111, pamprikrumplikas, pfapostol, prapandey031, swizz, trachev, twcctop, typicalHuman, unique, xyz
0.0148 USDC - $0.01
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L194 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L227
When dealing with usd price proposals, price feeds ought to have two options: vote to approve or to disapprove a proposed price. This is not enforced on its entirety at the LockManager contract, however. The disapproveUSDPrice function properly enforces that by checking whether the price feed hasn't approved nor disapproved a proposed price before accounting the disapprove vote:
function disapproveUSDPrice( uint256 _price ) ... if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError(); ...
The approveUSDPrice doesn't enforce the same checks. It only verifies the price feed hasn't approved the price priorly:
function approveUSDPrice( uint256 _price ) ... if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); ...
The current price approval mechanism is not fair on all price fees. This issue allows privilege escalation as a price feed is able to vote twice.
Allowing one voter to vote on both approval and disapproval exceeds the intended permissions by creating the possibility of voting ties: the system has 5 price feeds so that ties are impossible, but this privilege escalation allows both for and against positions to have 3 or more votes. This enables an unexpected logic for price approvals - price feeds will have incentives to vote as soon as possible.
Manual review
Add the following check to approveUSDPrice:
if (usdUpdateProposal.disapprovals[msg.sender] == _usdProposalId) revert ProposalAlreadyDisapprovedError();
Other
#0 - c4-judge
2024-06-05T12:42:36Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: PetarTolev
Also found by: Drynooo, MrPotatoMagic, bearonbike, joaovwfreire
900.8292 USDC - $900.83
https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L379 https://github.com/code-423n4/2024-05-munchables/blob/main/src/managers/LockManager.sol#L361
The lock private function can be executed from the lock and from the lockOnBehalf public functions. When called by the migrationManager role, the lock private function does a wrong state management on a locked token's remainder. Let's analyze it in-depth: The remainder uint256 variable is initialized as equal to zero:
uint256 remainder;
Then the function proceeds to update this variable only if the msg.sender is not the contract's migrationManager:
if (msg.sender != address(migrationManager)) { // calculate number of nfts remainder = quantity % configuredToken.nftCost; ...
Finally it takes the current remainder value and updates the remainder at the lockedToken storage mapping:
lockedToken.remainder = remainder;
The issue with that is that if we don't ever reach the if block limited by the migrationManager caller, we put a zero-valued remainder at the storage in lockedToken.remainder. This removes any remainder the user had left.
Users that have their locks updated by the migrationManager lose their locked token's remainder.
Manual review
Make sure to avoid updating the remainder storage variable inside a lockedToken if the currently calculated value is not different than the previously held. An interesting way would to keep the remainder value would be through an else block:
if (msg.sender != address(migrationManager)) { ... } else { remainder = lockedToken.remainder; }
This should suffice to ensure the remainder is not lost in this case.
Context
#0 - c4-judge
2024-06-04T10:54:30Z
alex-ppg marked the issue as not a duplicate
#1 - c4-judge
2024-06-04T10:54:40Z
alex-ppg marked the issue as duplicate of #455
#2 - c4-judge
2024-06-05T12:47:36Z
alex-ppg marked the issue as satisfactory