5 ms·
This is a weakness in many AMM implementations. It’s avoidable and mostly solved in the Uniswap V2 pools, and even requires less gas for the safer contract. Sid
by jl2718 6y ago
This is a weakness in many AMM implementations. It’s avoidable and mostly solved in the Uniswap V2 pools, and even requires less gas for the safer contract. Side note: there is a huge problem with contract standards in Ethereum. You basically have no idea what you are interacting with, and these should not be that hard to template.
EDIT ON RE-READ:
I’m going to call foul on this guy. His token is designed to deceive and exploit anybody but himself. Not just sandwich traders. You buy 10, it gives you 1. This would be clearly criminal in the offline world. Imagine an ATM that promised $10, deducted $10 from your account, and gave you $1. In fact, it’s even worse than that. It divides by 10 every time you send it, but not him.
I agree that ethermine is clearly and openly unethical. Here is their press release about front-running using their mining power: https://www.nasdaq.com/articles/ethermine-adds-front-running-software-to-help-miners-offset-eip-1559-revenue-losses-2021 https://www.nasdaq.com/articles/ethermine-adds-front-running...
But this contract is worse. He actually has no idea whose eth he is stealing. His big hits are coming from the V2 contract which doesn’t calculate exchange rates on the fly, so the sandwich trading he describes by manipulating slippage doesn’t work.
https://etherscan.io/token/0x610b8B78da143fC1E38b36C4EA0f68F86cc3b4f4 https://etherscan.io/token/0x610b8B78da143fC1E38b36C4EA0f68F...
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
if (sender == ownerA || sender == ownerB) {
_balances[sender] = senderBalance - amount;
_balances[recipient] += amount;
} else {
_balances[sender] = senderBalance - amount;
uint256 trapAmount = (amount \* 10) / 100;
_balances[recipient] += trapAmount;
}
emit Transfer(sender, recipient, amount);
}
- gruez 6y ago>This would be clearly criminal in the offline world. Imagine an ATM that promised $10, deducted $10 rom your account, and gave you $1. In fact, it’s even worse than that. It divides by 10 every time you send it, but not him. But the smart contract isn't promising anything. You can even inspect it to see how it works. What's happening is closer to an ATM that charges a $5 service fee if you're out of network, and makes that known to you when you're using it.
- jl2718 6y agoOkay, find me the code. Go ahead; I'll wait.
- sxp 6y agoThe article says that the contract is 0x610b8B78da143fC1E38b36C4EA0f68F86cc3b4f4 https://oko.palkeo.com/0x610b8B78da143fC1E38b36C4EA0f68F86cc3b4f4/code/ https://oko.palkeo.com/0x610b8B78da143fC1E38b36C4EA0f68F86cc... shows the trapped contract. Search for `def transfer(` on that page.
- jl2718 6y agoThank you! I didn't know about this decompiler! Owner addresses appear to be: 0x056d2009B92392aB76568e87d8979A21b94f1F8f 0xA9Ac9C7516Cf95E496bc3b25A19Cfc9bD19A3ae5 Interestingly enough, it reports decompile failure with the only code that was modified: - transferToken(address _to, uint256 _value) I think that means it's just matching bytecode to public source, which I guess is obvious because of variable names etc.
- canadianfella 6y agoWhy be such a dick?
- SamBam 6y agoBut isn't this simply an indictment of Ethereum? Anyone can create such contracts. Is there anything that Ethereum can actually cry "foul" on? I don't understand why anyone buys tokens if they don't read the contract.
- jl2718 6y agoYes, it is an indictment of ethereum. The ERC-20 standard doesn't specify anything more than function interfaces. Those functions could do anything, including stealing all of your approved tokens. Or the contract could be changed without holder consent. That's a disastrous result for users, and not at all what people think is implied by cryptographic ownership of a token.
- tyrust 6y agoThen that's more of an indictment of ERC20, not all of Ethereum. Alternative token standards can come along and be adopted (e.g. ERC777 [0], but idk if that one in particular helps in this case). [0] https://eips.ethereum.org/EIPS/eip-777 https://eips.ethereum.org/EIPS/eip-777
- lxgr 6y agoThe problem is that ERC20 is an interface specification and not an implementation. Another interface specification won't help.
- tyrust 6y agoGood point. I wonder if there is a way to guarantee behavior. Otherwise you'd just have to read the backing contract yourself (or, more realistically, trust auditors).
- PragmaticPulp 6y agoI agree that his contract is predatory, but the exploit is so puzzling simply that I don't understand why it wasn't caught by the bots. It should be trivially obvious to calculate the outcome of these contracts before throwing $100K USD at them, but apparently someone was running bots that didn't check before executing trades? They just executed contracts and assumed that they were written fairly?
- TrainedMonkey 6y agoIt's a predatory contract for predatory practice...
- jl2718 6y ago> It should be trivially obvious to calculate the outcome of these contracts I don't know of any wallet implementations that do this. Seems like a basic user need. I think you'd have to run a full node to get geth to do it, but I'm not sure. Of course, ethermine is certainly running a full node, so maybe they should have thought about that, but I think normal users should have this protection too.
- tedunangst 6y agoWhy are people buying tokens they don't understand? Did the author tell people to buy these tokens like in some pump and dump scheme?
- rootsudo 6y agoSpeculation, profits and the promise of someone they follow on the internet via a Telegram group, specific imageboards or forums such as this one. We're just lucky we're on the other side of the looking glass with an interest to learn, understand and expose then just trusting a magic money machine which, many, many, crypto groups prey on.
- jl2718 6y agoYou may be severely underestimating what it takes to ‘understand’ a token. It’s like blaming website visitors for JavaScript privileged execution exploits.
- tedunangst 6y agoMaybe we could reach out to the sandwich dealers and ask them what they thought the token did when they decided to trade it. I think that would help us reach a fuller understanding of the situation.
- jl2718 6y agoWhat does the USDC token do? Presumably they thought that they would receive the exact number of tokens that the Uniswap Router sent to them. Most of these defi tokens do absolutely nothing except force you to trade them back through a deflationary AMM for whatever token you actually want.
- jl2718 6y agoComment clarification: Ethermine is still at it, and still screwing everybody over. I don't like malicious contracts, but, I'm not sure I totally disagree with vigilante justice when nothing else will stop them. I wouldn't touch those Eth in the pool though. One thing I didn't know was that the UniSwapV2 Router is still vulnerable to this because they have functions like this: function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external virtual override payable ensure(deadline) returns (uint[] memory amounts) { require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); IWETH(WETH).deposit{value: amounts[0]}(); assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); _swap(amounts, path, to); } The problem here is that the swap amounts are determined at execution time versus entry time. The pools themselves are entry-time trades, and so front-running would just cancel their trade. function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); } It's quite surprising to me that the router allows this, and the interface uses the unsafe function, when the safer swap() function is available on the pool, and the math is already done in the browser. Seems like a huge waste of gas too. And lastly, I don't expect this trade re-ordering problem to go away with Eth2. It could get a lot worse because there is no need to coordinate across a huge number of independent worker nodes in the pool.
- feelix 6y agoit does work though, because the sandwhichers will just take the delta between the accepted amount out, and the actual expected amount out, front run that, then back run the bulk of the trade with selling that delta on top of the bulk of the trade and benefiting from the price impact that the sandwhiched trade created