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: 41/126
Findings: 2
Award: $0.02
🌟 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
Users' unlock time can be extended by hackers. This will lead to user's funds locked in the contract forever.
In lockOnBehalf(), the hacker can make use of his own funds to lock for the victim. And after that, the victim's unlock time will be extended based on current time.
Attack vector is like: the hacker call lockOnBehalf
with 0 _quantity
or a very small amount, the victim _onBehalfOf
's unlock time will be extended.
Considering the minimum LockDuration is 30 days, it's easy to lock the victims' funds forever.
function lockOnBehalf( address _tokenContract, uint256 _quantity, address _onBehalfOf ) external payable notPaused onlyActiveToken(_tokenContract) onlyConfiguredToken(_tokenContract) nonReentrant { address tokenOwner = msg.sender; address lockRecipient = msg.sender; if (_onBehalfOf != address(0)) { lockRecipient = _onBehalfOf; } _lock(_tokenContract, _quantity, tokenOwner, lockRecipient); }
function _lock( address _tokenContract, uint256 _quantity, address _tokenOwner, address _lockRecipient ) private { ...... LockedToken storage lockedToken = lockedTokens[_lockRecipient][ _tokenContract ]; // Transfer erc tokens if (_tokenContract != address(0)) { IERC20 token = IERC20(_tokenContract); token.transferFrom(_tokenOwner, address(this), _quantity); } lockedToken.remainder = remainder; lockedToken.quantity += _quantity; lockedToken.lastLockTime = uint32(block.timestamp); @==> unlock time will be extended by lockDuration based on current timestamp. lockedToken.unlockTime = uint32(block.timestamp) + uint32(_lockDuration); // set their lock duration in playerSettings playerSettings[_lockRecipient].lockDuration = _lockDuration; }
Manual
Don't allow the lockOnBehalf
or add one whitelist for the user. Only approved address by the user can call lockOnBehalf().
Invalid Validation
#0 - c4-judge
2024-06-05T12:59:14Z
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.0105 USDC - $0.01
Users' lock time can be reduced because incorrect time lock set in setLockDuration
.
Users can call function setLockDuration() to update lockedTokens' lock time. The function has already checked uint32(block.timestamp) + uint32(_duration) < lockedTokens[msg.sender][tokenContract].unlockTime
. It means that the update unlockTime should be equal to or larger than the previous unlockTime.
The vulnerability is the incorrect calculation of unlockTime
lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration);
The actual unlock time should be block.timestamp + _duariont.
For example:
setLockDuration
to update unlock time when 15 days pass. Now the updated unlock time is A + 15
. It's less than the previous unlock time. And users can unlock their funds directly.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) < lockedTokens[msg.sender][tokenContract].unlockTime ) { revert LockDurationReducedError(); } uint32 lastLockTime = lockedTokens[msg.sender][tokenContract] .lastLockTime; lockedTokens[msg.sender][tokenContract].unlockTime = lastLockTime + uint32(_duration); } } }
Manual
Change the calculation of unlockTime
in setLockDuration.
lockedTokens[msg.sender][tokenContract].unlockTime = uint32(block.timestamp) + uint32(_duration);
Context
#0 - c4-judge
2024-06-04T12:40:46Z
alex-ppg marked the issue as duplicate of #89
#1 - c4-judge
2024-06-05T12:54:04Z
alex-ppg marked the issue as partial-75