Protocol does not account for fee on transfer. This would lead to loss for the protocol.
Proof of Concept
According to the README.md, we are to assume that the protocol could add more tokens in the future for use in LockManager. How ever the current implementation will not work with fee on transfer tokens.
Upon locking tokens the user's lockedToken.quantity gets increased by _quantity, when it should be getting increased by _quantity - fee in the case of fee on transfer tokens.
This will lead to loss for the protocol, since in the unlock function, the token.transfer(msg.sender, _quantity); line will send the quantity back to the user, with the protocol having to pay the fees for the fee on transfer.
function _lock(
address _tokenContract,
uint256 _quantity,
address _tokenOwner,
address _lockRecipient
) private {
(
address _mainAccount,
MunchablesCommonLib.Player memory _player
) = accountManager.getPlayer(_lockRecipient);
if (_mainAccount != _lockRecipient) revert SubAccountCannotLockError();
if (_player.registrationDate == 0) revert AccountNotRegisteredError();
// check approvals and value of tx matches
if (_tokenContract == address(0)) {
if (msg.value != _quantity) revert ETHValueIncorrectError();
} else {
if (msg.value != 0) revert InvalidMessageValueError();
IERC20 token = IERC20(_tokenContract);
uint256 allowance = token.allowance(_tokenOwner, address(this));
if (allowance < _quantity) revert InsufficientAllowanceError();
}
LockedToken storage lockedToken = lockedTokens[_lockRecipient][
_tokenContract
];
ConfiguredToken storage configuredToken = configuredTokens[
_tokenContract
];
// they will receive schnibbles at the new rate since last harvest if not for force harvest
accountManager.forceHarvest(_lockRecipient);
// add remainder from any previous lock
uint256 quantity = _quantity + lockedToken.remainder;
uint256 remainder;
uint256 numberNFTs;
uint32 _lockDuration = playerSettings[_lockRecipient].lockDuration;
if (_lockDuration == 0) {
_lockDuration = lockdrop.minLockDuration;
}
if (
lockdrop.start <= uint32(block.timestamp) &&
lockdrop.end >= uint32(block.timestamp)
) {
if (
_lockDuration < lockdrop.minLockDuration ||
_lockDuration >
uint32(configStorage.getUint(StorageKey.MaxLockDuration))
) revert InvalidLockDurationError();
if (msg.sender != address(migrationManager)) {
// calculate number of nfts
remainder = quantity % configuredToken.nftCost;
numberNFTs = (quantity - remainder) / configuredToken.nftCost;
if (numberNFTs > type(uint16).max) revert TooManyNFTsError();
// Tell nftOverlord that the player has new unopened Munchables
nftOverlord.addReveal(_lockRecipient, uint16(numberNFTs));
}
}
// 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);
lockedToken.unlockTime =
uint32(block.timestamp) +
uint32(_lockDuration);
// set their lock duration in playerSettings
playerSettings[_lockRecipient].lockDuration = _lockDuration;
emit Locked(
_lockRecipient,
_tokenOwner,
_tokenContract,
_quantity,
remainder,
numberNFTs,
_lockDuration
);
}
function unlock(
address _tokenContract,
uint256 _quantity
) external notPaused nonReentrant {
LockedToken storage lockedToken = lockedTokens[msg.sender][
_tokenContract
];
if (lockedToken.quantity < _quantity)
revert InsufficientLockAmountError();
if (lockedToken.unlockTime > uint32(block.timestamp))
revert TokenStillLockedError();
// force harvest to make sure that they get the schnibbles that they are entitled to
accountManager.forceHarvest(msg.sender);
lockedToken.quantity -= _quantity;
// send token
if (_tokenContract == address(0)) {
payable(msg.sender).transfer(_quantity);
} else {
IERC20 token = IERC20(_tokenContract);
token.transfer(msg.sender, _quantity);
}
emit Unlocked(msg.sender, _tokenContract, _quantity);
}
Tools Used
Manual review
Recommended Mitigation Steps
It is recommended to get the balance of the current contract before and after the transferFrom to see how much tokens were received.
Lines of code
https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L311-L398 https://github.com/code-423n4/2024-05-munchables/blob/57dff486c3cd905f21b330c2157fe23da2a4807d/src/managers/LockManager.sol#L401-L427
Vulnerability details
Impact
Protocol does not account for fee on transfer. This would lead to loss for the protocol.
Proof of Concept
According to the README.md, we are to assume that the protocol could add more tokens in the future for use in LockManager. How ever the current implementation will not work with fee on transfer tokens.
Upon locking tokens the user's
lockedToken.quantity
gets increased by_quantity
, when it should be getting increased by_quantity - fee
in the case of fee on transfer tokens.This will lead to loss for the protocol, since in the
unlock
function, thetoken.transfer(msg.sender, _quantity);
line will send the quantity back to the user, with the protocol having to pay the fees for the fee on transfer.Tools Used
Manual review
Recommended Mitigation Steps
It is recommended to get the balance of the current contract before and after the transferFrom to see how much tokens were received.
Assessed type
ERC20