Bad check in Vesting.sol::_resetVestingPlans will prevent users from adding additional liquidity in SymmVesting.sol
Summary
The _resetVestingPlans check makes it impossible to increase a user's locked tokens if the increase does not push the new amount above the total unlocked tokens. This is problematic as it will prevent users from adding additional liquidity to the SymmVesting.sol after a certain number of their lp tokens have been unlocked.
Root Cause
In Vesting.sol:231, the check will cause a revert when a user tries to add additional liquidity
Internal Pre-conditions
The user must already have some vested lp tokens
External Pre-conditions
NIL
Attack Path
NIL
Impact
Users are unable to add additional liquidity
PoC
Follow the guide here to integrate foundry into this codebase. Then add the following test into a new file:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;
import {SymmStaking} from "../../contracts/staking/SymmStaking.sol";
import {Symmio} from "../../contracts/token/symm.sol";
import {MockERC20} from "../../contracts/mock/MockERC20.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {SymmVesting} from "../../contracts/vesting/SymmVesting.sol";
import {Test, console} from "forge-std/Test.sol";
contract TestSuite is Test {
SymmStaking symmStaking;
Symmio symm;
SymmStaking implementation;
SymmVesting symmVesting;
SymmVesting vestingImplementation;
address rewardToken;
address admin;
address lockedClaimPenaltyReceiver;
address pool;
address router;
address permit2;
address vault;
address usdc;
address symm_lp;
function setUp() public {
admin = makeAddr("admin");
lockedClaimPenaltyReceiver = makeAddr("lockedClaimPenaltyReceiver");
pool = 0x94Bf449AB92be226109f2Ed3CE2b297Db94bD995;
router = 0x76578ecf9a141296Ec657847fb45B0585bCDa3a6;
permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
vault = 0xbA1333333333a1BA1108E8412f11850A5C319bA9;
usdc = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
symm_lp = 0x94Bf449AB92be226109f2Ed3CE2b297Db94bD995;
symm = Symmio(0x800822d361335b4d5F352Dac293cA4128b5B605f);
implementation = new SymmStaking();
vestingImplementation = new SymmVesting();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(address(implementation), admin, "");
TransparentUpgradeableProxy vestingProxy =
new TransparentUpgradeableProxy(address(vestingImplementation), admin, "");
symmStaking = SymmStaking(address(proxy));
symmVesting = SymmVesting(address(vestingProxy));
vm.startPrank(admin);
symmStaking.initialize(admin, address(symm));
symmVesting.initialize(
admin, lockedClaimPenaltyReceiver, pool, router, permit2, vault, address(symm), usdc, symm_lp
);
rewardToken = address(new MockERC20("Token", "TOK"));
vm.stopPrank();
}
function testUsersWillBeUnableToProvideLiquidityAfterACertainNumberOfUnlockedTokens() public {
//admin creates user vest with symm
address user = makeAddr("user");
uint256 userVestAmount = 10e18;
uint256 totalVestedSymmAmount = 100e18;
uint256 startTime = block.timestamp;
uint256 endTime = block.timestamp + 10 days;
deal(usdc, user, 1000e18);
address[] memory users = new address[](1);
users[0] = user;
uint256[] memory amounts = new uint256[](1);
amounts[0] = userVestAmount;
vm.startPrank(admin);
deal(address(symm), address(symmVesting), totalVestedSymmAmount);
symmVesting.setupVestingPlans(address(symm), startTime, endTime, users, amounts);
vm.stopPrank();
//user adds half their vested tokens as liquidity
vm.startPrank(user);
MockERC20(usdc).approve(address(symmVesting), type(uint256).max);
symmVesting.addLiquidity(userVestAmount / 2, 0, 0);
vm.stopPrank();
//move time so more than half of created symm_lp vesting tokens are unlocked
vm.warp(block.timestamp + 7 days);
uint256 secondLiquidityAmount = symmVesting.getLockedAmountsForToken(user, address(symm));
//second addLiquidity call will revert with "AlreadyClaimedMoreThanThis" error
vm.startPrank(user);
MockERC20(usdc).approve(address(symmVesting), type(uint256).max);
vm.expectRevert();
symmVesting.addLiquidity(secondLiquidityAmount, 0, 0);
vm.stopPrank();
}
}Mitigation
Remove the check:
function _resetVestingPlans(address token, address[] memory users, uint256[] memory amounts) internal {
if (users.length != amounts.length) revert MismatchArrays();
uint256 len = users.length;
for (uint256 i = 0; i < len; i++) {
address user = users[i];
uint256 amount = amounts[i];
_claimUnlockedToken(token, user);
VestingPlan storage vestingPlan = vestingPlans[token][user];
- if (amount < vestingPlan.unlockedAmount()) revert AlreadyClaimedMoreThanThis();
uint256 oldTotal = vestingPlan.lockedAmount();
vestingPlan.resetAmount(amount);
totalVested[token] = totalVested[token] - oldTotal + amount;
emit VestingPlanReset(token, user, amount);
}
}