Unbounded delegating increases gas cost of transfer and can lock all funds #75
Labels
bug
Something isn't working
QA (Quality Assurance)
Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax
sponsor disputed
Sponsor cannot duplicate the issue, or otherwise disagrees this is an issue
Lines of code
https://github.com/fei-protocol/flywheel-v2/blob/main/src/token/ERC20MultiVotes.sol#L246
Vulnerability details
Impact
ERC20MultiVotes
has variablemaxDelegates
that determines the maximum number of accounts any account can delegate to. This check is ignored for contracts wherecanContractExceedMaxDelegates[address]
is set totrue
. These contracts can delegate to an uncapped number of accounts. This will in turn increase the gas cost of callingtransfer
transferFrom
and_burn
because they call_decrementVotesUntilFree
which caches every delegated address when the contract does not have enoughfreeVotes
to transfer the requested amount. Since the number of delegates is uncapped, it can increase to the point where transferring any amount of tokens will exceed the block gas limit, locking all funds in the delegating contract.Proof of Concept
The purpose of allowing some contracts to ignore the maxDelegates check is to allow users to earn yield by depositing while maintaining their ability to vote on governance proposals. If the contract that is deposited into automatically delegates all votes back to the users, and exceeds a certain number of users, all funds will be locked in the contract. Some example scenarios:
If a contract delegates to ~4000 users, it will no longer be able to transfer all funds. This can be an issue if trying to rescue funds from a contract with a bug or move funds to a new implementation.
If a contract delegates to ~30000 users, calling
delegates(contract)
in a state changing function will exceed the block gas limit.If a contract delegates to ~100000 users, transferring any amount of tokens will exceed the block gas limit.
This is a problem even if any of these limits aren't reached because the gas is paid for by users. Even at a fraction of the limit, UX will take a big hit since each user withdrawing will have to pay high fees.
Tools Used
Manual analysis. Foundry tests.
I added this test to
ERC20MultiVotes.t.sol
to come up with the numbers above.Recommended Mitigation Steps
To ensure the contract doesn't lock all funds, a global maxDelegates can be used. However, this issue is pretty complex and would more effectively be solved at the level of the contract that takes deposits. One solution would be to ensure the delegating contract has enough
freeVotes
to cover any single user withdrawing funds. This can be done by checking the state of the contract off-chain and depositing funds when necessary. It can also be somewhat mitigated by not delegating to every user that deposits and instead giving them the option of "requesting votes", however this can still lead to the same issue if everyone requests votes. A combination of these two methods would work well.Another solution, which I think is the most optimal, would be to reimplement
_decrementVotesUntilFree
to not cache all addresses and instead just remove delegates starting at index 0 until free using.length()
and.at()
methods provided byEnumerableSet
. This solution does not solve the problem with callingdelegates(contract)
nor does it allow transferring all tokens at once with > 4000 delegates. This second point can be dealt with by breaking up the transfer into smaller functions and bundling using flashbots. I have added an example implementation below.The sponsor mentioned they intend to solve this at the delegating contract level, nonetheless, this issue should at least be mentioned in the contract as it is open source and others might use it with an improper implementation.
The text was updated successfully, but these errors were encountered: