My Coding Journey: From Finance to Smart Contract Auditor

Day 70

Code

pragma solidity ^0.4.21;

contract FiftyYearsChallenge {
    struct Contribution {
        uint256 amount;
        uint256 unlockTimestamp;
    }
    Contribution[] queue;
    uint256 head;

    address owner;
    function FiftyYearsChallenge(address player) public payable {
        require(msg.value == 1 ether);

        owner = player;
        queue.push(Contribution(msg.value, now + 50 years));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function upsert(uint256 index, uint256 timestamp) public payable {
        require(msg.sender == owner);

        if (index >= head && index < queue.length) {
            // Update existing contribution amount without updating timestamp.
            Contribution storage contribution = queue[index];
            contribution.amount += msg.value;
        } else {
            // Append a new contribution. Require that each contribution unlock
            // at least 1 day after the previous one.
            require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

            contribution.amount = msg.value;
            contribution.unlockTimestamp = timestamp;
            queue.push(contribution);
        }
    }

    function withdraw(uint256 index) public {
        require(msg.sender == owner);
        require(now >= queue[index].unlockTimestamp);

        // Withdraw this and any earlier contributions.
        uint256 total = 0;
        for (uint256 i = head; i <= index; i++) {
            total += queue[i].amount;

            // Reclaim storage.
            delete queue[i];
        }

        // Move the head of the queue forward so we don't have to loop over
        // already-withdrawn contributions.
        head = index + 1;

        msg.sender.transfer(total);
    }
}

Breakdown

  • Contribution is a structure that contains an amount and a timestamp required when unlocking after the passage of 50 years – i.e. a lockup contract storing a withdrawal queue;
  • It is worth noting that contribution is a dynamic array that takes the queue;
  • FiftyYearsChallenge imposes an initial requirement that the msg.sender has exactly one ether and the player has contributed and must wait for 50 years to access the fund;
  • upsert takes the uint256 index and a timestamp, requiring the msg.sender to be the owner. If the index is greater than the head and the index is less than the queue length (remember the contribution above?), then it updates the existing contribution amount without updating timestamp; and
  • The way the withdraw function embeds the 50 year wait feature is by processing all matured withdrawals (evidenced by require(now >= queue[index].unlockTimestamp) by iterating from the index stored in the head storage variable up to the index value passed as an argument ( (uint256 i = head; i <= index; i++)).

Problem
The challenge is complete once the address of the contract is completely drained of funds prior to the 50 years.

Solution
The else branch of the upsert function uses an uninitialized storage pointer again as in the Donation challenge. Again, an issue touched on when we discussed proxies.
The storage issue stems from the following:

  1. contribution.amount = msg.value writes to storage slot 0, where queue.length is stored; and
  2. timestamp is being written to the head variable (slot 1).
    It wasn’t immediately obvious to me but when queue.push is called (i.e. to add a new contribution), the queue’s length is first incremented then the queue entry is copied. This presents an issue because the queue points to storage slot 0 and 1. Provided that storage slot 1, the queue’s length, has been incremented, the queue entry’s amount is actually msg.value + 1, not contribution.amount = msg.value. To exploit this, as alluded to above we can call the withdraw function with an index where the corresponding queue item has an unlockTimestamp in the past.
    This is achieved by three steps:
  3. create a new queue entry that calls upsert and choose the timestamp value such that it would overflow queue[queue.length - 1].unlockTimestamp + 1 days in a way to equal zero;
  4. call the upsert a second time given overflow will occur (i.e. head uint256 will be equal to 0, meaning timestamp = 0); and
  5. withdraw wei after force sending two wei to be entered into the contract (use the script outlined in day 64 changing the value to 2.

Day 71

Code

pragma solidity ^0.4.21;

interface ITokenReceiver {
    function tokenFallback(address from, uint256 value, bytes data) external;
}

contract SimpleERC223Token {
    // Track how many tokens are owned by each address.
    mapping (address => uint256) public balanceOf;

    string public name = "Simple ERC223 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);

    event Transfer(address indexed from, address indexed to, uint256 value);

    function SimpleERC223Token() public {
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function isContract(address _addr) private view returns (bool is_contract) {
        uint length;
        assembly {
            //retrieve the size of the code on target address, this needs assembly
            length := extcodesize(_addr)
        }
        return length > 0;
    }

    function transfer(address to, uint256 value) public returns (bool success) {
        bytes memory empty;
        return transfer(to, value, empty);
    }

    function transfer(address to, uint256 value, bytes data) public returns (bool) {
        require(balanceOf[msg.sender] >= value);

        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);

        if (isContract(to)) {
            ITokenReceiver(to).tokenFallback(msg.sender, value, data);
        }
        return true;
    }

    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => mapping(address => uint256)) public allowance;

    function approve(address spender, uint256 value)
        public
        returns (bool success)
    {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value)
        public
        returns (bool success)
    {
        require(value <= balanceOf[from]);
        require(value <= allowance[from][msg.sender]);

        balanceOf[from] -= value;
        balanceOf[to] += value;
        allowance[from][msg.sender] -= value;
        emit Transfer(from, to, value);
        return true;
    }
}

contract TokenBankChallenge {
    SimpleERC223Token public token;
    mapping(address => uint256) public balanceOf;

    function TokenBankChallenge(address player) public {
        token = new SimpleERC223Token();

        // Divide up the 1,000,000 tokens, which are all initially assigned to
        // the token contract's creator (this contract).
        balanceOf[msg.sender] = 500000 * 10**18;  // half for me
        balanceOf[player] = 500000 * 10**18;      // half for you
    }

    function isComplete() public view returns (bool) {
        return token.balanceOf(this) == 0;
    }

    function tokenFallback(address from, uint256 value, bytes) public {
        require(msg.sender == address(token));
        require(balanceOf[from] + value >= balanceOf[from]);

        balanceOf[from] += value;
    }

    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] >= amount);

        require(token.transfer(msg.sender, amount));
        balanceOf[msg.sender] -= amount;
    }
}

Breakdown

  • A quick note that the ERC-223 token is used as the bank’s custom token as opposed to the usual ERC-20. The difference between the two is the ERC-223 gives a notification to the recipient of a transfer by calling the tokenFallback function.
  • I won’t insult anyone’s intelligence by breaking the code down provided it sets the supply and of a custom token (alongside the ability to transfer it) and the actual challenge merely splits the total supply of the currency between users.

Problem
Simply, if you can empty the bank provided there is an exploit with the transfer functionality or implementation of a custom token.

Solution
The key callout is in the withdraw function:

    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] >= amount);

        require(token.transfer(msg.sender, amount));
        balanceOf[msg.sender] -= amount;
    }
}

After the contract checks that the balance of msg.sender is greater than the amount, the balance is updated after calling token.transfer. The implication of this is it allows us to repeatedly withdraw our funds each time.
In step format:

  1. deposit an amount;
  2. withdraw the amount;
  3. repeat until emptied.

Day 72

Code

pragma solidity ^0.4.21;

contract MappingChallenge {
    bool public isComplete;
    uint256[] map;

    function set(uint256 key, uint256 value) public {
        // Expand dynamic array as needed
        if (map.length <= key) {
            map.length = key + 1;
        }

        map[key] = value;
    }

    function get(uint256 key) public view returns (uint256) {
        return map[key];
    }
}

Breakdown

  • set holds a uint256 key and value and expands the map dynamic array as required; and
  • get contains the key and if called by the public, returns the value of the key.

Problem
Set the isComplete variable to true.

Solution
isComplete is contained at slot 1 and the uint256 map[] is defined. This is an issue because for a dynamic array, this should come before this state variable provided the array data will be located sequentially at the address keccak256(p). Put simply, we need to expand the map’s length to cover all 2^256 - 1 storage slots. Then we set the map of the computed index overwriting the first storage slot. This means isComplete becomes true provided it is overridden.

Day 73

As promised, I thought I would outline some of the EVM puzzles I had managed to solve to ensure that I can articulate a better understanding of inline assembly / opcodes.

Puzzle 5

00      34          CALLVALUE
01      80          DUP1
02      02          MUL
03      610100      PUSH2 0100
06      14          EQ
07      600C        PUSH1 0C
09      57          JUMPI
0A      FD          REVERT
0B      FD          REVERT
0C      5B          JUMPDEST
0D      00          STOP
0E      FD          REVERT
0F      FD          REVERT

Explanation
As stated in an earlier post, I recommend others look to this repo for the resources and the further explanation of opcodes as presented above.
By way of context, we are trying to send successful transaction to a contract. In this problem, this means that JUMPI(09) is being sent to JUMPDEST(0c).

Solution
From what I could tell 07 onwards is irrelevant to solving the puzzle provided 07 is the right value for 09 to go to JUMPDEST. Get past EQ for the rest of the lines to run through.

  • EQ is checking against the value 0100 at 03 (i.e. 0100) which is base 16 of 256.
  • MUL is multiplying the DUP1 with CALLVALUE, DUP1 is the duplicate of CALLVALUE which means MUL result is the square of CALLVALUE. The solution is the square root of 256 which is 16.

Day 74

Puzzle 6

00          60          PUSH1                      00
02          03          CALLDATALOAD
03          55          JUMP
04          6F          REVERT
05          FD          REVERT
06          FD          REVERT
07          FD          REVERT
08          FD          REVERT
09          FD          REVERT
0a          5B          JUMPDEST
0b          00          STOP

Problem
To solve this puzzle, we need to JUMP(03) to JUMPDEST(0a).

Solution
The key here is CALLDATALOAD. Calldata is the input data to a transaction on a function but CALLDATALOAD will load different data depending on the call frame (e.g. a transaction), and will persist for an individual call frame, but not to the next call frame.

By way of elaborated example, if EOA calls function a on contract A, and contract A calls function b on contract B, the calldataload will no longer load the EOA's call data and will instead load the data used in the sub-call frame.

So provided we have 00 pushed onto the stack here, all we need to do is load a 32 byte value string which looks like this: 0x000000000000000000000000000000000000000000000000000000000000000a

Day 75

Puzzle 9

00          36          CALLDATASIZE
01          60          PUSH1                     03
03          10          LT
04          60          PUSH1                     09
06          57          JUMPI
07          FD          REVERT
08          FD          REVERT
09          5B          JUMPDEST
0a          34          CALLVALUE
0b          36          CALLDATASIZE
0c          02          MUL
0d          60          PUSH1                     08
0f          14          EQ
10          60          PUSH1                     14
12          57          JUMPI
13          FD          REVERT
14          5B          JUMPDEST
15          00          STOP

Problem
To solve this puzzle, we need JUMPI(06) to JUMPDEST(09) and JUMPI(12) to JUMPDEST(14).

Solution
There are two requirements:

  1. 03 < calldatasize; and
  2. (CALLVALUE * CALLDATASIZE) result equals to 08 .

Using a bit of math, the CALLDATASIZE should be 4, and the CALLVALUE should be 2 to satisfy these conditions.

Day 76

When looking at storage issues in the context of proxies, I came across the concept of bitmaps, that I thought was worth exploring a little bit deeper.

What are they?
Bitmaps refer to the raw 1 and 0 bits in memory, which are then used to represent some program state.
An example I really think explains it best is if we have 10 people signed up to attend a class and we want to record whether each person showed up or not. It would look something like this:

// people who showed up: 1, 3, 4, 8, 9
uint8[] memory a = new uint8[](1, 0, 1, 1, 0, 0, 0, 1, 1, 0);

Notice that each value takes up 8 bits of space in RAM, meaning in total we are using up at least 80 bits of memory to represent each person. With the average computer running a 64-bit system, really his means that 128 bits of memory are used.

If we are only trying to represent the occurrence of something (via the array of 1s and 0s) then a single bit could actually represent each value. This would really end up costing 64-bits rather than the previous 128bits because of a more efficient use of storage. Put another way: uint8 could be represented in binary as 00000000, where each bit can be either 0 or 1. This strategy allows for the effective and economical management of boolean value through bitwise operation as opposed to storing each occurrence in a dynamic array.

What issue do they solve?
It saves massively on gas cost. As highlight in this article a setDataWithBoolArray may cost 140,583 gas compared to the use of a bitmap that almost halves it to 78,043 gas.

This is something totally foreign to myself so if others have experience with it or can further explain any points it would be much appreciated! For those interested, from the same linked article, I thought the below example with respect to participants brings the attendance example first explained full circle and applied to Defi:

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

contract ParticipatedWithBitmap {
    uint256[] public participantsBitmap;

    function setParticipants(uint256[] memory participantsBitmap_) external onlyOwner {
        participantsBitmap = participantsBitmap_;
    }
    
    function hasParticipated(uint256 bitmapIndex, uint256 indexFromRight) external view returns (bool) {
        uint256 bitAtIndex = participantsBitmap[bitmapIndex] & (1 << indexFromRight);
        return bitAtIndex > 0;
    }
}

Day 77
I haven’t had much experience with Yul but a lot of previous posts have dealt with the code at the inline assembly level so I thought t prompt to talk about Yul.

What is it?
Basically, this is a language that can be compiled to bytecode for different backends (i.e. inline assembly for Solidity). We have all inadvertently used Yul when using the Solidity compiler but if we wanted to make it even more efficient in terms of its gas cost, then this would be the langue to do so.

What’s it practical use?
Predominately gas-saving. To test this I decided to try it out on the following contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract BitWise {
    // count the number of bit set in data.  i.e. data = 7, result = 3
    function countBitSet(uint8 data) public pure returns (uint8 result) {
        for( uint i = 0; i < 8; i += 1) {
            if( ((data >> i) & 1) == 1) {
                result += 1;
            }
        }
    }
   function countBitSetAsm(uint8 data ) public pure returns (uint8 result) {
        result = countBitSet(data);
    }
}

Say I thought that the countBitSetAsm was not as gas efficient as it could be, I decided to re-implement the same function with inline assembly:

contract BitWise {

    function countBitSet(uint8 data) public pure returns (uint8 result) {
        for (uint i = 0; i < 8; i++) {
            if ((data >> i) & 1 == 1) {
                result++;
            }
        }
    }

    function countBitSetAsm(uint8 data) public pure returns (uint8 result) {
        assembly {
            // Initialize result variable to 0
            result := 0

            // Loop through each bit of data
            for { let i := 0 } lt(i, 8) { i := add(i, 1) } {
                // Shift data right by i bits, then bitwise AND with 1
                // If the result is 1, increment the result variable
                if and(shr(i, data), 1) {
                    result := add(result, 1)
                }
            }
        }
    }
}

Wouldn’t you know it I’ve saved a little bit of gas. The first contract generated 209240 gas whilst the second contract using inline assembly modifications used only 196891. That’s approximately 12k gas!

I thought this was pretty cool and wanted to share my findings from this, I recommend reading Solidity’s documentation on Yul alongside this basic introduction if this was of interest to you.
Any questions on the above and I’d be more than happy to answer.

Day 78

I’ve completed the first Damn Vulnerable Defi challenge but before uploading its solution, I wanted to discuss flash loans (although I note I have mentioned it in previous posts!).

Flash Loans
Flash loans are uncollateralized loans (i.e. they have no assets backing them) where a user borrows funds and returns them in the same transaction. If the user can’t repay the loan before the transaction is completed, a smart contract cancels the transaction and returns the money to the lender. The ‘flash’ aspect of them is because the loan is made instantaneously (unlike Banks that take months).

A simplified flash loan works as follows:

  1. The lender transfers requested amount to borrower;
  2. The borrower invokes pre-designed operations (e.g. makes a swap);
  3. The borrower uses the asset for whatever purpose they wanted (say to arbitrage a pricing difference between tokens);
  4. Once step 3 is complete, they return the assets to the lenders; and
  5. The lender checks the balance to ensure the user has given the full amount back. If they haven’t, the lender reverts the transaction immediately.

This application of Defi is a foreign to me. I don’t necessarily understand the value it offers users aside from potentially levering up to make a trade that could provide profit. Otherwise, at a macro level I guess the argument could be made that this enables speculation on a currency to ensure the pricing reflects fair value.

I would also note that from a security perspective if a malicious actor was to find a vulnerability in a contract, the flash loan would be used to further contribute capital to the malicious cause. I think look no further than day 49 in my post whereby flashloan bypasses are still common in audits and attacks are perpetrated by flash loans.

Day 79

Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "solmate/src/utils/FixedPointMathLib.sol";
import "solmate/src/utils/ReentrancyGuard.sol";
import { SafeTransferLib, ERC4626, ERC20 } from "solmate/src/mixins/ERC4626.sol";
import "solmate/src/auth/Owned.sol";
import { IERC3156FlashBorrower, IERC3156FlashLender } from "@openzeppelin/contracts/interfaces/IERC3156.sol";

/**
 * @title UnstoppableVault
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626 {
    using SafeTransferLib for ERC20;
    using FixedPointMathLib for uint256;

    uint256 public constant FEE_FACTOR = 0.05 ether;
    uint64 public constant GRACE_PERIOD = 30 days;

    uint64 public immutable end = uint64(block.timestamp) + GRACE_PERIOD;

    address public feeRecipient;

    error InvalidAmount(uint256 amount);
    error InvalidBalance();
    error CallbackFailed();
    error UnsupportedCurrency();

    event FeeRecipientUpdated(address indexed newFeeRecipient);

    constructor(ERC20 _token, address _owner, address _feeRecipient)
        ERC4626(_token, "Oh Damn Valuable Token", "oDVT")
        Owned(_owner)
    {
        feeRecipient = _feeRecipient;
        emit FeeRecipientUpdated(_feeRecipient);
    }

    /**
     * @inheritdoc IERC3156FlashLender
     */
    function maxFlashLoan(address _token) public view returns (uint256) {
        if (address(asset) != _token)
            return 0;

        return totalAssets();
    }

    /**
     * @inheritdoc IERC3156FlashLender
     */
    function flashFee(address _token, uint256 _amount) public view returns (uint256 fee) {
        if (address(asset) != _token)
            revert UnsupportedCurrency();

        if (block.timestamp < end && _amount < maxFlashLoan(_token)) {
            return 0;
        } else {
            return _amount.mulWadUp(FEE_FACTOR);
        }
    }

    function setFeeRecipient(address _feeRecipient) external onlyOwner {
        if (_feeRecipient != address(this)) {
            feeRecipient = _feeRecipient;
            emit FeeRecipientUpdated(_feeRecipient);
        }
    }

    /**
     * @inheritdoc ERC4626
     */
    function totalAssets() public view override returns (uint256) {
        assembly { // better safe than sorry
            if eq(sload(0), 2) {
                mstore(0x00, 0xed3ba6a6)
                revert(0x1c, 0x04)
            }
        }
        return asset.balanceOf(address(this));
    }

    /**
     * @inheritdoc IERC3156FlashLender
     */
    function flashLoan(
        IERC3156FlashBorrower receiver,
        address _token,
        uint256 amount,
        bytes calldata data
    ) external returns (bool) {
        if (amount == 0) revert InvalidAmount(0); // fail early
        if (address(asset) != _token) revert UnsupportedCurrency(); // enforce ERC3156 requirement
        uint256 balanceBefore = totalAssets();
        if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance(); // enforce ERC4626 requirement
        uint256 fee = flashFee(_token, amount);
        // transfer tokens out + execute callback on receiver
        ERC20(_token).safeTransfer(address(receiver), amount);
        // callback must return magic value, otherwise assume it failed
        if (receiver.onFlashLoan(msg.sender, address(asset), amount, fee, data) != keccak256("IERC3156FlashBorrower.onFlashLoan"))
            revert CallbackFailed();
        // pull amount + fee from receiver, then pay the fee to the recipient
        ERC20(_token).safeTransferFrom(address(receiver), address(this), amount + fee);
        ERC20(_token).safeTransfer(feeRecipient, fee);
        return true;
    }

    /**
     * @inheritdoc ERC4626
     */
    function beforeWithdraw(uint256 assets, uint256 shares) internal override nonReentrant {}

    /**
     * @inheritdoc ERC4626
     */
    function afterDeposit(uint256 assets, uint256 shares) internal override nonReentrant {}
}

Problem
In this challenge, you need to break the functionality of the flash loan contract, which is relatively simple so I have no provided a breakdown of the functionality of the contract.

Solution
At a higher level, the contract tracks its available balance in a storage variable poolBalance and requires this variable to equal the contract’s actual token balance (token.balanceOf(address(this))).

To break the functionality, send tokens to the contract directly through the token’s transfer function as opposed to using the flash loan contract’s deposit function.

Day 80

Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "solady/src/utils/SafeTransferLib.sol";
import "./FlashLoanReceiver.sol";

/**
 * @title NaiveReceiverLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract NaiveReceiverLenderPool is ReentrancyGuard, IERC3156FlashLender {

    address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
    uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
    bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

    error RepayFailed();
    error UnsupportedCurrency();
    error CallbackFailed();

    function maxFlashLoan(address token) external view returns (uint256) {
        if (token == ETH) {
            return address(this).balance;
        }
        return 0;
    }

    function flashFee(address token, uint256) external pure returns (uint256) {
        if (token != ETH)
            revert UnsupportedCurrency();
        return FIXED_FEE;
    }

    function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external returns (bool) {
        if (token != ETH)
            revert UnsupportedCurrency();
        
        uint256 balanceBefore = address(this).balance;

        // Transfer ETH and handle control to receiver
        SafeTransferLib.safeTransferETH(address(receiver), amount);
        if(receiver.onFlashLoan(
            msg.sender,
            ETH,
            amount,
            FIXED_FEE,
            data
        ) != CALLBACK_SUCCESS) {
            revert CallbackFailed();
        }

        if (address(this).balance < balanceBefore + FIXED_FEE)
            revert RepayFailed();

        return true;
    }

    // Allow deposits of ETH
    receive() external payable {}
}

Breakdown
There is a lot to unpack but keeping the objective in mind (steal all the funds in one transaction) the real high-level functions I was interested in were flashLoan, flashFee and maxFlashLoan.

I looked at these three to determine if you had to be an owner or msg.sender to receive the fee, the maximum amount you could take in one transaction and whether any modifiers existed on the flashloan itself.

Solution
As alluded to above, there is no authentication that the user must be an owner. This means anyone can receive any flash loan on behalf of that contract. The contract checks if msg.sender is the flash loan contract (if(receiver.onFlashLoan(msg.sender, …) but using a bit of logic, this is always the case provided the callback function ( bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");) is invoked from the flash loan contract.

This means we only need to deploy a contract that repeatedly takes flash loans on the user contract’s behalf until its balance is less than the flash loan fee (1 ETH).

1 Like

This is an awesome post @JarrodP :rocket:

And so ably and generously assisted by @barakman along the way.

I'm almost ready to begin my own coding journey from a dinosaur programmer and I will be sure to follow in some of your footsteps.

Thankyou for sharing it for the community's benefit.

2 Likes

@Neal I honestly can't tell you how much that means to me, thank you for such kind words.

If I am able to assist in your learning journey in any way, please let me know. Alternatively, if you are reading through any of my posts (either new or old) and have a question, I'd be more than happy to take the time to discuss!

2 Likes

Day 81

Code

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../DamnValuableToken.sol";

/**
 * @title TrusterLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
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;
    }
}

Breakdown
As explained in a previous post relating to flashloans (day 78), the aim of the contract is to immediately give a contract user tokens, they use the tokens in an another transaction and, they refund that amount. We can see exactly this logic play out with the following:

  1. DVT token is imported into the contract ( DamnValuableToken public immutable token; ;
  2. flashLoan takes an amount, the borrower's address, 'target' being the place it will be spent, and data;
  3. balance of this address is calculated as the flashLoan function is called and a transfer is made ( uint256 balanceBefore = token.balanceOf(address(this));) (note that a reentrancy guard is here); and
  4. revert the transaction if the contract balance is less than its original value prior to the borrower using the funds (if(token.balanceOf(address(this)) < balanceBefore) revert RepayFailed();).

Problem
We have a flash loan contract offering loans for the DVT token and we have to steal the tokens.

Solution
This was a bit tricky for me because the contract seemed to work fine when testing it out and there was no reentrancy exploit. The issue was actually with the line target.functionCall(data); because it allows a user to control what goes into the target and the data input parameters of the flashLoan function.

The way to exploit this is as follows:

  1. call the flashLoan function with the following parameters (0, /* player address */ , token, data);
  2. inputting token allows us to specify which tokens we are after (being the ERC20 DVT token) and data will need to be specified such that we are able to call the approve function in the ERC20 token (e.g. in a script you would set data to be something like interface.encodeFunctionData("approve", [player address, TOKENS_IN_POOL]);); and
  3. since we now have the allowance to spend the tokens on behalf of the flashloan contract, this means we can just transfer the entire amount from the pool to our address.
1 Like

You're welcome Jarrod and thanks for the kind offer to assist.

In the meantime, keep up the good work and I'm sure you will be auditing smart contracts before too long .. :fist_right: :fist_left:

2 Likes

Day 82

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "solady/src/utils/SafeTransferLib.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SideEntranceLenderPool {
    mapping(address => uint256) private balances;

    error RepayFailed();

    event Deposit(address indexed who, uint256 amount);
    event Withdraw(address indexed who, uint256 amount);

    function deposit() external payable {
        unchecked {
            balances[msg.sender] += msg.value;
        }
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        
        delete balances[msg.sender];
        emit Withdraw(msg.sender, amount);

        SafeTransferLib.safeTransferETH(msg.sender, amount);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;

        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

        if (address(this).balance < balanceBefore)
            revert RepayFailed();
    }
}

Breakdown

  • mapping(address => uint256) private balances; checks users balances, which is subsequently updated by the respective deposit and withdraw functions; and
  • flashLoan checks the token’s balance before a transaction and reverts if the current balance is less than the original balance (if (address(this).balance < balanceBefore).

Problem
Starting with 1 ETH in balance, pass the challenge by taking all ETH from the pool. Note that it already has 1000 ETH in balance already and is offering free flash loans using the deposited ETH to promote their system.

Solution

  1. Call flashLoan ensuring the amount is all currently held funds in the pool;
  2. Call the deposit function to ensure the same borrowed tokens are temporarily put back to the pool;
  3. call the withdraw function and take away all the ETH from the lending pool; and
  4. transfer to the attacker’s address.

The reason the above works is because when calling the flashLoan function of the pool, IFlashLoanEtherReceiver(msg.sender).execute{value: amount}(); will also be called. The effect of this is you will need to repay the loan, but if you did this via the deposit functionality, the contract records the deposit as both the repayment of the loan and a separate deposit, despite them being the same transaction.

Day 83

There are two important contracts here:

FlashLoan contract

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "../DamnValuableToken.sol";

/**
 * @title FlashLoanerPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 * @dev A simple pool to get flashloans of DVT
 */
contract FlashLoanerPool is ReentrancyGuard {
    using Address for address;

    DamnValuableToken public immutable liquidityToken;

    error NotEnoughTokenBalance();
    error CallerIsNotContract();
    error FlashLoanNotPaidBack();

    constructor(address liquidityTokenAddress) {
        liquidityToken = DamnValuableToken(liquidityTokenAddress);
    }

    function flashLoan(uint256 amount) external nonReentrant {
        uint256 balanceBefore = liquidityToken.balanceOf(address(this));

        if (amount > balanceBefore) {
            revert NotEnoughTokenBalance();
        }

        if (!msg.sender.isContract()) {
            revert CallerIsNotContract();
        }

        liquidityToken.transfer(msg.sender, amount);

        msg.sender.functionCall(abi.encodeWithSignature("receiveFlashLoan(uint256)", amount));

        if (liquidityToken.balanceOf(address(this)) < balanceBefore) {
            revert FlashLoanNotPaidBack();
        }
    }
}

Reward Pool

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "solady/src/utils/FixedPointMathLib.sol";
import "solady/src/utils/SafeTransferLib.sol";
import { RewardToken } from "./RewardToken.sol";
import { AccountingToken } from "./AccountingToken.sol";

/**
 * @title TheRewarderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract TheRewarderPool {
    using FixedPointMathLib for uint256;

    // Minimum duration of each round of rewards in seconds
    uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days;
    
    uint256 public constant REWARDS = 100 ether;

    // Token deposited into the pool by users
    address public immutable liquidityToken;

    // Token used for internal accounting and snapshots
    // Pegged 1:1 with the liquidity token
    AccountingToken public immutable accountingToken;

    // Token in which rewards are issued
    RewardToken public immutable rewardToken;

    uint128 public lastSnapshotIdForRewards;
    uint64 public lastRecordedSnapshotTimestamp;
    uint64 public roundNumber; // Track number of rounds
    mapping(address => uint64) public lastRewardTimestamps;

    error InvalidDepositAmount();

    constructor(address _token) {
        // Assuming all tokens have 18 decimals
        liquidityToken = _token;
        accountingToken = new AccountingToken();
        rewardToken = new RewardToken();

        _recordSnapshot();
    }

    /**
     * @notice Deposit `amount` liquidity tokens into the pool, minting accounting tokens in exchange.
     *         Also distributes rewards if available.
     * @param amount amount of tokens to be deposited
     */
    function deposit(uint256 amount) external {
        if (amount == 0) {
            revert InvalidDepositAmount();
        }

        accountingToken.mint(msg.sender, amount);
        distributeRewards();

        SafeTransferLib.safeTransferFrom(
            liquidityToken,
            msg.sender,
            address(this),
            amount
        );
    }

    function withdraw(uint256 amount) external {
        accountingToken.burn(msg.sender, amount);
        SafeTransferLib.safeTransfer(liquidityToken, msg.sender, amount);
    }

    function distributeRewards() public returns (uint256 rewards) {
        if (isNewRewardsRound()) {
            _recordSnapshot();
        }

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

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

    function _recordSnapshot() private {
        lastSnapshotIdForRewards = uint128(accountingToken.snapshot());
        lastRecordedSnapshotTimestamp = uint64(block.timestamp);
        unchecked {
            ++roundNumber;
        }
    }

    function _hasRetrievedReward(address account) private view returns (bool) {
        return (
            lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp
                && lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION
        );
    }

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

Higher Level Breakdown
High level summary here is you have FlashLoan contract that really isn’t any different to the others expressed in previous posts (aside from the fact it has proper accounting for depositing / withdrawing loans) and a reward contract. The reward contract pays out a reward every five days based on token amounts at a single point in time.

Problem
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…I think we can all see where this is going.

Solution
This one just required a bit of logic: if you know the contract is going to take a particular point in time and there happens to be a flashloan provider nearby…no one is too surprised when a massive influx of tokens suddenly appear at the last minute (i.e. before the snapshot of the token balance) and that user is paid the entirety of the reward.

1 Like

Hi @JarrodP,

Your posts have convinced me to join the community in this forum. I am also starting to learn about Web3 and starting to do some challenges.

I hope I will be as committed you are and diligent enough to record down your learning progress. I'll be on your posts now and then, and I might start my own journey posts as well!

Cheers and wishing you all the best in your journey

1 Like

Hi @kxun Thank you so much for posting and I'm so glad you decided to become part of the community. I've found that this community is always willing to help out and there are an abundance of resource at your fingertips (you need only ask for help!).

Don't be a stranger, I'd love to help where I can and also wishing you all the best on your journey.

Day 84

ISimpleGovernance

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ISimpleGovernance {
    struct GovernanceAction {
        uint128 value;
        uint64 proposedAt;
        uint64 executedAt;
        address target;
        bytes data;
    }

    error NotEnoughVotes(address who);
    error CannotExecute(uint256 actionId);
    error InvalidTarget();
    error TargetMustHaveCode();
    error ActionFailed(uint256 actionId);

    event ActionQueued(uint256 actionId, address indexed caller);
    event ActionExecuted(uint256 actionId, address indexed caller);

    function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId);
    function executeAction(uint256 actionId) external payable returns (bytes memory returndata);
    function getActionDelay() external view returns (uint256 delay);
    function getGovernanceToken() external view returns (address token);
    function getAction(uint256 actionId) external view returns (GovernanceAction memory action);
    function getActionCounter() external view returns (uint256);
}

SelfiePool

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "./SimpleGovernance.sol";

/**
 * @title SelfiePool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SelfiePool is ReentrancyGuard, IERC3156FlashLender {

    ERC20Snapshot public immutable token;
    SimpleGovernance public immutable governance;
    bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

    error RepayFailed();
    error CallerNotGovernance();
    error UnsupportedCurrency();
    error CallbackFailed();

    event FundsDrained(address indexed receiver, uint256 amount);

    modifier onlyGovernance() {
        if (msg.sender != address(governance))
            revert CallerNotGovernance();
        _;
    }

    constructor(address _token, address _governance) {
        token = ERC20Snapshot(_token);
        governance = SimpleGovernance(_governance);
    }

    function maxFlashLoan(address _token) external view returns (uint256) {
        if (address(token) == _token)
            return token.balanceOf(address(this));
        return 0;
    }

    function flashFee(address _token, uint256) external view returns (uint256) {
        if (address(token) != _token)
            revert UnsupportedCurrency();
        return 0;
    }

    function flashLoan(
        IERC3156FlashBorrower _receiver,
        address _token,
        uint256 _amount,
        bytes calldata _data
    ) external nonReentrant returns (bool) {
        if (_token != address(token))
            revert UnsupportedCurrency();

        token.transfer(address(_receiver), _amount);
        if (_receiver.onFlashLoan(msg.sender, _token, _amount, 0, _data) != CALLBACK_SUCCESS)
            revert CallbackFailed();

        if (!token.transferFrom(address(_receiver), address(this), _amount))
            revert RepayFailed();
        
        return true;
    }

    function emergencyExit(address receiver) external onlyGovernance {
        uint256 amount = token.balanceOf(address(this));
        token.transfer(receiver, amount);

        emit FundsDrained(receiver, amount);
    }
}

SimpleGovernance

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../DamnValuableTokenSnapshot.sol";
import "./ISimpleGovernance.sol"
;
/**
 * @title SimpleGovernance
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SimpleGovernance is ISimpleGovernance {

    uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days;
    DamnValuableTokenSnapshot private _governanceToken;
    uint256 private _actionCounter;
    mapping(uint256 => GovernanceAction) private _actions;

    constructor(address governanceToken) {
        _governanceToken = DamnValuableTokenSnapshot(governanceToken);
        _actionCounter = 1;
    }

    function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId) {
        if (!_hasEnoughVotes(msg.sender))
            revert NotEnoughVotes(msg.sender);

        if (target == address(this))
            revert InvalidTarget();
        
        if (data.length > 0 && target.code.length == 0)
            revert TargetMustHaveCode();

        actionId = _actionCounter;

        _actions[actionId] = GovernanceAction({
            target: target,
            value: value,
            proposedAt: uint64(block.timestamp),
            executedAt: 0,
            data: data
        });

        unchecked { _actionCounter++; }

        emit ActionQueued(actionId, msg.sender);
    }

    function executeAction(uint256 actionId) external payable returns (bytes memory) {
        if(!_canBeExecuted(actionId))
            revert CannotExecute(actionId);

        GovernanceAction storage actionToExecute = _actions[actionId];
        actionToExecute.executedAt = uint64(block.timestamp);

        emit ActionExecuted(actionId, msg.sender);

        (bool success, bytes memory returndata) = actionToExecute.target.call{value: actionToExecute.value}(actionToExecute.data);
        if (!success) {
            if (returndata.length > 0) {
                assembly {
                    revert(add(0x20, returndata), mload(returndata))
                }
            } else {
                revert ActionFailed(actionId);
            }
        }

        return returndata;
    }

    function getActionDelay() external pure returns (uint256) {
        return ACTION_DELAY_IN_SECONDS;
    }

    function getGovernanceToken() external view returns (address) {
        return address(_governanceToken);
    }

    function getAction(uint256 actionId) external view returns (GovernanceAction memory) {
        return _actions[actionId];
    }

    function getActionCounter() external view returns (uint256) {
        return _actionCounter;
    }

    /**
     * @dev an action can only be executed if:
     * 1) it's never been executed before and
     * 2) enough time has passed since it was first proposed
     */
    function _canBeExecuted(uint256 actionId) private view returns (bool) {
        GovernanceAction memory actionToExecute = _actions[actionId];
        
        if (actionToExecute.proposedAt == 0) // early exit
            return false;

        uint64 timeDelta;
        unchecked {
            timeDelta = uint64(block.timestamp) - actionToExecute.proposedAt;
        }

        return actionToExecute.executedAt == 0 && timeDelta >= ACTION_DELAY_IN_SECONDS;
    }

    function _hasEnoughVotes(address who) private view returns (bool) {
        uint256 balance = _governanceToken.getBalanceAtLastSnapshot(who);
        uint256 halfTotalSupply = _governanceToken.getTotalSupplyAtLastSnapshot() / 2;
        return balance > halfTotalSupply;
    }
}

Breakdown
There are three central contracts to consider for this vulnerability, which I have broken down below:

  • ISimpleGovernance and Governance together hold the custom errors and events. Most notably these contracts govern how actions are administered and created. What this means practically is users can vote on the management of the SelfiePool liquidity pool; and
  • SelfiePool receives tokens from users and contains a standard series of functions that are attributable to a flashloan (i.e. an actual flashloan function, fees, maximum flashloan capable of being drawn).

The most notable function across the respective contracts is the queueAction function (contained in SimpleGovernance) whereby a msg.sender is required to have enough votes (i.e. specially 51% of tokens) to queue an action that will eventually be admitted to the governance contract.

Problem
You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal is to take them all.

Solution
I think this was a very similar problem we exploited at day 83. The process flow for exploiting the contract would be:

  1. Take out a huge loan via the flashloan function;
  2. Create an action called something like takeEveryonesDVT;
  3. Sit back and wait for the governance contract to implement that change.
    I’d note that the amount you have to borrow via the flashloan doesn’t have to be in the amount of $1.5m pledged in the pool. It only has to be greater than 51% of the governance token supply.