Platypusdefi借贷逻辑漏洞分析

说明

本次的Platypus漏洞是一个很典型的借贷合约中的典型问题。借贷合约没有考虑到当前用户的借贷状态,导致用户在已经借贷的情况下,可以直接将之前的抵押取出来,这样就凭空地获得了多余的代币信息。

基本信息

  • Attacker: 0xeff003d64046a6f521ba31f39405cb720e953958
  • Attack Contract:
    • 0x67afdd6489d40a01dae65f709367e1b1d18a5322
  • Vulnerable Contract:
    • 0xff6934aac9c94e1c39358d4fdcf70aeca77d0ab0
    • 0xc007f27b757a782c833c568f5851ae1dfe0e6ec7
  • Chain:Avalance
  • Attack Tx
    • 0x1266a937c2ccd970e5d7929021eed3ec593a95c68a99b4920c2efa226679b430
  • Lost:8.5M

攻击分析

flashloan

攻击者通过闪电贷的方式,贷出44000000000000个USDC。

deposit

攻击者将贷出的所有的USDC,全部保存到 Platypus Finance: Pool 中。

攻击者通过质押了所有借贷的USDC,最终获得了44000100592104的LPUSDC。

攻击者然后又将自己所有获得的LPUSDC,全部质押到MasterPlatypusV4中。

borrow

攻击者通过positionView计算获得相关的借贷参数。如下所示:

image-20230403215845685

最终确定可以借贷出来的金额是41794533641783253909672000

攻击者又通过 PlatypusTreasure合约的borrow函数进行借贷, PlatypusTreasure 的借贷额度最高为质押的95%,借出了41794533641783253909672000个USP。

emergencyWithdraw

攻击者然后调用emergencyWithdraw,中文翻译就是紧急提款的意思。最终攻击者提取的LPUSDC的数量是44000100592104

这个数量就是攻击者开始抵押到 Platypus Finance: Pool 中的。

分析提款的核心代码:

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
/// @notice Withdraw without caring about rewards. EMERGENCY ONLY.
/// @param _pid the pool id
function emergencyWithdraw(uint256 _pid) public nonReentrant {
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][msg.sender];

if (address(platypusTreasure) != address(0x00)) {
(bool isSolvent, ) = platypusTreasure.isSolvent(msg.sender, address(poolInfo[_pid].lpToken), true);
require(isSolvent, 'remaining amount exceeds collateral factor');
}

// reset rewarder before we update lpSupply and sumOfFactors
IBoostedMultiRewarder rewarder = pool.rewarder;
if (address(rewarder) != address(0)) {
rewarder.onPtpReward(msg.sender, user.amount, 0, user.factor, 0);
}

// SafeERC20 is not needed as Asset will revert if transfer fails
pool.lpToken.transfer(address(msg.sender), user.amount);

// update non-dialuting factor
pool.sumOfFactors -= user.factor;

user.amount = 0;
user.factor = 0;
user.rewardDebt = 0;

emit EmergencyWithdraw(msg.sender, _pid, user.amount);
}

其中关键的代码是需要判断用户的抵押头寸是否有偿付能力的功能,即platypusTreasure.isSolvent

前面调用时传递的参数是:platypusTreasure.isSolvent(msg.sender, address(poolInfo[_pid].lpToken), true);

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
/**
* @notice function to check if user's collateral position is solvent
* @dev returns (true, 0) if the token is not a valid collateral
* @param _user address of the user
* @param _token address of the token
* @param _open open a position or close a position
* @return solvent
* @return debtAmount total debt amount including interests
*/
function isSolvent(
address _user,
ERC20 _token,
bool _open
) external view returns (bool solvent, uint256 debtAmount) {
return _isSolvent(_user, _token, _open);
}


/**
* @notice function to check if user's collateral position is solvent
* @dev returns (true, 0) if the token is not a valid collateral
* @param _user address of the user
* @param _token address of the token
* @param _open open a position or close a position
* @return solvent
* @return debtAmount total debt amount including interests
*/
function _isSolvent(
address _user,
ERC20 _token,
bool _open
) internal view returns (bool solvent, uint256 debtAmount) {
uint256 debtShare = userPositions[_token][_user].debtShare;

// fast path
if (debtShare == 0) return (true, 0);

// totalDebtShare > 0 as debtShare is non-zero
debtAmount = (debtShare * (totalDebtAmount + _interestSinceLastAccrue())) / totalDebtShare;
solvent = debtAmount <= (_open ? _borrowLimitUSP(_user, _token) : _liquidateLimitUSP(_user, _token));
}

因为debtAmount 表示的是借贷的数量,_borrowLimitUSP表示的是借贷的限额。借贷数量是小于借贷限额的。所以solvent恒为True。

所以攻击者就可以提取所有的借贷数量,即44000100592104 的LPUSDC。

到这一步,攻击者不仅将之前抵押在MasterPlatypusV4中的所有的LPUSDC全部取出来了,同时还通过borrow的方式额外借贷了41794533641783253909672000个USP。

withdraw

攻击者通过获得了44000100592104 的LPUSDC之后,又重新调用Platypus Finance: Pool:withdraw,用于获得抵押的USDC。

最终,攻击者通过调用withdraw方法移除了流动性,最终获得了43999999921036 数量的USDC。

swap

攻击者将自己获得的所有的41794533641783253909672000个USP全部swap成为各种稳定币。

攻击者通过swap操作将总结获得的USP全部兑换成为稳定币,作为自己的盈利。

FlashLoan Back

由于最开始攻击者通过aave3的闪电贷的方式获取了44000000000000个USDC,最终需要进行归还。

归还的数量是44022000000000

获利

折算成为稳定币的价值是:8.5M 做有数量的价值。

总结

这个漏洞本质上是一个逻辑漏洞,因为合约在函数emergencyWithdraw()中仅仅只是考虑了用户是否能够足额借出代币,但是没有考虑到当时用户的状态。这样就导致攻击者在借贷之后,还可以通过emergencyWithdraw()的方式获得额外的USP代币。

对于借贷合约来说,这一点要有尤为注意,合约在进行抵押和借贷时,要对借贷的偿还状态以及借贷金额做严格的检查。

参考

https://learnblockchain.cn/article/5412

https://mp.weixin.qq.com/s/3hx8DXZcHo5r0fa4E0x45g

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2023/04/04/Platypusdefi-vuln/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow