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: 9/126
Findings: 4
Award: $136.42
🌟 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.0042 USDC - $0.00
The lockOnBehalf function allows an attacker to repeatedly force a harvest operation on a victim user by passing zero ETH and zero quantity to a lockOnBehalf call where the OnBehalfOf is the victim. This results in a Denial-of-Service (DoS) attack that prevents the victim from accumulating Schnibbles rewards over time. in fact forceHarvest must be called only from wallets authorized by the user who is involved in the harvest, not just anyone can do it, otherwise this problem arises also because inside (_harvest) there is a division for /1 days which ultimately helps this problem making the rounding down.
_tokenContract: address(0) _quantity: 0 _onBehalfOf: Address of the victim user msg.value: 0
hypothetical transaction (could be tested in the current suit):
it("should lock token for other player", async () => { const { request } = await testContracts.lockManager.contract.simulate.lockOnBehalf( [zeroAddress, parseEther("0"), alice], { account: bob, value: parseEther("0") } ); const txHash = await testClient.writeContract(request); await assertLockSuccess({ testContracts, testERC20Contract, numberNFTs: 0n, player: alice, quantity: parseEther("0"), lockedQuantity: parseEther("0"), remainder: 0n, sender: bob, tokenContractAddress: zeroAddress, txHash, }); });
repeat this process every few hours,depending on how much the victim has in the protocol(getDailySchnibbles)
manual review
add an allowance mapping that tracks every delegation,like so:
mapping(address account => mapping(address spender => bool)) private _allowances;
and add the checks as a modifier like so:
modifier onlyAuthorized(address onBehalfOf){ if (!_allowances[onBehalfOf][msg.sender])revert NotAuthorized(); _; }
that allows only authorized parties to lock tokens on behalf of other users.
DoS
#0 - c4-judge
2024-06-05T12:57:39Z
alex-ppg marked the issue as partial-75
#1 - c4-judge
2024-06-05T13:00:03Z
alex-ppg changed the severity to 3 (High Risk)
🌟 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
Users can exploit this vulnerability to reduce the lock duration of their tokens, allowing them to unlock tokens earlier than intended. This undermines the lock mechanism and allows users to receive rewards without adhering to the proper lock time, potentially leading to economic imbalances and unfair advantages.
Assume the following variables:
currentTimestamp = 100 (attackCall) _duration = 30 lastLockTime = 80 (some point in the past where user locked tokens) pastDuration = 50 (original duration of the locked token at 80 of timestamp,lastLockTime) currentUnlockTime = lastLockTime + pastDuration = 80 + 50 = 130
If the current unlockTime is, for example, 130, this check will pass:
if (uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime) { revert LockDurationReducedError(); }
in fact this if statement will be: 130(100+30) < 130 => false = skip
and lastly will be reduced the unlockTime at this line:
lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration); // 80 +30 = 110 @audit reduced!
reducing in this way the unlockTime!
will leave the function commented to get a better idea:
function setLockDuration(uint256 _duration) external notPaused { if (_duration > configStorage.getUint(StorageKey.MaxLockDuration)) revert MaximumLockDurationError(); playerSettings[msg.sender].lockDuration = uint32(_duration); // update any existing lock uint256 configuredTokensLength = configuredTokenContracts.length; for (uint256 i; i < configuredTokensLength; i++) { address tokenContract = configuredTokenContracts[i]; if (lockedTokens[msg.sender][tokenContract].quantity > 0) { // check they are not setting lock time before current unlocktime if ( uint32(block.timestamp) + uint32(_duration) < // 100 + 30 @audit - MEDIUM needs to check lastLockTime + duration lower than current unlockTime lockedTokens[msg.sender][tokenContract].unlockTime // 130 ) { revert LockDurationReducedError(); } uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] .lastLockTime; lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration); // 80 +30 = 110 @audit reduced! } // users can reduce duration of timelock } emit LockDuration(msg.sender, _duration); }
manual review
2 options:
lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration);
to this:
lockedTokens[msg.sender][tokenContract].unlockTime = uint32(block.timestamp) + uint32(_duration);
if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime )
to this
if ( lockedTokens[msg.sender][tokenContract] .lastLockTime + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime )
Context
#0 - c4-judge
2024-06-04T12:41:29Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:53:10Z
alex-ppg marked the issue as satisfactory
#2 - c4-judge
2024-06-05T12:54:34Z
alex-ppg changed the severity to 3 (High Risk)
🌟 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.0037 USDC - $0.00
Judge has assessed an item in Issue #549 as 2 risk. The relevant finding follows:
The approveUSDPrice
function allows price feed contracts to vote in favor of a proposed price. However, unlike the disapproveUSDPrice
function, approveUSDPrice
does not verify whether the voter has already voted against the current proposal. A price feed contract should only be allowed to vote on one side (approve or disapprove) of the proposal. This missing check can lead to inconsistent voting behavior and potential conflicts in the proposal process.
function approveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); if (usdUpdateProposal.proposer == msg.sender) revert ProposerCannotApproveError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); // @audit - LOW should check even disapprovals if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); usdUpdateProposal.approvals[msg.sender] = _usdProposalId; usdUpdateProposal.approvalsCount++; if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) { _execUSDPriceUpdate(); } emit ApprovedUSDPrice(msg.sender); }
Without a check to prevent a voter who has disapproved the proposal from approving it, the voting process can become inconsistent, leading to potential manipulation or invalid results. This could undermine the integrity of the price update mechanism.
Add a check in the approveUSDPrice
function to ensure that a voter who has already disapproved the proposal cannot approve it.
#0 - c4-judge
2024-06-05T12:34:10Z
alex-ppg marked the issue as duplicate of #495
#1 - c4-judge
2024-06-05T12:34:23Z
alex-ppg marked the issue as partial-25
🌟 Selected for report: MrPotatoMagic
Also found by: 0xAkira, Bauchibred, Deivitto, Walter, bigtone, merlinboii
136.4103 USDC - $136.41
A potential issue has been identified within the configureToken
function related to the validation of token data. Specifically, there is no check to ensure that the decimals value in the _tokenData
parameter is different from 0, and that the usdPrice is equal to 0.
This is important because if decimals is 0 and usdPrice is different from 0, the price will later be modified by the price feed, which may lead to unexpected behavior or incorrect pricing.
function configureToken( address _tokenContract, ConfiguredToken memory _tokenData ) external onlyAdmin { if (_tokenData.nftCost == 0) revert NFTCostInvalidError(); // @audit - add here all the validation checks if (configuredTokens[_tokenContract].nftCost == 0) { configuredTokenContracts.push(_tokenContract); } configuredTokens[_tokenContract] = _tokenData; emit TokenConfigured(_tokenContract, _tokenData); }
Add checks to ensure that the decimals field in _tokenData
is not 0 and that the usdPrice is 0 before assigning _tokenData
to configuredTokens[_tokenContract]
.
The setUSDThresholds
function is intended to set the thresholds for approvals and disapprovals of a price by the price feed contracts. However, the function lacks validation checks for the _approve
and _disapprove
parameters. Specifically, these values must be between 2 and 5 (inclusive). If the thresholds are not properly validated, it can lead to invalid configurations that disrupt the voting process.
function setUSDThresholds( uint8 _approve, uint8 _disapprove ) external onlyAdmin { if (usdUpdateProposal.proposer != address(0)) revert ProposalInProgressError(); // @audit approve and disapprove need to be >= 2 and <= 5 APPROVE_THRESHOLD = _approve; DISAPPROVE_THRESHOLD = _disapprove; emit USDThresholdUpdated(_approve, _disapprove); }
Without proper validation, thresholds could be set to values that are either too low or too high. If the threshold is set to 1, it undermines the voting process, as the proposer is always counted as one vote. And for the fact that in the propose function there's no if statement that checks approvals,this will skip the first vote,making the first effective valide threshold at 2.
Add checks to ensure that the _approve
and _disapprove
values are between 2 and 5 inclusive. This ensures that the voting process is not compromised by invalid threshold values.
The proposeUSDPrice
function is responsible for proposing a new price for an asset by the price feed contracts. Within this function, the usdUpdateProposal
struct is instantiated to temporarily store the proposed values. This struct is used throughout the proposal process and is either approved or disapproved through separate functions (approveUSDPrice
and disapproveUSDPrice
). When a proposal reaches a threshold for approval or disapproval, the proposal is either executed or discarded, and the struct is reset to make way for new proposals.
The issue is that the line delete usdUpdateProposal;
is unnecessarily present at the beginning of the proposeUSDPrice
function. This deletion is redundant because the usdUpdateProposal
struct will be reset regardless of whether the proposal is approved or disapproved. Therefore, this line of code does not contribute to the function's purpose and can be removed to clean up the code.
function proposeUSDPrice( uint256 _price, address[] calldata _contracts ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer != address(0)) revert ProposalInProgressError(); if (_contracts.length == 0) revert ProposalInvalidContractsError(); delete usdUpdateProposal; // @audit QA no need to delete // Approvals will use this because when the struct is deleted the approvals remain ++_usdProposalId; usdUpdateProposal.proposedDate = uint32(block.timestamp); usdUpdateProposal.proposer = msg.sender; usdUpdateProposal.proposedPrice = _price; usdUpdateProposal.contracts = _contracts; usdUpdateProposal.approvals[msg.sender] = _usdProposalId; usdUpdateProposal.approvalsCount++; emit ProposedUSDPrice(msg.sender, _price); }
While this issue does not affect the functionality or security of the contract, it represents unnecessary code that could be removed to improve readability and maintainability.
Remove the line delete usdUpdateProposal;
from the proposeUSDPrice
function to eliminate redundant code.
The approveUSDPrice
function allows price feed contracts to vote in favor of a proposed price. However, unlike the disapproveUSDPrice
function, approveUSDPrice
does not verify whether the voter has already voted against the current proposal. A price feed contract should only be allowed to vote on one side (approve or disapprove) of the proposal. This missing check can lead to inconsistent voting behavior and potential conflicts in the proposal process.
function approveUSDPrice( uint256 _price ) external onlyOneOfRoles( [ Role.PriceFeed_1, Role.PriceFeed_2, Role.PriceFeed_3, Role.PriceFeed_4, Role.PriceFeed_5 ] ) { if (usdUpdateProposal.proposer == address(0)) revert NoProposalError(); if (usdUpdateProposal.proposer == msg.sender) revert ProposerCannotApproveError(); if (usdUpdateProposal.approvals[msg.sender] == _usdProposalId) revert ProposalAlreadyApprovedError(); // @audit - LOW should check even disapprovals if (usdUpdateProposal.proposedPrice != _price) revert ProposalPriceNotMatchedError(); usdUpdateProposal.approvals[msg.sender] = _usdProposalId; usdUpdateProposal.approvalsCount++; if (usdUpdateProposal.approvalsCount >= APPROVE_THRESHOLD) { _execUSDPriceUpdate(); } emit ApprovedUSDPrice(msg.sender); }
Without a check to prevent a voter who has disapproved the proposal from approving it, the voting process can become inconsistent, leading to potential manipulation or invalid results. This could undermine the integrity of the price update mechanism.
Add a check in the approveUSDPrice
function to ensure that a voter who has already disapproved the proposal cannot approve it.
The setLockDuration
function allows setting the lock time for a specific asset locked in the contract. While it correctly checks that the provided duration is less than the MaxLockDuration
, it does not ensure that the duration is greater than the MinLockDuration
. This omission can lead to lock durations that are too short, bypassing the intended minimum lock period.
function setLockDuration(uint256 _duration) external notPaused { if (_duration > configStorage.getUint(StorageKey.MaxLockDuration)) revert MaximumLockDurationError(); // @audit - LOW - need to add minLockDuration this could bypass minLockDuration check (present in lock,but not here) playerSettings[msg.sender].lockDuration = uint32(_duration); // update any existing lock uint256 configuredTokensLength = configuredTokenContracts.length; for (uint256 i; i < configuredTokensLength; i++) { address tokenContract = configuredTokenContracts[i]; if (lockedTokens[msg.sender][tokenContract].quantity > 0) { // check they are not setting lock time before current unlocktime if ( uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); } uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] .lastLockTime; lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration); } } emit LockDuration(msg.sender, _duration); }
Failing to enforce a minimum lock duration can allow users to set lock times that are too short, which might not comply with the intended locking policies. This can lead to issues where assets are unlocked too soon, defeating the purpose of having a minimum lock duration.
Add a check to ensure that the _duration
is also greater than or equal to the MinLockDuration
.
The _lock
function allows locking tokens within the contract, but it lacks a check for the active
status of the ConfiguredToken
. According to the provided documentation in the codebase:
/// @notice Struct holding details about tokens that can be locked /// @param usdPrice USD price per token /// @param nftCost Cost of the NFT associated with locking this token /// @param decimals Number of decimals for the token /// @param active Boolean indicating if the token is currently active for locking struct ConfiguredToken { uint256 usdPrice; uint256 nftCost; uint8 decimals; bool active; }
The active
status should be verified before allowing tokens to be locked. However, this check is missing from the _lock
function, which may allow users to lock tokens that are not intended to be locked according to the contract's configuration.
Without verifying the active
status of the token, the _lock
function may allow tokens to be locked even if they are not currently intended to be locked according to the contract's configuration. This could lead to unexpected behavior and potential misuse of the locking mechanism.
Add a check to ensure that the active
status of the ConfiguredToken
is true before allowing tokens to be locked in the _lock
function.
#0 - c4-judge
2024-06-05T12:37:50Z
alex-ppg marked the issue as grade-a
#1 - DecentralDisco
2024-06-11T19:21:59Z
For awarding purposes, staff have marked as 2nd place
. Also noting there was a 3-way tie for 2nd/3rd place.