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: 49/110
Findings: 2
Award: $83.85
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: deepplus
Also found by: 0xDING99YA, 0xmystery, Aymen0909, DanielArmstrong, Inference, KupiaSec, SadeeqXmosh, SpicyMeatball, Tricko, adeolu, jnforja, passteque, rvierdiiev, wangxx2026, zhaojie
32.7129 USDC - $32.71
Users can buy NontransferableERC20Token by calling buyToken
function directly. At that time, the expected amount of tokens they will receive is determined based on current supply and their paying ether amount. But, due to some transactions(such as settleAuction or another user's buyToken) which is running in front of caller's transaction, they can get less token than they expected.
The VRGDAC always exponentially increase the price of tokens if the supply is ahead of schedule. Therefore, if another transaction of buying token is frontrun against a user's buying token transaction, the token price can arise than expected.
For instance, let's assume that ERC20TokenEmitter is initialized with following params:
To avoid complexity, we will assume that the supply of token so far is consistent with the schedule. When alice tries to buy token with 5 ether
, expected amount is calculated by getTokenQuoteForEther(5 ether)
and the value is about 4.87 ether
.
However, if Bob's transaction to buy tokens with 10 ether
is executed before Alice, the real amount which Alice will receive is about 4.43 ether
.
You can check result through following test:
function testBuyTokenWithoutSlippageCheck() public { address alice = makeAddr("Alice"); address bob = makeAddr("Bob"); vm.deal(address(alice), 100000 ether); vm.deal(address(bob), 100000 ether); address[] memory recipients = new address[](1); recipients[0] = address(1); uint256[] memory bps = new uint256[](1); bps[0] = 10_000; // expected amount of minting token when alice calls buyToken int256 expectedAmount = erc20TokenEmitter.getTokenQuoteForEther(5 ether); vm.startPrank(bob); // assume that bob calls buy token with 10 ether erc20TokenEmitter.buyToken{ value: 10 ether }( recipients, bps, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); vm.stopPrank(); vm.startPrank(alice); // calculate the amount of tokens which alice will actually receive int256 realAmount = erc20TokenEmitter.getTokenQuoteForEther(5 ether); vm.stopPrank(); emit log_string("Expected Amount: "); emit log_int(expectedAmount); emit log_string("Real Amount: "); emit log_int(realAmount); assertLt(realAmount, expectedAmount, "Alice should receive less than expected if Bob frontrun buyToken"); }
Therefore, Alice will get about 0.44 ether
less tokens than expected since there is no any checking of slippage in buyToken
function.
VS Code
Add slippage checking to buyToken
function. This slippage checking should be executed only when the user calls buyToken
function directly. In other words, it should not be executed when settleAuction calls buyToken
function.
Other
#0 - c4-pre-sort
2023-12-22T16:15:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T16:15:47Z
raymondfam marked the issue as duplicate of #26
#2 - c4-pre-sort
2023-12-24T06:00:11Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-12-24T06:00:15Z
raymondfam marked the issue as primary issue
#4 - rocketman-21
2024-01-04T23:23:23Z
this is intended and a consequence of how the VRGDA functions, when people buy tokens the price goes up if it is ahead of schedule
not ideal UX, but not going to fix for now
#5 - c4-sponsor
2024-01-04T23:23:27Z
rocketman-21 (sponsor) acknowledged
#6 - MarioPoneder
2024-01-06T16:27:33Z
Even though the increasing price is intended, it's state of the art to introduce a slippage parameter to protect users from receiving less than expected. Therefore, maintaining Medium
severity seems appropriate.
#7 - c4-judge
2024-01-06T16:27:39Z
MarioPoneder marked the issue as satisfactory
#8 - c4-judge
2024-01-06T16:32:12Z
MarioPoneder marked the issue as selected for report
🌟 Selected for report: osmanozdemir1
Also found by: 0xDING99YA, BARW, Brenzee, DanielArmstrong, KupiaSec, SpicyMeatball, bart1e, deepplus, haxatron, rouhsamad, rvierdiiev
51.1381 USDC - $51.14
The amounts of token for buyers and creators isn't accurately calculated.
ERC20TokenEmitter enables anyone to purchase the ERC20 governance token at any time through buyToken
function. The buyToken
function sends a portion of the paid ETH to protocolRewards. And then calculates the amount of tokens to be distributed to buyers and creators at a certain ratio based on the remaining ETH. The buyToken
function calculates the amount of ETH according to the specified ratio and then calls the getTokenQuoteForEther
function to calculate the each amount of tokens to be distributed to buyers and creators, respectively.
//Tokens to emit to creators int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) : int(0);
// Tokens to emit to buyers int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0);
However, since the buyToken
function doesn't accurately update emittedTokenWad
state(total amount of emitted token), which is used for calculating the token issuance amount in getTokenQuoteForEther
function, the calculated value is different from the amount that should actually be minted.
//Transfer ETH to treasury and update emitted emittedTokenWad += totalTokensForBuyers; if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators;
As you can see above code, it updates emittedTokenWad
state after calculating token amounts for both of buyers and creators. This cause incorrect calculation of minting token. Let me explain with example.
let's assume that ERC20TokenEmitter is initialized with following params:
Alice is trying to call buyToken
function with over 5 ether
by passing her address as addresses
param, and 10_000(100%) as basisPointSplits
param. Assume that remaining ETH value is 5 ether
after sending a portion of msg.value
to protocolRewards.
Then, the amount of ETH which will be used to mint for buyers is 4 ether
:
5 * (10000 - 2000) / 1000 = 4 (ether)
uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000;
And, the amount of ETH which will be used to mint for creators is 0.5 ether
:
(5 - 4) * 5000 / 10000 = 0.5 (ether)
(5 - 4) - 0.5 = 0.5 (ether)
//Ether directly sent to creators uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000; //Tokens to emit to creators int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) : int(0);
And total amount of minting token is getTokenQuoteForEther(4 ether) + getTokenQuoteForEther(0.5 ether)
. But buyTokenAmount
isn't update the emittedTokenWad
between calculating token amount for crators and buyers. This causes wrong calculating price of token based on paid ETH for buyers because VRGDA's calculation is based on total supply and paid ETH amount. As a result, more ERC20 token is minted than expected.
You can check result through the following test:
function testComparingTokenAmountWithUpdatingEmittedAmount() public { address alice = makeAddr("Alice"); vm.deal(address(alice), 100000 ether); vm.startPrank(alice); address[] memory recipients = new address[](1); recipients[0] = address(1); uint256[] memory bps = new uint256[](1); bps[0] = 10_000; // ensure that enough volume was bought for the day, so purchase expectedVolume amount first erc20TokenEmitter.buyToken{ value: expectedVolume }( recipients, bps, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); // first, calculate the actual amount of tokens for buyers and creators. int256 for_buyer_1 = erc20TokenEmitter.getTokenQuoteForEther(4 ether); int256 for_creators_1 = erc20TokenEmitter.getTokenQuoteForEther(0.5 ether); // first, calculate the amount of tokens for buyers and creators with updating emittedTokenWad btw them. int256 for_buyer_2 = erc20TokenEmitter.getTokenQuoteForEther(4 ether); erc20TokenEmitter.setEmittedTokenWad(erc20TokenEmitter.emittedTokenWad() + for_buyer_2); int256 for_creators_2 = erc20TokenEmitter.getTokenQuoteForEther(0.5 ether); emit log_string("Current Calc: "); emit log_int(for_buyer_1 + for_creators_1); emit log_string("Calc with updating emittedTokenWad btw them: "); emit log_int(for_buyer_2 + for_creators_2); // Assert that the new token amount is less than the previous tokenAmount assertGt(for_buyer_1 + for_creators_1, for_buyer_2 + for_creators_2, ""); vm.stopPrank(); }
The result is:
[PASS] testComparingTokenAmountWithUpdatingEmittedAmount() (gas: 360782) Logs: Current Calc: 4012115812150412050 Calc with updating emittedTokenWad btw them: 3995502668080304120
As you can see, more ERC20 token is minted.
VSCode
Move line 188 to line 182:
uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000; //Tokens to emit to creators int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) : int(0); + if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators; // Tokens to emit to buyers int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0); //Transfer ETH to treasury and update emitted emittedTokenWad += totalTokensForBuyers; - if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators;
Other
#0 - c4-pre-sort
2023-12-22T17:32:18Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T17:32:28Z
raymondfam marked the issue as duplicate of #194
#2 - c4-sponsor
2023-12-27T20:00:33Z
rocketman-21 (sponsor) confirmed
#3 - c4-judge
2024-01-06T14:01:45Z
MarioPoneder marked the issue as satisfactory