ERC20Snapshot and flash loans/swaps/mints

Hi! I wanted to ask about ERC20Snapshot and how it interplays with flash loans/swaps in governance designs.

Say, for example, a token is used in a governance process for voting and uses a snapshot to record balances during the voting process. Say this voting process used a commit-reveal pattern that takes the snapshot at the first reveal transaction. How can you protect this design from the voter taking a flash loan of the token before the snapshot, revealing their vote and then paying back the flash loan? In this case the voter can “trick” the snapshot into thinking they hold the tokens over a longer period of time than they actually do.

Obviously solutions exist such as not using ERC20Snapshot at all and rather transferring tokens at the commit phase and then returning them at the reveal phase which would make the flash loan impossible. Rather, I want to see if there are other designs that resolve this while still using the ERC20Snapshot pattern.

4 Likes

Hi @chrismaree,

Welcome to the community forum :wave:.

Given that flash loans/swaps/mints only apply for a single transaction, then I assume that the only transaction where balances can be adjusted using flash mechanisms and impact the balances recorded in an ERC20Snapshot is the transaction which creates the snapshot.

I also assume the access to creating an ERC20Snapshot will generally be protected by access control, so it should only be privileged accounts which can do this.

Safe guards would need to be put in place to protect against this, such as only allowing non contracts to create snapshots.

This is all based on my understanding of flash mechanisms and ERC20Snapshot, it would be great to hear from other members of the community who may have greater expertise on this.

Thanks for the feedback @abcoathup. I agree with you that separating these the first reveal and snapshot into two transactions is a good initial step. Specifically, the flashswap exploit originates from the attacker’s ability to execute the reveal snapshot after flash borrowing voting tokens. A possible mitigation to this, as you described, is to separate the snapshot and reveal logic and impose a restriction that only EOA’s can execute the snapshot. This makes it impossible to perform the flashswap exploit as this requires a contract to perform the snapshotting after the flashswap.

The logic to validate that the caller is an EOA can use the OZ isContract function here.

However, this can be circumvented by calling the function call from a contract constructor, thereby tricking the isContract into thinking it’s an EOA. To avoid this I’ve considered combining this check with a tx.origin == msg.sender check like this:

function isContract() public view returns(bool){
    uint32 size;
    address callerAddress = msg.sender;
    assembly {
      size := extcodesize(callerAddress)
    }
    return (size > 0 || callerAddress != tx.origin);
}

Does this seem reasonable? Is my proposed isContract() implementation sufficient to ensure that the caller is an EOA in all cases?

I know it’s a bit of an antipattern to use tx.origin and it might get removed from Solidity in future versions but for now I’m not sure how else to check that the caller is not a contract constructor.

1 Like

Hi @chrismaree,

Have a look at @frangio's suggestion for checking that an address is an EOA, also that isContract shouldn't be used to check that an address is not a contract:

I was thinking about the use case and I’m curious about the expected attack vector. It seems to me that if a token holder puts up their governance token for a loan, then they are explicitly choosing to loan their voting rights. Whether or not that is a legitimate use, is it significantly different to obtain those voting rights through a flash loan rather than a regular loan and then paying it back in the next block? Naturally, it requires less up-front collateral, but that seems incidental to the actual attack. It doesn’t imply the attacker is risking that collateral on a bad voting result.

If the concern is that voters should have a long-term interest in the protocol, and we stipulate that some voters will loan their rights, would it make sense to have a snapshot that also includes the last time the tokens moved? So then you could reduce the weight of tokens that were acquired recently, using whatever time period makes sense for the actual system.

On the other hand, if we’re already stipulating that token holders will loan their voting rights, it’s not obvious how much they would cooperate with the attacker to bypass any restriction to the loan.

1 Like

On the question of validating whether an address is an EOA, it might be overkill, but one possible pattern is to get the EOA to explicitly sign a message. Then you could check that the signer is msg.sender. See the mitigations proposed in this post for an example.

I haven’t got the design clear in my mind yet but the Bypasser contract in that post also suggests a mechanism to separate the economic value of the token from the voting rights, which might be useful to support the use case of a token holder who wanted to loan the tokens but retain the voting rights. The problem that I see with using it directly is that the economic value is ultimately derived from the fact that the token will eventually be transferred. I will think about this to see if I can make it work.

EDIT: @abcoathup already linked to that post - sorry about that. Also, it occurs to me that separating voting rights from economic value might be exactly what you’re trying to avoid. Unfortunately, I already shared the question with my colleague and he’s off to the races :stuck_out_tongue:. If we do end up with a mechanism, we’ll be sure to mention ways to mitigate against it as well.

1 Like

I think tx.origin == msg.sender is completely valid in this case, but I would love to hear arguments against it. I don’t think the tx.origin opcode will ever be removed from the EVM.

See next post.

1 Like

Sorry, I replied too quickly. In my previous post I meant to say that using msg.sender == tx.origin should be enough to verify that an account is an EOA (but please see an important caveat at the end of this post). After looking at your isContract function I realized you were planning to do something more complex involving tx.origin. Your function is essentially evaluating to the following expression:

Address.isContract(msg.sender) || msg.sender != tx.origin

Including Address.isContract (or any check of bytecode size or hash) in this expression is redundant if we assume that tx.origin can never be a contract. So under that assumption it should be sufficient to check only msg.sender == tx.origin before allowing a snapshot to be taken.

However, here’s the important caveat: I’m not sure we can make that assumption. There has long been an intent to introduce some kind of account abstraction in Ethereum, which might imply that in the future tx.origin could be a contract.

Here’s a recent post by Vitalik on the topic:

I would say the only 100% foolproof way to assert an account is an EOA is to require a message signed using the account’s keys.

2 Likes

In this case the voter can “trick” the snapshot into thinking they hold the tokens over a longer period of time than they actually do.

I think this sentence is useful in revealing the underlying design goal here. It indicates that the length of time that the user holds the tokens is relevant to whether or not their vote should count.

In particular, it suggests that user's vote should count only if they have "skin in the game" -- as measured both by how many tokens they have, and how long they hold them.

I don't think that simply preventing flash loans addresses this concern. For example, a whale can buy a lot of tokens, vote, and then sell the tokens a few blocks later. Such a whale would not really have much "skin in the game" either -- barely more than the flash-borrower did.

One solution to this "skin in the game" problem that I've seen proposed many times over the years is to weight votes by "token days destroyed". In other words, the votes are weighted by a function that considers both how many tokens the user holds, as well as how long they've been holding them.

However, this approach is deeply flawed and provides only a false sense of "skin in the game", because it is purely backwards-looking. It measures only how much skin in the game the user had before the time that is relevant to the current vote. It does not encourage longterm thinking on the part of the voters (as the proponents of this method often suggest), because the voters can sell immediately after the vote.

An approach that I think is promising is this one:

When the user votes, they say how long they are willing to lock up their tokens for the vote in question (this is their locktime). Then the user's vote is weighted as non-decreasing function the number of tokens they hold and their chosen locktime.

In this way the weight of the vote is a direct measure of the amount of "skin in the game" the voter has.

Note that this trivially solves the flash-loan problem, because flash-loan voters have a locktime of 0, and so their vote will have a weight of 0 no matter how many tokens they hold.

I want to make clear that I have NOT done a formal analysis of this approach. But it is a direction that seems promising to me.

EDIT/UPDATE:: I'm sure this has already been mentioned, but I want to repeat it again here: This type of timelock (that is used for incentive alignment) can be bypassed via binding agreements. In the blockchain setting, this kind of binding agreement can be coordinated via smart contract. See Bypassing Smart Contract Timelocks for more info.

3 Likes

I was thinking about the use case and I’m curious about the expected attack vector. It seems to me that if a token holder puts up their governance token for a loan, then they are explicitly choosing to loan their voting rights.

@nikeshnazareth This is definitely a valid perspective. I 100% agree with you when it comes to protocols whose primary purpose is lending. However, the protocol in question is Uniswap V2, which is a DEX that has flash loan capabilities to earn their liquidity providers more yield. Technically, you can even use Uniswap V1 for a similar end-goal.

I expect to see a general trend where protocols that lock ERC20 tokens for any purpose begin offering flash loans as a way to earn extra yield with no additional risk for their members. For that reason, I think flash loans should be considered separately from other types of loans in that the lending may not be the primary goal.

4 Likes

Ah yes, that makes sense. Thanks for the clarification!

1 Like

An idea for another mechanism to prevent manipulation is to take two snapshots, then use as voting weight the minimum balance among the two snapshots for each voter account. Maybe the first snapshot would be on the first commit, and the second on the last reveal.

Didn’t think this through so it might not work, but it occurred to me as a mechanism that doesn’t depend on checking whether accounts are EOAs or contracts (which feels like an anti-pattern).

Edit: It doesn’t work, a manipulator could do flash minting during both snapshots.

1 Like

Thanks everyone for the super useful feedback! The best solution to this problem seems to be:

  1. Separate the first revealer logic from the snapshotting logic
  2. Enforce that the snapshot is called by an EOA. While a tx.origin check would work here, this is not strictly the best solution due to the unclear future for tx.origin. Rather, getting the first revealed to submit a signature that is then validated in the contract seems like the best option.

It would also be possible to keep these contract calls joined if you enforced that the first revealer also submitted a signature with their reveal call which has the same effect as splitting the operations. Subsequent revealers don’t need to send a signature (can be a blank field) as only the snapshotting step needs to enforce the EOA check.

1 Like