In LendingLedger.sol and votingEscrow.sol, low level call made using the call, According to the Solidity docs, "The low-level functions call, delegatecall and staticcall return true as their first return value if the account called is non-existent, as part of the design of the EVM. Account existence must be checked prior to calling if needed".As a result, it is possible that this call will fail, but votingEscrow#withdraw() and lendingLedger#claim() will not notice anything went wrong
Proof of Concept
the claim function using .call to transfer token to callee:
function claim(
address _market,
uint256 _claimFromTimestamp,
uint256 _claimUpToTimestamp
) external is_valid_epoch(_claimFromTimestamp) is_valid_epoch(_claimUpToTimestamp) {
address lender = msg.sender;
uint256 userLastClaimed = userClaimedEpoch[_market][lender];
require(userLastClaimed > 0, "No deposits for this user");
_checkpoint_lender(_market, lender, _claimUpToTimestamp);
_checkpoint_market(_market, _claimUpToTimestamp);
uint256 currEpoch = (block.timestamp / WEEK) * WEEK;
uint256 claimStart = Math.max(userLastClaimed, _claimFromTimestamp);
uint256 claimEnd = Math.min(currEpoch - WEEK, _claimUpToTimestamp);
uint256 cantoToSend;
if (claimEnd >= claimStart) {
// This ensures that we only set userClaimedEpoch when a claim actually happened
for (uint256 i = claimStart; i <= claimEnd; i += WEEK) {
uint256 userBalance = lendingMarketBalances[_market][lender][i];
uint256 marketBalance = lendingMarketTotalBalance[_market][i];
RewardInformation memory ri = rewardInformation[i];
require(ri.set, "Reward not set yet"); // Can only claim for epochs where rewards are set, even if it is set to 0
uint256 marketWeight = gaugeController.gauge_relative_weight_write(_market, i); // Normalized to 1e18
cantoToSend += (marketWeight * userBalance * ri.amount) / (1e18 * marketBalance); // (marketWeight / 1e18) * (userBalance / marketBalance) * ri.amount;
}
userClaimedEpoch[_market][lender] = claimEnd + WEEK;
}
//@audit
if (cantoToSend > 0) {
(bool success, ) = msg.sender.call{value: cantoToSend}("");
require(success, "Failed to send CANTO");
}
}
same thing for withdraw function
function withdraw() external nonReentrant {
LockedBalance memory locked_ = locked[msg.sender];
// Validate inputs
require(locked_.amount > 0, "No lock");
require(locked_.end <= block.timestamp, "Lock not expired");
require(locked_.delegatee == msg.sender, "Lock delegated");
// Update lock
uint256 amountToSend = uint256(uint128(locked_.amount));
LockedBalance memory newLocked = _copyLock(locked_);
newLocked.amount = 0;
newLocked.end = 0;
newLocked.delegated -= int128(int256(amountToSend));
newLocked.delegatee = address(0);
locked[msg.sender] = newLocked;
newLocked.delegated = 0;
// oldLocked can have either expired <= timestamp or zero end
// currentLock has only 0 end
// Both can have >= 0 amount
_checkpoint(msg.sender, locked_, newLocked);
// Send back deposited tokens
//@audit
(bool success, ) = msg.sender.call{value: amountToSend}("");
require(success, "Failed to send CANTO");
emit Withdraw(msg.sender, amountToSend, LockAction.WITHDRAW, block.timestamp);
}
Tools Used
manual review
Recommended Mitigation Steps
Check for the msg.sender if its valid address or not, so that failures are not missed.
Lines of code
https://github.com/code-423n4/2023-08-verwa/blob/498a3004d577c8c5d0c71bff99ea3a7907b5ec23/src/VotingEscrow.sol#L346 https://github.com/code-423n4/2023-08-verwa/blob/498a3004d577c8c5d0c71bff99ea3a7907b5ec23/src/LendingLedger.sol#L179
Vulnerability details
Impact
In
LendingLedger.sol
andvotingEscrow.sol
, low level call made using thecall
, According to the Solidity docs, "The low-level functionscall
,delegatecall
andstaticcall
return true as their first return value if the account called is non-existent, as part of the design of the EVM. Account existence must be checked prior to calling if needed".As a result, it is possible that this call will fail, butvotingEscrow#withdraw()
andlendingLedger#claim()
will not notice anything went wrongProof of Concept
the
claim
function using .call to transfer token to callee:same thing for
withdraw
functionTools Used
manual review
Recommended Mitigation Steps
Check for the msg.sender if its valid address or not, so that failures are not missed.
Assessed type
Other