In the setLockDuration function in LockManager.sol, the protocol tries to ensure, that the player does not reduce the locking time of their tokens with the following check:
Since lastLockTime is almost always lower than block.timestamp, it is possible to reduce the unlockTime.
This causes the player to be able to wait a bit and then reduce the unlockTime due to the discrepancy between lastLockTime and block.timestamp.
On the code4rena audit page the following is stated: The most important thing is that funds cannot get locked forever, people cannot take other people's funds, and that people cannot reduce lockup times that are previously set.
which gets clearly violated by this, breaking one of the very important parts of the protocol.
Proof of Concept
Please create a file LockManager.t.sol in src/test/ and add the following contract:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
import {console} from "forge-std/console.sol";
import "./MunchablesTest.sol";
import "../managers/LockManager.sol";
contract LManager is MunchablesTest {
function test_LockDuration() public {
deployContracts();
address[] memory addresses = new address[](1);
addresses[0] = address(this);
amp.register(MunchablesCommonLib.Realm(3), address(0));
// mint an NFT for me
IOldMunchNFT(address(oldnftp)).safeMint(address(this), "Q");
lm.getConfiguredToken(address(usdb));
IERC20(usdb).balanceOf(address(this));
IERC20(usdb).approve(address(lm), 1000000);
lm.lock(address(usdb), 1000000);
uint lastLockedBefore = lm.getLocked(address(this))[1].lockedToken.lastLockTime;
uint unlockTimeBefore = lm.getLocked(address(this))[1].lockedToken.unlockTime;
console.logUint(unlockTimeBefore);
vm.warp(unlockTimeBefore/2);
lm.setLockDuration(unlockTimeBefore - unlockTimeBefore/2);
uint unlockTimeAfter = lm.getLocked(address(this))[1].lockedToken.unlockTime;
console.logUint(unlockTimeAfter);
vm.warp(block.timestamp + 5);
console.logUint(block.timestamp);
lm.unlock(address(usdb), 1000000);
}
}
You can then test this with forge test --match-test test_LockDuration -vv.
This will show that we can unlock our tokens way before the unlockTime has passed.
The output will look somehow like this:
Logs:
Created config storage
Created account manager 0x2e234DAe75C793f67A35089C9d99245E1C58470b
.
.
86401
43202
43205
As it will not revert we can see, we are able to unlock our tokens at the timestamp 43205 while the unlockTime initially is set to 86401.
Tools Used
Manual review, foundry
Recommended Mitigation Steps
The if-statement should be checked based on lastLockTime:
if (lockedTokens[msg.sender][tokenContract].quantity > 0) {
uint32 lastLockTime = lockedTokens[msg.sender][tokenContract]
.lastLockTime;
// check they are not setting lock time before current unlocktime
if (
lastLockTime + uint32(_duration) <
lockedTokens[msg.sender][tokenContract].unlockTime
) {
revert LockDurationReducedError();
}
lockedTokens[msg.sender][tokenContract].unlockTime =
lastLockTime +
uint32(_duration);
}
Lines of code
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L256-L267
Vulnerability details
In the
setLockDuration
function inLockManager.sol
, the protocol tries to ensure, that the player does not reduce the locking time of their tokens with the following check:The problem arises when the
unlockTime
is then later set by adding_duration
to thelastLockedTime
and notblock.timestamp
:Impact
Since
lastLockTime
is almost always lower thanblock.timestamp
, it is possible to reduce theunlockTime
. This causes the player to be able to wait a bit and then reduce theunlockTime
due to the discrepancy betweenlastLockTime
andblock.timestamp
. On the code4rena audit page the following is stated:The most important thing is that funds cannot get locked forever, people cannot take other people's funds, and that people cannot reduce lockup times that are previously set.
which gets clearly violated by this, breaking one of the very important parts of the protocol.Proof of Concept
Please create a file
LockManager.t.sol
insrc/test/
and add the following contract:You can then test this with
forge test --match-test test_LockDuration -vv
. This will show that we can unlock our tokens way before theunlockTime
has passed. The output will look somehow like this:As it will not revert we can see, we are able to unlock our tokens at the timestamp
43205
while theunlockTime
initially is set to86401
.Tools Used
Manual review, foundry
Recommended Mitigation Steps
The if-statement should be checked based on
lastLockTime
:Assessed type
Invalid Validation