There seems to be an edge case where a user can cast a vote and end a poll within the same block. As such, this exposes several attack vectors but more specifically a case where a user can use a flashloan to stake ANC tokens, vote on a poll, end the poll and withdraw their staked tokens in a single transaction. This may allows users to execute arbitrary proposals, assuming their are more liquid ANC tokens than staked ANC tokens.
Proof of Concept
Let's consider the following scenario:
A poll has started and a snapshot has been taken where there are 1000 staked ANC tokens.
Alice waits until the last block to cast her vote. Before she does this, she utilises a flashloan to gain access to 500 ANC tokens of liquidity.
Because end_poll() has check a_poll.end_height > env.block.height to see if the voting period is active and cast_vote() has check env.block.height > a_poll.end_height to see if the voting period has elapsed. We can satisfy both of these cases when env.block.height = a_poll.end_height. As such, we can cast a vote and end the poll in the same block.
Alice casts a vote of 500 ANC tokens and ends the poll, resulting in the successful execution of the proposal without having to have locked any tokens as she can withdraw these tokens to pay back the flashloan.
Tools Used
Manual code review.
Recommended Mitigation Steps
Consider updating one of the two checks in end_poll() or cast_vote() such that there is no crossover between the two checks. There should be no way to stake tokens, cast a vote, end the poll and withdraw staked tokens, so it might also be useful to prevent staking and withdrawing ANC tokens within a single block.
Lines of code
https://github.com/code-423n4/2022-02-anchor/blob/main/contracts/anchor-token-contracts/contracts/gov/src/contract.rs#L364-L455 https://github.com/code-423n4/2022-02-anchor/blob/main/contracts/anchor-token-contracts/contracts/gov/src/contract.rs#L582-L665 https://github.com/code-423n4/2022-02-anchor/blob/main/contracts/anchor-token-contracts/contracts/gov/src/staking.rs#L15-L116
Vulnerability details
Impact
There seems to be an edge case where a user can cast a vote and end a poll within the same block. As such, this exposes several attack vectors but more specifically a case where a user can use a flashloan to stake ANC tokens, vote on a poll, end the poll and withdraw their staked tokens in a single transaction. This may allows users to execute arbitrary proposals, assuming their are more liquid ANC tokens than staked ANC tokens.
Proof of Concept
Let's consider the following scenario:
end_poll()
has checka_poll.end_height > env.block.height
to see if the voting period is active andcast_vote()
has checkenv.block.height > a_poll.end_height
to see if the voting period has elapsed. We can satisfy both of these cases whenenv.block.height = a_poll.end_height
. As such, we can cast a vote and end the poll in the same block.Tools Used
Manual code review.
Recommended Mitigation Steps
Consider updating one of the two checks in
end_poll()
orcast_vote()
such that there is no crossover between the two checks. There should be no way to stake tokens, cast a vote, end the poll and withdraw staked tokens, so it might also be useful to prevent staking and withdrawing ANC tokens within a single block.