Platform: Code4rena
Start Date: 03/05/2023
Pot Size: $60,500 USDC
Total HM: 25
Participants: 114
Period: 8 days
Judge: Picodes
Total Solo HM: 6
Id: 234
League: ETH
Rank: 56/114
Findings: 1
Award: $171.22
🌟 Selected for report: 0
🚀 Solo Findings: 0
171.2239 USDC - $171.22
https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L226 https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L164 https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/token/AjnaToken.sol#L11 https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/GrantFund.sol#L58 https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L105
It is possible to pass an extraordinary funding proposal without the required quorum at the snapshot.
Furthermore, it is possible to submit a proposal requesting zero tokens, exposing a potential DoS attack on extraordinary funding and locking up treasury funds.
Since there are only 9 extraordinary proposals possible during the lifetime of the protocol, this can jeopardize the longevity and long-term functioning of the project and therefore rate this as a medium risk.
Passing a proposal depends on _extraordinaryProposalSucceeded
returning true. One of the conditions is to have votesReceived
be greater or equal than the tokensRequested
and the _getSliceOfNonTreasury
.
function _extraordinaryProposalSucceeded( uint256 proposalId_, uint256 tokensRequested_ ) internal view returns (bool) { uint256 votesReceived = uint256(_extraordinaryFundingProposals[proposalId_].votesReceived); uint256 minThresholdPercentage = _getMinimumThresholdPercentage(); return // succeeded if proposal's votes received doesn't exceed the minimum threshold required (votesReceived >= tokensRequested_ + _getSliceOfNonTreasury(minThresholdPercentage)) && // succeeded if tokens requested are available for claiming from the treasury (tokensRequested_ <= _getSliceOfTreasury(Maths.WAD - minThresholdPercentage)) ; }
This is supposed to get a percentage of the non treasury tokens to determine whether the proposal has received enough votes. This calculation relies on the current totalSupply
and value of the treasury
.
/** * @notice Get the number of ajna tokens equivalent to a given percentage. * @param percentage_ The percentage of the Non treasury to retrieve, in WAD. * @return The number of tokens, in WAD. */ function _getSliceOfNonTreasury( uint256 percentage_ ) internal view returns (uint256) { uint256 totalAjnaSupply = IERC20(ajnaTokenAddress).totalSupply(); return Maths.wmul(totalAjnaSupply - treasury, percentage_); }
Both totalSupply
and treasury
can be manipulated while a proposal is active to decrease the threshold needed to pass the proposal.
Furthermore in proposeExtraordinary
, only an upper bound is checked for totalTokensRequested
so it's possible to submit proposals asking for zero tokens.
// check tokens requested are available for claiming from the treasury if (uint256(totalTokensRequested) > _getSliceOfTreasury(Maths.WAD - _getMinimumThresholdPercentage())) revert InvalidProposal();
The AjnaToken
implements OZ's ERC20Burnable
and thus exposes the burn
function. A sufficiently motivated actor can burn their own tokens or buy up tokens in the open market to reduce the total supply.
Let us assume a total supply of 1,000
tokens with the treasury holding 500
tokens and thus 500
non treasury tokens in circulation. Let's further assume this is the first proposal asking for zero tokens so requiring a quorum of 50%
of non treasury tokens or 250
tokens.
At snapshot time, this would require 250
votes. Let's assume an actor holds 200
tokens and has already cast their vote. He can then go ahead and burn 100
tokens to reduce the supply of non treasury tokens from 500
down to 400
. He now only requires 200
votes and can thus pass the proposal.
The GrantFund
contract exposes a fundTreasury
function allowing anyone to transfer tokens to the treasury and thus increase the treasury
. A sufficiently motivated actor can transfer tokens to the treasury decreasing the number of non-treasury tokens and thus reducing the number of votes required.
Let us again assume a total supply of 1,000
tokens with the treasury holding 500
tokens and thus 500
non treasury tokens in circulation. Let's also again assume this is the first proposal asking for zero tokens so requiring a quorum of 50%
of non treasury tokens or 250
tokens.
At snapshot time, this would require 250 votes
. Let's assume an actor holds 200 tokens
and has already cast their vote. He can then go ahead and transfer 100 tokens
to increase the treasury from 500
to 600
and thus the amount of non treasury tokens to 400
. He now only requires 200 votes
and can thus pass the proposal.
Manual review
Possible mitigation to avoid manipulation of totalSupply
, treasury
and totalTokensRequested
-
ERC20Votes.getPastTotalSupply
function to query the token supply at a specific snapshot timeERC20Votes.getPastVotes
for the GrantFund
contract to snapshot the balance of the treasurytotalTokensRequested
Math
#0 - c4-sponsor
2023-05-19T18:58:37Z
MikeHathaway marked the issue as sponsor disputed
#1 - MikeHathaway
2023-05-19T18:59:35Z
It's valid behavior for token holders to decide to use up the extraordinary mechanism with 0 vote proposals. Likewise, there wouldn't be much harm to users from large scale burns or transfers of tokens to the treasury. Both of those cases represent a transfer of wealth from the "attacker" to other token holders.
#2 - c4-judge
2023-05-30T22:33:08Z
Picodes marked the issue as duplicate of #285
#3 - c4-judge
2023-05-30T22:33:12Z
Picodes marked the issue as satisfactory