An account with a short lock can delegate to an account with the same length of lock. The delegatee can then call increaseUnlockTime() to increase their lock time all the way to maximum. There is nothing the delegator can do to avoid this. The delegator's tokens would then be effectively locked by their delegatee, who could continue to extend every week. The only way for the delegator to release themselves is to extend their lock time to match the delegatee's and then undelegate. This could significantly harm that user, forcing them to keep tokens locked for much longer than intended. It could also significantly harm the protocol, which may have to deal with many unhappy, trapped users and the resulting reputational harm.
Note also that the delegatee has a financial incentive to perpetrate this exploit, especially if they believe the delegator will not notice for a long time: it increases the delegatee's voting power.
With a short lock period, a delegatee could potentially allow their lock to expire, transfer out the majority of their own tokens, and then recreate the maximum length lock, gaining the voting power of the dellegator for a long period without locking many of their own tokens.
Recommended Mitigation Steps
Add a field to the struct LockedBalance to store a timestamp: LockedBalance.delegatedEnd
When an account delegates, record the expiry timestamp of the delegatee's lock in the delegator's LockedBalance.delegatedEnd. Then use LockedBalance.delegatedEnd in the tests within delegate(): if LockedBalance.delegatedEnd < block.timestamp, undelegation should be allowed.
Lines of code
https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L493-L659
Vulnerability details
Impact
An account with a short lock can delegate to an account with the same length of lock. The delegatee can then call
increaseUnlockTime()
to increase their lock time all the way to maximum. There is nothing the delegator can do to avoid this. The delegator's tokens would then be effectively locked by their delegatee, who could continue to extend every week. The only way for the delegator to release themselves is to extend their lock time to match the delegatee's and then undelegate. This could significantly harm that user, forcing them to keep tokens locked for much longer than intended. It could also significantly harm the protocol, which may have to deal with many unhappy, trapped users and the resulting reputational harm. Note also that the delegatee has a financial incentive to perpetrate this exploit, especially if they believe the delegator will not notice for a long time: it increases the delegatee's voting power. With a short lock period, a delegatee could potentially allow their lock to expire, transfer out the majority of their own tokens, and then recreate the maximum length lock, gaining the voting power of the dellegator for a long period without locking many of their own tokens.Recommended Mitigation Steps
Add a field to the struct
LockedBalance
to store a timestamp:LockedBalance.delegatedEnd
When an account delegates, record the expiry timestamp of the delegatee's lock in the delegator'sLockedBalance.delegatedEnd
. Then useLockedBalance.delegatedEnd
in the tests withindelegate()
: ifLockedBalance.delegatedEnd < block.timestamp
, undelegation should be allowed.Proof of Concept