
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讲:数字签名。 数字签名一般有两种常见的重放攻击...
Share Dialog
Share Dialog
WTF Academy: wtf.academy

Subscribe to 0xAA

Subscribe to 0xAA
>100 subscribers
>100 subscribers
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity
这一讲,我们就将介绍时间锁和时间锁合约。代码由Compound的Timelock合约简化而来。

时间锁(Timelock)是银行金库和其他高安全性容器中常见的锁定机制。它是一种计时器,旨在防止保险箱或保险库在预设时间之前被打开,即便开锁的人知道正确密码。
在区块链,时间锁被DeFi和DAO大量采用。它是一段代码,他可以将智能合约的某些功能锁定一段时间。它可以大大改善智能合约的安全性,举个例子,假如一个黑客黑了Uniswap的多签,准备提走金库的钱,但金库合约加了2天锁定期的时间锁,那么黑客从创建提钱的交易,到实际把钱提走,需要2天的等待期。在这一段时间,项目方可以找应对办法,投资者可以提前抛售代币减少损失。
下面,我们介绍一下时间锁Timelock合约。它的逻辑并不复杂:
在创建Timelock合约时,项目方可以设定锁定期,并把合约的管理员设为自己。
时间锁主要有三个功能:
创建交易,并加入到时间锁队列。
在交易的锁定期满后,执行交易。
后悔了,取消时间锁队列中的某些交易。
项目方一般会把时间锁合约设为重要合约的管理员,例如金库合约,再通过时间锁操作他们。
时间锁合约的管理员一般为项目的多签钱包,保证去中心化。
Timelock合约中共有4个事件。
QueueTransaction:交易创建并进入时间锁队列的事件。
ExecuteTransaction:锁定期满后交易执行的事件。
CancelTransaction:交易取消事件。
NewAdmin:修改管理员地址的事件。
// 事件
// 交易取消事件
event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
// 交易执行事件
event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
// 交易创建并进入队列 事件
event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
// 修改管理员地址的事件
event NewAdmin(address indexed newAdmin);
Timelock合约中共有4个状态变量。
admin:管理员地址。
delay:锁定期。
GRACE_PERIOD:交易过期时间。如果交易到了执行的时间点,但在GRACE_PERIOD没有被执行,就会过期。
queuedTransactions:进入时间锁队列交易的标识符txHash到bool的映射,记录所有在时间锁队列中的交易。
// 状态变量
address public admin; // 管理员地址
uint public constant GRACE_PERIOD = 7 days; // 交易有效期,过期的交易作废
uint public delay; // 交易锁定时间 (秒)
mapping (bytes32 => bool) public queuedTransactions; // txHash到bool,记录所有在时间锁队列中的交易
Timelock合约中共有2个modifier。
onlyOwner():被修饰的函数只能被管理员执行。
onlyTimelock():被修饰的函数只能被时间锁合约执行。
// onlyOwner modifier
modifier onlyOwner() {
require(msg.sender == admin, "Timelock: Caller not admin");
_;
}
// onlyTimelock modifier
modifier onlyTimelock() {
require(msg.sender == address(this), "Timelock: Caller not Timelock");
_;
}
Timelock合约中共有7个函数。
构造函数:初始化交易锁定时间(秒)和管理员地址。
queueTransaction():创建交易并添加到时间锁队列中。参数比较复杂,因为要描述一个完整的交易:
target:目标合约地址
value:发送ETH数额
signature:调用的函数签名(function signature)
data:交易的call data
executeTime:交易执行的区块链时间戳。
调用这个函数时,要保证交易预计执行时间executeTime大于当前区块链时间戳+锁定时间delay。交易的唯一标识符为所有参数的哈希值,利用getTxHash()函数计算。进入队列的交易会更新在queuedTransactions变量中,并释放QueueTransaction事件。
executeTransaction():执行交易。它的参数与queueTransaction()相同。要求被执行的交易在时间锁队列中,达到交易的执行时间,且没有过期。执行交易时用到了solidity的低级成员函数call,在第22讲中有介绍。
cancelTransaction():取消交易。它的参数与
为了构造交易,我们要分别填入以下参数: address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime
target:因为调用的是Timelock自己的函数,填入合约地址。
value:不用转入ETH,这里填0。
signature:changeAdmin()的函数签名为:"changeAdmin(address)"。
data:这里填要传入的参数,也就是新管理员的地址。但是要把地址填充为32字节的数据,以满足以太坊ABI编码标准。可以使用hashex网站进行参数的ABI编码。例子:
executeTime:先调用getBlockTimestamp()得到当前区块链时间,再在它的基础上加个150秒填入。
时间锁可以将智能合约的某些功能锁定一段时间,大大减少项目方rug pull和黑客攻击的机会,增加去中心化应用的安全性。它被DeFi和DAO大量采用,其中包括Uniswap和Compound。你投资的项目有使用时间锁吗?
我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity
这一讲,我们就将介绍时间锁和时间锁合约。代码由Compound的Timelock合约简化而来。

时间锁(Timelock)是银行金库和其他高安全性容器中常见的锁定机制。它是一种计时器,旨在防止保险箱或保险库在预设时间之前被打开,即便开锁的人知道正确密码。
在区块链,时间锁被DeFi和DAO大量采用。它是一段代码,他可以将智能合约的某些功能锁定一段时间。它可以大大改善智能合约的安全性,举个例子,假如一个黑客黑了Uniswap的多签,准备提走金库的钱,但金库合约加了2天锁定期的时间锁,那么黑客从创建提钱的交易,到实际把钱提走,需要2天的等待期。在这一段时间,项目方可以找应对办法,投资者可以提前抛售代币减少损失。
下面,我们介绍一下时间锁Timelock合约。它的逻辑并不复杂:
在创建Timelock合约时,项目方可以设定锁定期,并把合约的管理员设为自己。
时间锁主要有三个功能:
创建交易,并加入到时间锁队列。
在交易的锁定期满后,执行交易。
后悔了,取消时间锁队列中的某些交易。
项目方一般会把时间锁合约设为重要合约的管理员,例如金库合约,再通过时间锁操作他们。
时间锁合约的管理员一般为项目的多签钱包,保证去中心化。
Timelock合约中共有4个事件。
QueueTransaction:交易创建并进入时间锁队列的事件。
ExecuteTransaction:锁定期满后交易执行的事件。
CancelTransaction:交易取消事件。
NewAdmin:修改管理员地址的事件。
// 事件
// 交易取消事件
event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
// 交易执行事件
event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
// 交易创建并进入队列 事件
event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
// 修改管理员地址的事件
event NewAdmin(address indexed newAdmin);
Timelock合约中共有4个状态变量。
admin:管理员地址。
delay:锁定期。
GRACE_PERIOD:交易过期时间。如果交易到了执行的时间点,但在GRACE_PERIOD没有被执行,就会过期。
queuedTransactions:进入时间锁队列交易的标识符txHash到bool的映射,记录所有在时间锁队列中的交易。
// 状态变量
address public admin; // 管理员地址
uint public constant GRACE_PERIOD = 7 days; // 交易有效期,过期的交易作废
uint public delay; // 交易锁定时间 (秒)
mapping (bytes32 => bool) public queuedTransactions; // txHash到bool,记录所有在时间锁队列中的交易
Timelock合约中共有2个modifier。
onlyOwner():被修饰的函数只能被管理员执行。
onlyTimelock():被修饰的函数只能被时间锁合约执行。
// onlyOwner modifier
modifier onlyOwner() {
require(msg.sender == admin, "Timelock: Caller not admin");
_;
}
// onlyTimelock modifier
modifier onlyTimelock() {
require(msg.sender == address(this), "Timelock: Caller not Timelock");
_;
}
Timelock合约中共有7个函数。
构造函数:初始化交易锁定时间(秒)和管理员地址。
queueTransaction():创建交易并添加到时间锁队列中。参数比较复杂,因为要描述一个完整的交易:
target:目标合约地址
value:发送ETH数额
signature:调用的函数签名(function signature)
data:交易的call data
executeTime:交易执行的区块链时间戳。
调用这个函数时,要保证交易预计执行时间executeTime大于当前区块链时间戳+锁定时间delay。交易的唯一标识符为所有参数的哈希值,利用getTxHash()函数计算。进入队列的交易会更新在queuedTransactions变量中,并释放QueueTransaction事件。
executeTransaction():执行交易。它的参数与queueTransaction()相同。要求被执行的交易在时间锁队列中,达到交易的执行时间,且没有过期。执行交易时用到了solidity的低级成员函数call,在第22讲中有介绍。
cancelTransaction():取消交易。它的参数与
为了构造交易,我们要分别填入以下参数: address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime
target:因为调用的是Timelock自己的函数,填入合约地址。
value:不用转入ETH,这里填0。
signature:changeAdmin()的函数签名为:"changeAdmin(address)"。
data:这里填要传入的参数,也就是新管理员的地址。但是要把地址填充为32字节的数据,以满足以太坊ABI编码标准。可以使用hashex网站进行参数的ABI编码。例子:
executeTime:先调用getBlockTimestamp()得到当前区块链时间,再在它的基础上加个150秒填入。
时间锁可以将智能合约的某些功能锁定一段时间,大大减少项目方rug pull和黑客攻击的机会,增加去中心化应用的安全性。它被DeFi和DAO大量采用,其中包括Uniswap和Compound。你投资的项目有使用时间锁吗?
queuedTransactionsCancelTransactionchangeAdmin():修改管理员地址,只能被Timelock合约调用。
getBlockTimestamp():获取当前区块链时间戳。
getTxHash():返回交易的标识符,为很多交易参数的hash。
/**
* @dev 构造函数,初始化交易锁定时间 (秒)和管理员地址
*/
constructor(uint delay_) {
delay = delay_;
admin = msg.sender;
}
/**
* @dev 改变管理员地址,调用者必须是Timelock合约。
*/
function changeAdmin(address newAdmin) public onlyTimelock {
admin = newAdmin;
emit NewAdmin(newAdmin);
}
/**
* @dev 创建交易并添加到时间锁队列中。
* @param target: 目标合约地址
* @param value: 发送eth数额
* @param signature: 要调用的函数签名(function signature)
* @param data: call data,里面是一些参数
* @param executeTime: 交易执行的区块链时间戳
*
* 要求:executeTime 大于 当前区块链时间戳+delay
*/
function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner returns (bytes32) {
// 检查:交易执行时间满足锁定时间
require(executeTime >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay.");
// 计算交易的唯一识别符:一堆东西的hash
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
// 将交易添加到队列
queuedTransactions[txHash] = true;
emit QueueTransaction(txHash, target, value, signature, data, executeTime);
return txHash;
}
/**
* @dev 取消特定交易。
*
* 要求:交易在时间锁队列中
*/
function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner{
// 计算交易的唯一识别符:一堆东西的hash
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
// 检查:交易在时间锁队列中
require(queuedTransactions[txHash], "Timelock::cancelTransaction: Transaction hasn't been queued.");
// 将交易移出队列
queuedTransactions[txHash] = false;
emit CancelTransaction(txHash, target, value, signature, data, executeTime);
}
/**
* @dev 执行特定交易。
*
* 要求:
* 1. 交易在时间锁队列中
* 2. 达到交易的执行时间
* 3. 交易没过期
*/
function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public payable onlyOwner returns (bytes memory) {
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
// 检查:交易是否在时间锁队列中
require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
// 检查:达到交易的执行时间
require(getBlockTimestamp() >= executeTime, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
// 检查:交易没过期
require(getBlockTimestamp() <= executeTime + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale.");
// 将交易移出队列
queuedTransactions[txHash] = false;
// 获取call data
bytes memory callData;
if (bytes(signature).length == 0) {
callData = data;
} else {
callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
}
// 利用call执行交易
(bool success, bytes memory returnData) = target.call{value: value}(callData);
require(success, "Timelock::executeTransaction: Transaction execution reverted.");
emit ExecuteTransaction(txHash, target, value, signature, data, executeTime);
return returnData;
}
/**
* @dev 获取当前区块链时间戳
*/
function getBlockTimestamp() public view returns (uint) {
return block.timestamp;
}
/**
* @dev 将一堆东西拼成交易的标识符
*/
function getTxHash(
address target,
uint value,
string memory signature,
bytes memory data,
uint executeTime
) public pure returns (bytes32) {
return keccak256(abi.encode(target, value, signature, data, executeTime));
}
queuedTransactionsCancelTransactionchangeAdmin():修改管理员地址,只能被Timelock合约调用。
getBlockTimestamp():获取当前区块链时间戳。
getTxHash():返回交易的标识符,为很多交易参数的hash。
/**
* @dev 构造函数,初始化交易锁定时间 (秒)和管理员地址
*/
constructor(uint delay_) {
delay = delay_;
admin = msg.sender;
}
/**
* @dev 改变管理员地址,调用者必须是Timelock合约。
*/
function changeAdmin(address newAdmin) public onlyTimelock {
admin = newAdmin;
emit NewAdmin(newAdmin);
}
/**
* @dev 创建交易并添加到时间锁队列中。
* @param target: 目标合约地址
* @param value: 发送eth数额
* @param signature: 要调用的函数签名(function signature)
* @param data: call data,里面是一些参数
* @param executeTime: 交易执行的区块链时间戳
*
* 要求:executeTime 大于 当前区块链时间戳+delay
*/
function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner returns (bytes32) {
// 检查:交易执行时间满足锁定时间
require(executeTime >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay.");
// 计算交易的唯一识别符:一堆东西的hash
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
// 将交易添加到队列
queuedTransactions[txHash] = true;
emit QueueTransaction(txHash, target, value, signature, data, executeTime);
return txHash;
}
/**
* @dev 取消特定交易。
*
* 要求:交易在时间锁队列中
*/
function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner{
// 计算交易的唯一识别符:一堆东西的hash
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
// 检查:交易在时间锁队列中
require(queuedTransactions[txHash], "Timelock::cancelTransaction: Transaction hasn't been queued.");
// 将交易移出队列
queuedTransactions[txHash] = false;
emit CancelTransaction(txHash, target, value, signature, data, executeTime);
}
/**
* @dev 执行特定交易。
*
* 要求:
* 1. 交易在时间锁队列中
* 2. 达到交易的执行时间
* 3. 交易没过期
*/
function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public payable onlyOwner returns (bytes memory) {
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
// 检查:交易是否在时间锁队列中
require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
// 检查:达到交易的执行时间
require(getBlockTimestamp() >= executeTime, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
// 检查:交易没过期
require(getBlockTimestamp() <= executeTime + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale.");
// 将交易移出队列
queuedTransactions[txHash] = false;
// 获取call data
bytes memory callData;
if (bytes(signature).length == 0) {
callData = data;
} else {
callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
}
// 利用call执行交易
(bool success, bytes memory returnData) = target.call{value: value}(callData);
require(success, "Timelock::executeTransaction: Transaction execution reverted.");
emit ExecuteTransaction(txHash, target, value, signature, data, executeTime);
return returnData;
}
/**
* @dev 获取当前区块链时间戳
*/
function getBlockTimestamp() public view returns (uint) {
return block.timestamp;
}
/**
* @dev 将一堆东西拼成交易的标识符
*/
function getTxHash(
address target,
uint value,
string memory signature,
bytes memory data,
uint executeTime
) public pure returns (bytes32) {
return keccak256(abi.encode(target, value, signature, data, executeTime));
}
No activity yet