
WTF Solidity 合约安全: S08. 绕过合约检查
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍绕过合约长度检查,并介绍预防的方法。绕过合约检查很多 freemint 的项目为了限制科学家(程序员)会用到 isContract() 方法,希望将调用者 msg.sender 限制为外部账户(EOA),而非合约。这个函数利用 extcodesize 获取该地址所存储的 bytecode 长度(runtime),若大于0,则判断为合约,否则就是EOA(用户)。 // 利用 extcodesize 检查是否为合约 function isContract(address account) public view returns (bool) { // extcodesize > 0 的地址一定是合约...

WTF Solidity 合约安全: S09. 拒绝服务
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的拒绝服务(Denial of Service, DoS)漏洞,并介绍预防的方法。NFT项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH,当时价值 3400 万美元。DoS在 Web2 中,拒绝服务攻击(DoS)是指通过向服务器发送大量垃圾信息或干扰信息的方式,导致服务器无法向正常用户提供服务的现象。而在 Web3,它指的是利用漏洞使得智能合约无法正常提供服务。 在2022年4月,一个很火的 NFT 项目名为 Akutar,他们使用荷兰拍卖进行公开发行,筹集了 11,539.5 ETH,非常成功。之前持有他们社区Pass的参与者会得到 0.5 ETH的退款,但是他们处理...

WTF Solidity 合约安全 S06. 签名重放
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的签名重放(Signature Replay)攻击和预防方法,它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。签名重放上学的时候,老师经常会让家长签字,有时候家长很忙,我就会很“贴心”照着以前的签字抄一遍。某种意义上来说,这就是签名重放。 在区块链中,数字签名可以用于识别数据签名者和验证数据完整性。发送交易时,用户使用私钥签名交易,使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 ECDSA 算法验证用户将在链下创建的签名,然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见WTF Solidity第37讲:数字签名。 数字签名一般有两种常见的重放攻击...
WTF Academy: wtf.academy



WTF Solidity 合约安全: S08. 绕过合约检查
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍绕过合约长度检查,并介绍预防的方法。绕过合约检查很多 freemint 的项目为了限制科学家(程序员)会用到 isContract() 方法,希望将调用者 msg.sender 限制为外部账户(EOA),而非合约。这个函数利用 extcodesize 获取该地址所存储的 bytecode 长度(runtime),若大于0,则判断为合约,否则就是EOA(用户)。 // 利用 extcodesize 检查是否为合约 function isContract(address account) public view returns (bool) { // extcodesize > 0 的地址一定是合约...

WTF Solidity 合约安全: S09. 拒绝服务
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的拒绝服务(Denial of Service, DoS)漏洞,并介绍预防的方法。NFT项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH,当时价值 3400 万美元。DoS在 Web2 中,拒绝服务攻击(DoS)是指通过向服务器发送大量垃圾信息或干扰信息的方式,导致服务器无法向正常用户提供服务的现象。而在 Web3,它指的是利用漏洞使得智能合约无法正常提供服务。 在2022年4月,一个很火的 NFT 项目名为 Akutar,他们使用荷兰拍卖进行公开发行,筹集了 11,539.5 ETH,非常成功。之前持有他们社区Pass的参与者会得到 0.5 ETH的退款,但是他们处理...

WTF Solidity 合约安全 S06. 签名重放
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的签名重放(Signature Replay)攻击和预防方法,它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。签名重放上学的时候,老师经常会让家长签字,有时候家长很忙,我就会很“贴心”照着以前的签字抄一遍。某种意义上来说,这就是签名重放。 在区块链中,数字签名可以用于识别数据签名者和验证数据完整性。发送交易时,用户使用私钥签名交易,使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 ECDSA 算法验证用户将在链下创建的签名,然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见WTF Solidity第37讲:数字签名。 数字签名一般有两种常见的重放攻击...
WTF Academy: wtf.academy
Share Dialog
Share Dialog

Subscribe to 0xAA

Subscribe to 0xAA
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity
这一讲,我们介绍代理合约(Proxy Contract)。教学代码由OpenZepplin的Proxy合约简化而来。
Solidity合约部署在链上之后,代码是不可变的(immutable)。这样既有优点,也有缺点:
优点:安全,用户知道会发生什么(大部分时候)。
坏处:就算合约中存在bug,也不能修改或升级,只能部署新合约。但是新合约的地址与旧的不一样,且合约的数据也需要花费大量gas进行迁移。
有没有办法在合约部署后进行修改或升级呢?答案是有的,那就是代理模式。

我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity
这一讲,我们介绍代理合约(Proxy Contract)。教学代码由OpenZepplin的Proxy合约简化而来。
Solidity合约部署在链上之后,代码是不可变的(immutable)。这样既有优点,也有缺点:
优点:安全,用户知道会发生什么(大部分时候)。
坏处:就算合约中存在bug,也不能修改或升级,只能部署新合约。但是新合约的地址与旧的不一样,且合约的数据也需要花费大量gas进行迁移。
有没有办法在合约部署后进行修改或升级呢?答案是有的,那就是代理模式。

代理模式将合约数据和逻辑分开,分别保存在不同合约中。我们拿上图中简单的代理合约为例,数据(状态变量)存储在代理合约中,而逻辑(函数)保存在另一个逻辑合约中。代理合约(Proxy)通过delegatecall,将函数调用全权委托给逻辑合约(Implementation)执行,再把最终的结果返回给调用者(Caller)。
代理模式主要有两个好处:
可升级:当我们需要升级合约的逻辑时,只需要将代理合约指向新的逻辑合约。
省gas:如果多个合约复用一套逻辑,我们只需部署一个逻辑合约,然后再部署多个只保存数据的代理合约,指向逻辑合约。
提示:对delegatecall不熟悉的朋友可以看下本教程第23讲Delegatecall。
下面我们介绍一个简单的代理合约,它由OpenZepplin的Proxy合约简化而来。它有三个部分:代理合约Proxy,逻辑合约Logic,和一个调用示例Caller。它的逻辑并不复杂:
首先部署逻辑合约Logic。
创建代理合约Proxy,状态变量implementation记录Logic合约地址。
Proxy合约利用回调函数fallback,将所有调用委托给Logic合约
最后部署调用示例Caller合约,调用Proxy合约。
注意:Logic合约和Proxy合约的状态变量存储结构相同,不然delegatecall会产生意想不到的行为,有安全隐患。
Proxy合约不长,但是用到了内联汇编,因此比较难理解。它只有一个状态变量,一个构造函数,和一个回调函数。状态变量implementation,在构造函数中初始化,用于保存Logic合约地址。
contract Proxy {
address public immutable implementation; // 逻辑合约地址。implementation合约同一个位置的状态变量类型必须和Proxy合约的相同,不然会报错。
/**
* @dev 初始化逻辑合约地址
*/
constructor(address implementation_){
implementation = implementation_;
}
Proxy的回调函数将外部对本合约的调用委托给 Logic 合约。这个回调函数很别致,它利用内联汇编(inline assembly),让本来不能有返回值的回调函数有了返回值。其中用到的内联汇编操作码:
calldatacopy(t, f, s):将calldata(输入数据)从位置f开始复制s字节到mem(内存)的位置t。
delegatecall(g, a, in, insize, out, outsize):调用地址a的合约,输入为mem[in..(in+insize)) ,输出为mem[out..(out+outsize)), 提供g的gas 和v wei的以太坊。这个操作码在错误时返回0,在成功时返回1。
returndatacopy(t, f, s):将returndata(输出数据)从位置f开始复制s字节到mem(内存)的位置t。
switch:基础版if/else,不同的情况case返回不同值。可以有一个默认的default情况。
return(p, s):终止函数执行, 返回数据mem[p..(p+s))。
revert(p, s):终止函数执行, 回滚状态,返回数据mem[p..(p+s))。
/**
* @dev 回调函数,将本合约的调用委托给 `implementation` 合约
* 通过assembly,让回调函数也能有返回值
*/
fallback() external payable {
address _implementation = implementation;
assembly {
// 将msg.data拷贝到内存里
// calldatacopy操作码的参数: 内存起始位置,calldata起始位置,calldata长度
calldatacopy(0, 0, calldatasize())
// 利用delegatecall调用implementation合约
// delegatecall操作码的参数:gas, 目标合约地址,input mem起始位置,input mem长度,output area mem起始位置,output area mem长度
// output area起始位置和长度位置,所以设为0
// delegatecall成功返回1,失败返回0
let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
// 将return data拷贝到内存
// returndata操作码的参数:内存起始位置,returndata起始位置,returndata长度
returndatacopy(0, 0, returndatasize())
switch result
// 如果delegate call失败,revert
case 0 {
revert(0, returndatasize())
}
// 如果delegate call成功,返回mem起始位置为0,长度为returndatasize()的数据(格式为bytes)
default {
return(0, returndatasize())
}
}
}
这是一个非常简单的逻辑合约,只是为了演示代理合约。它包含2个变量,1个事件,1个函数:
implementation:占位变量,与Proxy合约保持一致,防止插槽冲突。
x:uint变量,被设置为99。
CallSuccess事件:在调用成功时释放。
increment()函数:会被Proxy合约调用,释放CallSuccess事件,并返回一个uint,它的selector为0xd09de08a。如果直接调用increment()回返回100,但是通过Proxy调用它会返回1,大家可以想想为什么?
/**
* @dev 逻辑合约,执行被委托的调用
*/
contract Logic {
address public implementation; // 与Proxy保持一致,防止插槽冲突
uint public x = 99;
event CallSuccess(); // 调用成功事件
// 这个函数会释放CallSuccess事件并返回一个uint。
// 函数selector: 0xd09de08a
function increment() external returns(uint) {
emit CallSuccess();
return x + 1;
}
}
Caller合约会演示如何调用一个代理合约,它也非常简单。但是要理解它,你需要先学习本教程的第22讲 Call和第27讲 ABI编码。
它有1个变量,2个函数:
proxy:状态变量,记录代理合约地址。
构造函数:在部署合约时初始化proxy变量。
increase():利用call来调用代理合约的increment()函数,并返回一个uint。在调用时,我们利用abi.encodeWithSignature()获取了increment()函数的selector。在返回时,利用abi.decode()将返回值解码为uint类型。
/**
* @dev Caller合约,调用代理合约,并获取执行结果
*/
contract Caller{
address public proxy; // 代理合约地址
constructor(address proxy_){
proxy = proxy_;
}
// 通过代理合约调用increment()函数
function increment() external returns(uint) {
( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()"));
return abi.decode(data,(uint));
}
}
部署Logic合约。
调用Logic合约的increment()函数,返回100。
部署Proxy合约,初始化时填入Logic合约地址。
调用Proxy合约increment()函数,无返回值。
调用方法:在Remix部署面板中点Proxy合约,在最下面的Low level interaction中填入increment()函数的选择器0xd09de08a,并点击Transact。
部署Caller合约,初始化时填入Proxy合约地址。
调用Caller合约increment()函数,返回1。
这一讲,我们介绍了代理模式和简单的代理合约。代理合约利用delegatecall将函数调用委托给了另一个逻辑合约,使得数据和逻辑分别由不同合约负责。并且,它利用内联汇编黑魔法,让没有返回值的回调函数也可以返回数据。下一讲,我们会介绍可升级代理合约。
代理合约虽然很强大,但是它非常容易出bug,用的时候最好直接复制OpenZepplin的模版合约。
代理模式将合约数据和逻辑分开,分别保存在不同合约中。我们拿上图中简单的代理合约为例,数据(状态变量)存储在代理合约中,而逻辑(函数)保存在另一个逻辑合约中。代理合约(Proxy)通过delegatecall,将函数调用全权委托给逻辑合约(Implementation)执行,再把最终的结果返回给调用者(Caller)。
代理模式主要有两个好处:
可升级:当我们需要升级合约的逻辑时,只需要将代理合约指向新的逻辑合约。
省gas:如果多个合约复用一套逻辑,我们只需部署一个逻辑合约,然后再部署多个只保存数据的代理合约,指向逻辑合约。
提示:对delegatecall不熟悉的朋友可以看下本教程第23讲Delegatecall。
下面我们介绍一个简单的代理合约,它由OpenZepplin的Proxy合约简化而来。它有三个部分:代理合约Proxy,逻辑合约Logic,和一个调用示例Caller。它的逻辑并不复杂:
首先部署逻辑合约Logic。
创建代理合约Proxy,状态变量implementation记录Logic合约地址。
Proxy合约利用回调函数fallback,将所有调用委托给Logic合约
最后部署调用示例Caller合约,调用Proxy合约。
注意:Logic合约和Proxy合约的状态变量存储结构相同,不然delegatecall会产生意想不到的行为,有安全隐患。
Proxy合约不长,但是用到了内联汇编,因此比较难理解。它只有一个状态变量,一个构造函数,和一个回调函数。状态变量implementation,在构造函数中初始化,用于保存Logic合约地址。
contract Proxy {
address public immutable implementation; // 逻辑合约地址。implementation合约同一个位置的状态变量类型必须和Proxy合约的相同,不然会报错。
/**
* @dev 初始化逻辑合约地址
*/
constructor(address implementation_){
implementation = implementation_;
}
Proxy的回调函数将外部对本合约的调用委托给 Logic 合约。这个回调函数很别致,它利用内联汇编(inline assembly),让本来不能有返回值的回调函数有了返回值。其中用到的内联汇编操作码:
calldatacopy(t, f, s):将calldata(输入数据)从位置f开始复制s字节到mem(内存)的位置t。
delegatecall(g, a, in, insize, out, outsize):调用地址a的合约,输入为mem[in..(in+insize)) ,输出为mem[out..(out+outsize)), 提供g的gas 和v wei的以太坊。这个操作码在错误时返回0,在成功时返回1。
returndatacopy(t, f, s):将returndata(输出数据)从位置f开始复制s字节到mem(内存)的位置t。
switch:基础版if/else,不同的情况case返回不同值。可以有一个默认的default情况。
return(p, s):终止函数执行, 返回数据mem[p..(p+s))。
revert(p, s):终止函数执行, 回滚状态,返回数据mem[p..(p+s))。
/**
* @dev 回调函数,将本合约的调用委托给 `implementation` 合约
* 通过assembly,让回调函数也能有返回值
*/
fallback() external payable {
address _implementation = implementation;
assembly {
// 将msg.data拷贝到内存里
// calldatacopy操作码的参数: 内存起始位置,calldata起始位置,calldata长度
calldatacopy(0, 0, calldatasize())
// 利用delegatecall调用implementation合约
// delegatecall操作码的参数:gas, 目标合约地址,input mem起始位置,input mem长度,output area mem起始位置,output area mem长度
// output area起始位置和长度位置,所以设为0
// delegatecall成功返回1,失败返回0
let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
// 将return data拷贝到内存
// returndata操作码的参数:内存起始位置,returndata起始位置,returndata长度
returndatacopy(0, 0, returndatasize())
switch result
// 如果delegate call失败,revert
case 0 {
revert(0, returndatasize())
}
// 如果delegate call成功,返回mem起始位置为0,长度为returndatasize()的数据(格式为bytes)
default {
return(0, returndatasize())
}
}
}
这是一个非常简单的逻辑合约,只是为了演示代理合约。它包含2个变量,1个事件,1个函数:
implementation:占位变量,与Proxy合约保持一致,防止插槽冲突。
x:uint变量,被设置为99。
CallSuccess事件:在调用成功时释放。
increment()函数:会被Proxy合约调用,释放CallSuccess事件,并返回一个uint,它的selector为0xd09de08a。如果直接调用increment()回返回100,但是通过Proxy调用它会返回1,大家可以想想为什么?
/**
* @dev 逻辑合约,执行被委托的调用
*/
contract Logic {
address public implementation; // 与Proxy保持一致,防止插槽冲突
uint public x = 99;
event CallSuccess(); // 调用成功事件
// 这个函数会释放CallSuccess事件并返回一个uint。
// 函数selector: 0xd09de08a
function increment() external returns(uint) {
emit CallSuccess();
return x + 1;
}
}
Caller合约会演示如何调用一个代理合约,它也非常简单。但是要理解它,你需要先学习本教程的第22讲 Call和第27讲 ABI编码。
它有1个变量,2个函数:
proxy:状态变量,记录代理合约地址。
构造函数:在部署合约时初始化proxy变量。
increase():利用call来调用代理合约的increment()函数,并返回一个uint。在调用时,我们利用abi.encodeWithSignature()获取了increment()函数的selector。在返回时,利用abi.decode()将返回值解码为uint类型。
/**
* @dev Caller合约,调用代理合约,并获取执行结果
*/
contract Caller{
address public proxy; // 代理合约地址
constructor(address proxy_){
proxy = proxy_;
}
// 通过代理合约调用increment()函数
function increment() external returns(uint) {
( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()"));
return abi.decode(data,(uint));
}
}
部署Logic合约。
调用Logic合约的increment()函数,返回100。
部署Proxy合约,初始化时填入Logic合约地址。
调用Proxy合约increment()函数,无返回值。
调用方法:在Remix部署面板中点Proxy合约,在最下面的Low level interaction中填入increment()函数的选择器0xd09de08a,并点击Transact。
部署Caller合约,初始化时填入Proxy合约地址。
调用Caller合约increment()函数,返回1。
这一讲,我们介绍了代理模式和简单的代理合约。代理合约利用delegatecall将函数调用委托给了另一个逻辑合约,使得数据和逻辑分别由不同合约负责。并且,它利用内联汇编黑魔法,让没有返回值的回调函数也可以返回数据。下一讲,我们会介绍可升级代理合约。
代理合约虽然很强大,但是它非常容易出bug,用的时候最好直接复制OpenZepplin的模版合约。
>100 subscribers
>100 subscribers
No activity yet