Platform: Code4rena
Start Date: 13/12/2023
Pot Size: $36,500 USDC
Total HM: 18
Participants: 110
Period: 8 days
Judge: 0xTheC0der
Id: 311
League: ETH
Rank: 5/110
Findings: 3
Award: $1,220.27
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: ast3ros
Also found by: 0xG0P1, KupiaSec, Pechenite, cccz, deth, dimulski, mojito_auditor, osmanozdemir1, peanuts, rvierdiiev, zhaojie
51.1381 USDC - $51.14
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L226 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L313
In CultureIndex
, malicious users buy governance tokens immediately after createPiece
to allow voting to pass early, because quorumVotes
are lower than expected.
quorumVotes
are calculated using the totalVotesSupply
of governance tokens:
function createPiece( ArtPieceMetadata calldata metadata, CreatorBps[] calldata creatorArray ) public returns (uint256) { ..... newPiece.creationBlock = block.number; newPiece.totalVotesSupply = _calculateVoteWeight( erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply() ); .... newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000; }
If the malicious user mint token after createPiece
, totalSupply
has actually increased, but the quorumVotes
calculated previously still use the old value, so quorumVotes
are lower than they actually are.
A malicious user can create piece in the same block first and then buy tokens, so that his Piece will be more advantageous,
In the early days of the protocol's launch, when totalSupply
is low, malicious users can mint tokens that exceed the threshold of quorumVotes
and can immediately pass the vote.
When voting, the BlockNumber used at the time of piece creation is used to obtain a snapshot of voting power, so minted's token(voting power) after createPiece
in the same block can be used for voting.
function _vote(uint256 pieceId, address voter) internal { require(pieceId < _currentPieceId, "Invalid piece ID"); require(voter != address(0), "Invalid voter address"); require(!pieces[pieceId].isDropped, "Piece has already been dropped"); require(!(votes[pieceId][voter].voterAddress != address(0)), "Already voted"); @> uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock); require(weight > minVoteWeight, "Weight must be greater than minVoteWeight"); votes[pieceId][voter] = Vote(voter, weight); totalVoteWeights[pieceId] += weight; uint256 totalWeight = totalVoteWeights[pieceId]; maxHeap.updateValue(pieceId, totalWeight); emit VoteCast(pieceId, voter, weight, totalWeight); }
vscode manual
function _vote(uint256 pieceId, address voter) internal { ..... - uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock); + uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock - 1); }
Governance
#0 - c4-pre-sort
2023-12-22T07:59:17Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T07:59:26Z
raymondfam marked the issue as duplicate of #16
#2 - c4-pre-sort
2023-12-24T15:11:13Z
raymondfam marked the issue as duplicate of #449
#3 - c4-judge
2024-01-06T15:56:35Z
MarioPoneder changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-01-06T16:01:29Z
MarioPoneder marked the issue as not a duplicate
#5 - c4-judge
2024-01-06T16:01:38Z
MarioPoneder marked the issue as duplicate of #409
#6 - c4-judge
2024-01-07T00:23:21Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: deepplus
Also found by: 0xDING99YA, 0xmystery, Aymen0909, DanielArmstrong, Inference, KupiaSec, SadeeqXmosh, SpicyMeatball, Tricko, adeolu, jnforja, passteque, rvierdiiev, wangxx2026, zhaojie
25.1638 USDC - $25.16
ERC20TokenEmitter#buyToken lacks slippage protection, users may pay a higher price than expected.
buyToken uses variable price (VRGDAC), when the total supply increases, the price is likely to increase, especially when the threshold is reached, the price will increase very quickly (according to VRGDAC).
For example, after the user initiates a transaction, a large transaction is inserted in front of the total supply increases a lot, because the large transaction is executed before the user's transaction, so the number of tokens obtained by the user will be far less than the expected value, which is the user does not want to see.
Therefore, it is necessary to set a minimum value when purchasing tokens. If the price changes cause the purchased tokens to be less than this minimum value, the transaction will be cancelled.
vscode manual
add slippage protection
Error
#0 - c4-pre-sort
2023-12-22T08:00:21Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T08:00:29Z
raymondfam marked the issue as duplicate of #26
#2 - c4-pre-sort
2023-12-24T06:00:47Z
raymondfam marked the issue as duplicate of #397
#3 - c4-judge
2024-01-06T16:30:11Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: zhaojie
Also found by: jerseyjoewalcott
1143.9691 USDC - $1,143.97
The timeSinceStart
in the vrgdac.xToY
function will revert over a certain value, resulting in the ERC20TokenEmitter#buyToken
function always revert.
Initialize the VRGDAC using the parameters in the test code.
VRGDAC vrgdac = new VRGDAC(1 ether, 1e18 / 10, 1_000 * 1e18);
The timeSinceStart
is set to 394 days in the test code:
function testVRGDAC_time() public { VRGDAC vrgdac = new VRGDAC(1 ether, 1e18 / 10, 1_000 * 1e18); int256 x = vrgdac.yToX({ timeSinceStart: toDaysWadUnsafe(86400 * 400), sold: 1000 ether, amount: 1 ether }); uint256 xx = uint256(x); console.log(xx + 1); console.log(xx / 1e18); }
Run the forge test -vvvv
, console to output:
[FAIL. Reason: UNDEFINED] testVRGDAC_time() (gas: 554525) Traces: [106719] CounterTest::setUp() ├─ [49499] → new Counter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f │ └─ ← 247 bytes of code ├─ [2390] Counter::setNumber(0) │ └─ ← () └─ ← () [554525] CounterTest::testVRGDAC_time() ├─ [517512] → new VRGDAC@0x2e234DAe75C793f67A35089C9d99245E1C58470b │ └─ ← 2578 bytes of code ├─ [3617] VRGDAC::yToX(400000000000000000000, 1000000000000000000000, 1000000000000000000) [staticcall] │ └─ ← "UNDEFINED" └─ ← "UNDEFINED"
Changing the timeSinceStart
to toDaysWadUnsafe(86400 * 365)
will work.
When this function is used in ERC20TokenEmitter#buyToken
, the timeSinceStart
is: block.timestamp-startTime
function buyTokenQuote(uint256 amount) public view returns (int spentY) { require(amount > 0, "Amount must be greater than 0"); return vrgdac.xToY({ timeSinceStart: toDaysWadUnsafe(block.timestamp - startTime), sold: emittedTokenWad, amount: int(amount) }); }
startTime
is set during the initialization of the ERC20TokenEmitter
contract:
function initialize( address _initialOwner, address _erc20Token, address _treasury, address _vrgdac, address _creatorsAddress ) external initializer { ..... startTime = block.timestamp; }
In other words, if the ERC20TokenEmitter
contract is deployed and initialized and becomes unavailable after 400 days(400 days is the test value, the actual value will be affected by other parameters), calling the buyToken
function will always revert.
vscode manual
Optimize the vrgdac.yToX function, or set a minimum timeSinceStart
value, which is used when the minimum is exceeded.
Math
#0 - c4-pre-sort
2023-12-22T07:59:49Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T08:00:11Z
raymondfam marked the issue as duplicate of #357
#2 - c4-judge
2024-01-06T00:41:10Z
MarioPoneder marked the issue as satisfactory
#3 - c4-judge
2024-01-06T00:41:19Z
MarioPoneder marked the issue as selected for report
#4 - rocketman-21
2024-01-06T21:06:49Z
Looked into this extensively, it operates under the assumption that the token emission schedule (# sold) will be way off schedule. eg: over a year, 75%+ lower than expected according to the target sale per time unit
In that case, after a prolonged amount of time, the math in the VRGDA will break due to a param reaching 0 in the available fixed point decimals. given the VRGDA pricing mechanism decreases the price, my assumption is that in a rational market we will never reach the above case, because tokens will become cheaper and cheaper. or the DAO/community will be dead and no one wants to buy tokens anymore
#5 - MarioPoneder
2024-01-06T21:10:43Z
@rocketman-21 Thanks for digging deeper into this. I would have missed this since it seems perfectly valid based on the test.
#6 - c4-judge
2024-01-06T21:11:17Z
MarioPoneder marked the issue as unsatisfactory: Invalid
#7 - rocketman-21
2024-01-08T23:21:21Z
upon second look here - I do think this is valid
I was able to reproduce the VRGDA breaking in multiple different scenarios due to precision loss in the VRGDA math
even if the supply was way off schedule, there could be scenarios imho where the community sets a large perTimeUnit and targetPrice and never meets that volume, and after 1+ years their vrgda will likely break
fix here: https://github.com/collectivexyz/revolution-protocol/commit/2bd0b35df870097be166a0d57af0a1f0d62a7518
#8 - MarioPoneder
2024-01-08T23:28:56Z
@rocketman-21
Thanks again for this insight, as far as I can reproduce the PoCs it's hard to distinguish if this can have an impact on the protocol or just arises under unrealistic assumptions.
Nevertheless, it seems like there could be realistic scenarios where this is a problem, therefore restoring severity.
#9 - c4-judge
2024-01-08T23:29:23Z
MarioPoneder removed the grade
#10 - c4-judge
2024-01-08T23:29:30Z
MarioPoneder marked the issue as satisfactory
#11 - c4-judge
2024-01-08T23:29:53Z
MarioPoneder marked the issue as selected for report
#12 - c4-sponsor
2024-01-09T15:38:14Z
rocketman-21 (sponsor) confirmed
#13 - bart1e
2024-01-10T18:41:40Z
I was able to reproduce the VRGDA breaking in multiple different scenarios due to precision loss in the VRGDA math
even if the supply was way off schedule, there could be scenarios imho where the community sets a large perTimeUnit and targetPrice and never meets that volume, and after 1+ years their vrgda will likely break
Could we ask Wardens who created this submission or its duplicate to share an example scenario where this happens? Or possibly @rocketman-21 could share such an example if he was able to find it? Because protocol being dead for 400
days without any activity whatsoever and then out of a sudden someone buying some tokens doesn't sound very realistic (as already confirmed in the previous comments). And both submissions show only this case.
Moreover, in the contest's README there was the following information:
If you create the VRGDA with shoddy parameters you can get bad outputs and errors. Will add checks on the deployer/manager contract to ensure valid params.
If I understand this correctly, we were expected to assume that VRGDA will be deployed with reasonable parameters. If the example of protocol breaking would be only when:
the community sets a large perTimeUnit and targetPrice and never meets that volume
then I think this is an example of "shoddy parameters" and it could be unfair for Wardens who assumed correct parameters (and didn't want to submit an unrealistic scenario of protocol not being touched for 400
days) to judge this as Medium, especially that the exploit scenario was in fact found by the Sponsor himself.