Fee Refund Bug: Discounted Token Purchases Explained
Summary
The buy
function in this contract has a critical flaw: it incorrectly refunds the 1% fee intended for the contract. This allows users to purchase tokens at a discount, effectively subsidizing their purchase with the contract's own balance. This issue can lead to a significant drain on the contract's funds and disrupt the intended tokenomics.
Finding Description
Here's the core of the problem, guys: the buy
function is designed to take a 1% fee from every Ether sent by users, use that fee to buy tokens, and then forward the fee to a factory address. Sounds good, right? But here's the kicker: the way the refund is calculated, it returns that fee back to the user. This means that instead of paying the full price for the tokens (including the 1% fee), users are only effectively paying 99% of the intended amount. The contract ends up footing the bill for that missing 1%, draining its balance with every transaction. This completely throws off the economic model of the token sale, giving users a sneaky discount and costing the contract valuable Ether.
This isn't just a minor inconvenience; it's a fundamental flaw that undermines the entire token sale. Every single buy
call is affected, turning the intended fee structure on its head. Instead of the contract benefiting from the fees, it's essentially losing money on each transaction. This is a serious violation of the token sales' economic integrity and could have significant consequences for the long-term viability of the project. Imagine running a store where you accidentally give every customer a 1% discount – it wouldn't take long to eat into your profits!
To break it down further, the issue lies in the refund calculation itself. The function subtracts the Ether value of the purchased tokens from the full amount of Ether sent by the user. However, the Ether value of the tokens is calculated based on the sent amount minus the fee. This creates a circular situation where the fee is effectively being deducted twice: once in the initial calculation and again in the refund. This double deduction results in the user receiving the 1% fee back, while the contract has to cover that fee from its existing balance. It's like a mathematical error that's actively costing the contract money.
The consequences of this flaw are far-reaching. Not only does it provide an unfair advantage to token buyers, but it also puts the contract's Ether reserves at risk. As users continue to exploit this unintended discount, the contract's balance will dwindle, potentially jeopardizing the ability to complete the token sale or transition to Uniswap as planned. This is a critical issue that needs to be addressed immediately to prevent further financial losses and ensure the long-term health of the project. We're talking about a real risk of depleting the contract's funds, which is never a good sign in the world of DeFi!
Root Cause
The root cause boils down to a faulty calculation in the refund mechanism. The refund logic uses the full sent Ether (msg.value
) minus the Ether value of the tokens purchased. The problem is that the Ether value of the tokens is calculated based on the sent amount after deducting the fee. This circular dependency leads to the fee being refunded to the user, effectively making the contract subsidize the fee from its own balance.
Attack Path
Here's how an attacker could exploit this vulnerability, step by step:
- The attacker sends Ether to the
buy
function. Let's say they sendmsg.value = 1_000_000_000_000_000_000 wei
(1 Ether) to keep it simple. - The contract calculates the 1% fee:
(1_000_000_000_000_000_000 * 10) / 1000 = 10_000_000_000_000_000 wei
(0.01 Ether). This fee is sent to the factory address. - The amount of Ether used to buy tokens (
buyAmount
) is calculated:1 - 0.01 = 0.99 Ether
. - The contract calculates the number of tokens the user should receive based on the
buyAmount
. Let's assume the current price is such that(990_000_000_000_000_000 / 7692307691) * 1e18 ≈ 128_700_389_000_000_000_000_000 tokens
. - Now, here's the crucial part: the refund calculation. The contract calculates the Ether value of the received tokens using a function called
getEthQoute
. In our example,getEthQoute(128_700_389_000_000_000_000_000) ≈ 0.99 Ether
. So, the refund is calculated asrefund = 1 - 0.99 = 0.01 Ether
. - The attacker receives the tokens (worth 0.99 Ether) plus the 0.01 Ether refund, effectively paying only 0.99 Ether for tokens worth the full 1 Ether.
- The contract keeps 0.99 Ether (the original amount minus the refund), but it has given away tokens worth 1 Ether. This means the contract loses 0.01 Ether on this transaction.
So, the attacker gets a sweet discount, and the contract eats the cost. Imagine repeating this process thousands of times – the contract's balance would be drained pretty quickly!
This attack path is straightforward and can be easily automated. An attacker could write a simple script to repeatedly call the buy
function, taking advantage of the fee refund and gradually depleting the contract's Ether reserves. This is a serious concern that highlights the severity of this vulnerability.
Impact Explanation
This issue has a Medium impact. Here's why:
- Users get a discount: Users are essentially buying tokens at a 1% discount, which isn't the end of the world, but it's certainly not the intended behavior.
- Contract loses value: The contract loses 0.01 Ether in value for every transaction. This might seem small at first, but over time, it can add up and significantly deplete the contract's Ether reserves.
While this issue doesn't allow for direct theft of funds, the consistent drain on the contract's balance can have serious consequences. Over many transactions, the contract's Ether reserves could be depleted, potentially disrupting the token sale or the planned transition to Uniswap. Think of it like a slow leak in a tire – it might not cause a flat immediately, but eventually, it will leave you stranded.
The primary concern is the erosion of the contract's financial stability. If the contract runs out of Ether, it may not be able to fulfill its intended purpose, whether that's funding future development, providing liquidity on Uniswap, or other crucial operations. This makes it a Medium impact issue because while it doesn't result in immediate and catastrophic loss, it undermines the long-term health and viability of the project.
Likelihood Explanation
This vulnerability is highly likely to be exploited. Here's the deal: this isn't some obscure bug that requires special conditions to trigger. It happens in every single buy
transaction. There's no way to avoid it, and it doesn't require any complex manipulation or insider knowledge. All a user has to do is call the buy
function, and they'll automatically get the 1% discount.
Even non-malicious users will inadvertently benefit from this discount. They might not even realize it's happening, but they'll still be getting tokens for slightly less than they should be. And of course, a malicious actor could easily take advantage of this. They could write a script to repeatedly call the buy
function, maximizing their discount and draining the contract's Ether reserves as quickly as possible. This is a classic case of a low-hanging fruit – an easily exploitable vulnerability that's just waiting to be picked.
The likelihood of exploitation is further increased by the fact that there are no built-in safeguards to prevent it. There are no rate limits, no checks on the amount of Ether being sent, and no mechanisms to detect or prevent this type of attack. This means that an attacker can repeat the attack as many times as they want, until either the token supply or the contract's Ether balance is exhausted. This combination of easy exploitation and lack of preventative measures makes this vulnerability a ticking time bomb.
Proof of Concept
Let's walk through a proof-of-concept scenario to really drive home the impact of this vulnerability:
- Setup:
- Contract balance: 10 Ether
initialPrice = 7692307691 wei
(this is an example price, the actual value might vary)total_Trade_Volume = 200_000_000 * 1e18
tokens (the total number of tokens available for sale)tokensSoldSoFar = 0
(initially, no tokens have been sold)- A user sends
msg.value = 1 Ether
to thebuy
function and setsminExpected = 0
(meaning they're not expecting a specific minimum amount of tokens).
- Execution:
- The contract receives 1 Ether, increasing its balance to 11 Ether.
- The 1% fee is calculated:
(1_000_000_000_000_000_000 * 10) / 1000 = 0.01 Ether
. This fee is sent to the factory address, reducing the contract's balance to 10.99 Ether. - The
buyAmount
(the amount of Ether used to buy tokens) is calculated:0.99 Ether
. - The contract calculates the number of tokens the user should receive:
tokenAmount = (990_000_000_000_000_000 / 7692307691) * 1e18 ≈ 128_700_389_000_000_000_000_000 tokens
. - The contract performs some checks (e.g., ensuring the
minExpected
value is met and that the user isn't trying to buy more tokens than they're allowed). These checks pass in this scenario. - The contract transfers approximately 128.7 million tokens to the user.
- Now, the crucial refund calculation:
getEthQoute(128_700_389_000_000_000_000_000) ≈ 0.99 Ether
. The refund is calculated asrefund = 1 - 0.99 = 0.01 Ether
. - The 0.01 Ether refund is sent back to the user, further reducing the contract's balance to 10.98 Ether.
- Outcome:
- User: Sends 1 Ether, receives tokens worth 0.99 Ether and a 0.01 Ether refund, effectively paying only 0.99 Ether for tokens worth the full 1 Ether.
- Contract: Gains 0.99 Ether in sent value, loses tokens worth 1 Ether, and pays 0.01 Ether in refund, resulting in a net loss of 0.01 Ether per transaction.
- The Drain: If this process is repeated approximately 1555 times (until all 200 million tokens are sold), the contract will lose approximately 15.55 Ether. This is significantly more than the initial balance of 10 Ether, demonstrating how quickly this vulnerability can deplete the contract's funds.
This PoC clearly illustrates how the unintended fee refund leads to a loss of Ether for the contract with each transaction. By repeatedly exploiting this vulnerability, an attacker can quickly drain the contract's balance, potentially crippling its ability to operate as intended. It's a real problem that needs a real solution, and fast!
Recommendation
The best way to fix this, guys, is to adjust the refund calculation. Instead of using the full msg.value
, we should use the buyAmount
(the amount of Ether actually used to purchase tokens) in the refund calculation. Here's the corrected code:
if(buyAmount > getEthQoute(tokenAmount)){
uint256 refund = buyAmount - getEthQoute(tokenAmount);
(bool success,) = msg.sender.call{value: refund}("");
require(success, "Refund Failed");
}
By using buyAmount
in the calculation, we ensure that the refund only reflects the difference between the Ether spent on tokens and the value of those tokens. This eliminates the unintended fee refund and prevents the contract from subsidizing the 1% fee. The rounding error will be minimal, preventing the contract from losing funds. This simple change will protect the contract's Ether reserves and ensure the intended tokenomics are maintained. It's a small fix with a huge impact, so let's get it done!