该文档介绍了一个用于检测智能合约漏洞、确保符合相关标准以及提供编写不变性指南的属性库,该库包含了针对ERC20、ERC721、ERC4626和ABDKMath64x64等类型的token和库的168个代码属性,并提供了使用Echidna或Medusa进行模糊测试的步骤和配置示例,以及辅助函数和HEVM作弊码的支持。
此仓库包含 168 个代码属性,用于:
这些属性的目标是:
这些属性可以通过单元测试或通过使用 Echidna 或 Medusa 进行模糊测试来使用。
将属性导入到你的项目中:
npm install https://github.com/crytic/properties.git 或 yarn add https://github.com/crytic/properties.gitforge install crytic/properties根据所需的测试,转到特定部分:
要测试 ERC20 token,请按照以下步骤操作:
你可以查看 符合标准的 token 和 不符合标准的 token 的输出。
确定你想要进行内部测试还是外部测试。这两种方法都有优点和缺点,你可以在此处查看有关它们的更多信息。
对于内部测试,创建一个新的 Solidity 文件,其中包含 CryticERC20InternalHarness 合约。USER1、USER2 和 USER3 常量在 PropertiesConstants 合约中默认初始化为 echidna 发送交易的地址,并且 INITIAL_BALANCE 默认设置为 1000e18:
pragma solidity ^0.8.0;
import "@crytic/properties/contracts/ERC20/internal/properties/ERC20BasicProperties.sol";
import "./MyToken.sol";
contract CryticERC20InternalHarness is MyToken, CryticERC20BasicProperties {
constructor() {
// Setup balances for USER1, USER2 and USER3:
// 为 USER1、USER2 和 USER3 设置余额:
_mint(USER1, INITIAL_BALANCE);
_mint(USER2, INITIAL_BALANCE);
_mint(USER3, INITIAL_BALANCE);
// Setup total supply:
// 设置总供应量:
initialSupply = totalSupply();
}
}
对于外部测试,创建两个合约:CryticERC20ExternalHarness 和 TokenMock,如下所示。
在 CryticERC20ExternalHarness 合约中,你可以通过继承来指定要测试的属性。在 TokenMock 合约中,如果你的合约能够铸造或销毁 token,则需要修改 isMintableOrBurnable 变量。
pragma solidity ^0.8.0;
import "./MyToken.sol";
import {ITokenMock} from "@crytic/properties/contracts/ERC20/external/util/ITokenMock.sol";
import {CryticERC20ExternalBasicProperties} from "@crytic/properties/contracts/ERC20/external/properties/ERC20ExternalBasicProperties.sol";
import {PropertiesConstants} from "@crytic/properties/contracts/util/PropertiesConstants.sol";
contract CryticERC20ExternalHarness is CryticERC20ExternalBasicProperties {
constructor() {
// Deploy ERC20
// 部署 ERC20
token = ITokenMock(address(new CryticTokenMock()));
}
}
contract CryticTokenMock is MyToken, PropertiesConstants {
bool public isMintableOrBurnable;
uint256 public initialSupply;
constructor () {
_mint(USER1, INITIAL_BALANCE);
_mint(USER2, INITIAL_BALANCE);
_mint(USER3, INITIAL_BALANCE);
_mint(msg.sender, INITIAL_BALANCE);
initialSupply = totalSupply();
isMintableOrBurnable = true;
}
}
Echidna
创建以下 Echidna 配置文件
corpusDir: "tests/crytic/erc20/echidna-corpus-internal"
testMode: assertion
testLimit: 100000
deployer: "0x10000"
sender: ["0x10000", "0x20000", "0x30000"]
## Uncomment the following line for external testing
## 取消注释以下行以进行外部测试
##allContracts: true
Medusa
创建以下 Medusa 配置文件:
{
"fuzzing": {
"testLimit": 100000,
"corpusDirectory": "tests/medusa-corpus",
"deployerAddress": "0x10000",
"senderAddresses": ["0x10000", "0x20000", "0x30000"],
"assertionTesting": {
"enabled": true
},
"propertyTesting": {
"enabled": false
},
"optimizationTesting": {
"enabled": false
}
},
// Uncomment the following lines for external testing
// 取消注释以下行以进行外部测试
// "testing": {
// "testAllContracts": true
// },
"compilation": {
"platform": "crytic-compile",
"platformConfig": {
"target": ".",
"solcVersion": "",
"exportDirectory": "",
"args": ["--foundry-compile-all"]
}
}
}
要执行多个测试,请使用描述性路径保存文件,以标识每个文件或语料库属于哪个测试。例如,对于这些例子,我们使用 tests/crytic/erc20/echidna-internal.yaml 和 tests/crytic/erc20/echidna-external.yaml 用于 ERC20 的 Echidna 测试。我们建议相应地修改外部测试的语料库目录配置选项。
上述配置将以断言模式启动 Echidna 或 Medusa。目标合约将从地址 0x10000 部署,并且交易将从所有者以及两个不同的用户(0x20000 和 0x30000)发送。初始限制为 100000 个测试,但根据 token 代码的复杂性,可以增加此限制。最后,一旦我们的模糊测试工具完成模糊测试活动,语料库和覆盖率结果将可在指定的语料库目录中找到。
Echidna
echidna . --contract CryticERC20InternalHarness --config tests/crytic/erc20/echidna-internal.yamlechidna . --contract CryticERC20ExternalHarness --config tests/crytic/erc20/echidna-external.yamlMedusa
cd tests/crytic/erc20medusa fuzz --target-contracts CryticERC20InternalHarness --config medusa-internal.yamlmedusa fuzz --target-contracts CryticERC20ExternalHarness --config medusa-external.yaml如果被测 token 符合标准,并且在模糊测试期间没有属性会失败,则 Echidna 的输出应类似于以下屏幕:
$ echidna . --contract CryticERC20InternalHarness --config tests/echidna.config.yaml
Loaded total of 23 transactions from corpus/coverage
Analyzing contract: contracts/ERC20/CryticERC20InternalHarness.sol:CryticERC20InternalHarness
name(): passed! 🎉
test_ERC20_transferFromAndBurn(): passed! 🎉
approve(address,uint256): passed! 🎉
test_ERC20_userBalanceNotHigherThanSupply(): passed! 🎉
totalSupply(): passed! 🎉
...
对于此示例,ExampleToken 的 approval 函数被修改为不执行任何操作:
function approve(address spender, uint256 amount) public virtual override(ERC20) returns (bool) {
// do nothing
// 不做任何事
return true;
}
在这种情况下,Echidna 的输出应类似于以下屏幕,请注意,所有依赖 approve() 正常工作的功能都将中断其断言,并报告情况。
$ echidna . --contract CryticERC20ExternalHarness --config tests/echidna.config.yaml
Loaded total of 25 transactions from corpus/coverage
Analyzing contract: contracts/ERC20/CryticERC20ExternalHarness.sol:CryticERC20ExternalHarness
name(): passed! 🎉
test_ERC20_transferFromAndBurn(): passed! 🎉
approve(address,uint256): passed! 🎉
...
test_ERC20_setAllowance(): failed!💥
Call sequence:
test_ERC20_setAllowance()
Event sequence: Panic(1), AssertEqFail("Equal assertion failed. Message: Failed to set allowance") from: ERC20PropertyTests@0x00a329c0648769A73afAc7F9381E08FB43dBEA72
...
要测试 ERC721 token,请按照以下步骤操作:
你可以查看 符合标准的 token 和 不符合标准的 token 的输出。
确定你想要进行内部测试还是外部测试。这两种方法都有优点和缺点,你可以在此处查看有关它们的更多信息。
对于内部测试,创建一个新的 Solidity 文件,其中包含 CryticERC721InternalHarness 合约。USER1、USER2 和 USER3 常量在 PropertiesConstants 合约中默认初始化为 echidna 发送交易的地址。
pragma solidity ^0.8.0;
import "@crytic/properties/contracts/ERC721/internal/properties/ERC721BasicProperties.sol";
import "./MyToken.sol";
contract CryticERC721InternalHarness is MyToken, CryticERC721BasicProperties {
constructor() {
}
}
对于外部测试,创建两个合约:CryticERC721ExternalHarness 和 TokenMock,如下所示。
在 CryticERC721ExternalHarness 合约中,你可以通过继承来指定要测试的属性。在 TokenMock 合约中,如果你的合约能够铸造或销毁 token,则需要修改 isMintableOrBurnable 变量。
pragma solidity ^0.8.0;
import "./MyToken.sol";
import {IERC721Internal} from "@crytic/properties/contracts/ERC721/util/IERC721Internal.sol";
import {CryticERC721ExternalBasicProperties} from "@crytic/properties/contracts/ERC721/external/properties/ERC721ExternalBasicProperties.sol";
import {PropertiesConstants} from "@crytic/properties/contracts/util/PropertiesConstants.sol";
contract CryticERC721ExternalHarness is CryticERC721ExternalBasicProperties {
constructor() {
// Deploy ERC721
// 部署 ERC721
token = IERC721Internal(address(new CryticTokenMock()));
}
}
contract CryticTokenMock is MyToken, PropertiesConstants {
uint256 public counter;
bool public isMintableOrBurnable;
constructor() {
isMintableOrBurnable = true;
}
function burn(uint256 tokenId) public {
_burn(tokenId);
}
function _customMint(address to, uint256 amount) public {
for (uint256 i; i < amount; i++) {
_mint(to, counter++);
}
}
}
创建以下 Echidna 配置文件
corpusDir: "tests/crytic/erc721/echidna-corpus-internal"
testMode: assertion
testLimit: 100000
deployer: "0x10000"
sender: ["0x10000", "0x20000", "0x30000"]
如果你使用外部测试,你还需要指定:
allContracts: true
要执行多个测试,请使用描述性路径保存文件,以标识每个文件或语料库属于哪个测试。对于这些例子,我们使用 tests/crytic/erc721/echidna-internal.yaml 和 tests/crytic/erc721/echidna-external.yaml 用于 ERC721 的 Echidna 测试。我们建议相应地修改外部测试的 corpusDir。
上述配置将以断言模式启动 Echidna。合约将从地址 0x10000 部署,并且交易将从所有者和两个不同的用户(0x20000 和 0x30000)发送。初始限制为 100000 个测试,但根据 token 代码的复杂性,可以增加此限制。最后,一旦 Echidna 完成模糊测试活动,语料库和覆盖率结果将可在 tests/crytic/erc721/echidna-corpus-internal 目录中找到。
运行 Echidna:
echidna . --contract CryticERC721InternalHarness --config tests/crytic/erc721/echidna-internal.yamlechidna . --contract CryticERC721ExternalHarness --config tests/crytic/erc721/echidna-external.yaml最后,完成后检查 tests/crytic/erc721/echidna-corpus-internal 或 tests/crytic/erc721/echidna-corpus-external 中的覆盖率报告。
如果被测 token 符合标准,并且在模糊测试期间没有属性会失败,则 Echidna 的输出应类似于以下屏幕:
$ echidna . --contract CryticERC721InternalHarness --config tests/echidna.config.yaml
Loaded total of 23 transactions from corpus/coverage
Analyzing contract: contracts/ERC721/CryticERC721InternalHarness.sol:CryticERC721InternalHarness
name(): passed! 🎉
test_ERC721_external_transferFromNotApproved(): passed! 🎉
approve(address,uint256): passed! 🎉
test_ERC721_external_transferFromUpdatesOwner(): passed! 🎉
totalSupply(): passed! 🎉
...
对于此示例,ExampleToken 的 balanceOf 函数被修改为不检查 owner 是否为零地址:
function balanceOf(address owner) public view virtual override returns (uint256) {
return _balances[owner];
}
在这种情况下,Echidna 的输出应类似于以下屏幕,请注意,所有依赖 balanceOf() 正常工作的功能都将中断其断言,并报告情况。
$ echidna . --contract CryticERC721ExternalHarness --config tests/echidna.config.yaml
Loaded total of 25 transactions from corpus/coverage
Analyzing contract: contracts/ERC721/CryticERC721ExternalHarness.sol:CryticERC721ExternalHarness
name(): passed! 🎉
test_ERC721_external_transferFromUpdatesOwner(): passed! 🎉
approve(address,uint256): passed! 🎉
...
test_ERC721_external_balanceOfZeroAddressMustRevert(): failed!💥
Call sequence:
test_ERC721_external_balanceOfZeroAddressMustRevert()
Event sequence: Panic(1), AssertFail("address(0) balance query should have reverted") from: ERC721PropertyTests@0x00a329c0648769A73afAc7F9381E08FB43dBEA72
...
要测试 ERC4626 token,请按照以下步骤操作:
创建一个新的 Solidity 文件,其中包含 CryticERC4626Harness 合约。确保它使用测试 token(TestERC20Token)正确初始化你的 ERC4626 vault:
如果你使用的是 Hardhat:
import {CryticERC4626PropertyTests} from "@crytic/properties/contracts/ERC4626/ERC4626PropertyTests.sol";
// this token _must_ be the vault's underlying asset
// 此 token 必须是 vault 的底层资产
import {TestERC20Token} from "@crytic/properties/contracts/ERC4626/util/TestERC20Token.sol";
// change to your vault implementation
// 更改为你的 vault 实现
import "./Basic4626Impl.sol";
contract CryticERC4626Harness is CryticERC4626PropertyTests {
constructor () {
TestERC20Token _asset = new TestERC20Token("Test Token", "TT", 18);
Basic4626Impl _vault = new Basic4626Impl(_asset);
initialize(address(_vault), address(_asset), false);
}
}
如果你使用的是 Foundry:
import {CryticERC4626PropertyTests} from "properties/ERC4626/ERC4626PropertyTests.sol";
// this token _must_ be the vault's underlying asset
// 此 token 必须是 vault 的底层资产
import {TestERC20Token} from "properties/ERC4626/util/TestERC20Token.sol";
// change to your vault implementation
// 更改为你的 vault 实现
import "../src/Basic4626Impl.sol";
contract CryticERC4626Harness is CryticERC4626PropertyTests {
constructor () {
TestERC20Token _asset = new TestERC20Token("Test Token", "TT", 18);
Basic4626Impl _vault = new Basic4626Impl(_asset);
initialize(address(_vault), address(_asset), false);
}
}
创建一个最小的 Echidna 配置文件(例如 tests/echidna.config.yaml)
corpusDir: "tests/echidna-corpus"
testMode: assertion
testLimit: 100000
deployer: "0x10000"
sender: ["0x10000"]
使用 echidna . --contract CryticERC4626Harness --config tests/echidna.config.yaml 运行测试套件,并在完成后检查 tests/echidna-corpus 中的覆盖率报告。
一旦一切正常运行,请考虑向你的 Vault ABI 添加内部测试方法,以允许测试像舍入这样的特殊边缘情况属性。有关更多信息,请参见 ERC4626 自述文件。
Solidity 智能合约编程语言没有任何用于处理十进制数字的内置功能,因此对于处理非整数值的合约,需要第三方解决方案。ABDKMath64x64 是一个定点算术 Solidity 库,它对 64.64 位数字进行运算。
64.64 位定点数是一种数据类型,由符号位、63 位整数部分和 64 位小数部分组成。由于 EVM 中没有直接支持小数,因此存储值的底层数据类型是 128 位有符号整数。
ABDKMath64x64 库使用定点数实现 19 个算术运算,以及整数类型和定点类型之间的 6 个转换函数。
我们提供了许多与浮点数的基本数学属性相关的测试。要将这些测试包含到你的仓库中,请按照以下步骤操作:
创建一个新的 Solidity 文件,其中包含 ABDKMath64x64Harness 合约:
pragma solidity ^0.8.0;
import "@crytic/properties/contracts/Math/ABDKMath64x64/ABDKMath64x64PropertyTests.sol;
contract CryticABDKMath64x64Harness is CryticABDKMath64x64PropertyTests {
/* Any additional test can be added here */
/* 任何其他测试都可以添加到此处 */
}
使用 echidna . --contract CryticABDKMath64x64Harness --seq-len 1 --test-mode assertion --corpus-dir tests/echidna-corpus 运行测试套件,并在完成后检查 tests/echidna-corpus 中的覆盖率报告。
该仓库提供了一系列函数和事件,旨在简化 Echidna 中断言的调试和测试。常用的函数,例如整数钳位或不同类型的日志记录,可在 contracts/util/PropertiesAsserts.sol 中找到。
可用的辅助函数:
LogXxx:可用于在模糊测试中记录值的事件。提供了 string、uint256 和 address 记录器。在 Echidna 的断言模式下,当检测到断言违规时,将打印调用序列中发出的所有事件。assertXxx:断言满足某个条件,并记录违规。提供了用于相等、不相等、大于、大于等于、小于和小于等于比较的断言,并且支持用户提供的消息进行日志记录。clampXxx:将 int256 或 uint256 限制在特定范围内。提供了用于小于、小于等于、大于、大于等于和范围的钳位。记录一个值以进行调试。当断言被违反时,将打印 someValue 的值:
pragma solidity ^0.8.0;
import "@crytic/properties/contracts/util/PropertiesAsserts.sol";
contract TestProperties is PropertiesAsserts {
// ...
function test_some_invariant(uint256 someValue) public {
// ...
LogUint256("someValue is: ", someValue);
// ...
assert(fail);
// ...
}
// ...
}
断言相等,并记录违规:
pragma solidity ^0.8.0;
import "@crytic/properties/contracts/util/PropertiesAsserts.sol";
contract TestProperties is PropertiesAsserts {
// ...
function test_some_invariant(uint256 someValue) public {
// ...
assertEq(someValue, 25, "someValue doesn't have the correct value");
// ...
}
// ...
}
如果此断言失败,例如,如果 someValue 为 30,则以下内容将打印在 Echidna 中:
Invalid: 30!=25, reason: someValue doesn't have the correct value
无效:30!=25,原因:someValue 没有正确的值
确保函数经过模糊处理的参数在特定范围内:
pragma solidity ^0.8.0;
import "@crytic/properties/contracts/util/PropertiesAsserts.sol";
contract TestProperties is PropertiesAsserts {
int256 constant MAX_VALUE = 2 ** 160;
int256 constant MIN_VALUE = -2 ** 24;
// ...
function test_some_invariant(int256 someValue) public {
someValue = clampBetween(someValue, MIN_VALUE, MAX_VALUE);
// ...
}
// ...
}
自 2.0.5 版本以来,Echidna 支持 HEVM 作弊码。此仓库包含一个 IHevm.sol 合约,该合约公开了作弊码,以便轻松集成到被测合约中。
应谨慎使用作弊码,因为它们可能会以意外的方式改变执行环境,并可能引入误报或漏报。
使用 prank 模拟来自不同 msg.sender 的调用:
pragma solidity ^0.8.0;
import "@crytic/properties/contracts/util/IHevm.sol";
contract TestProperties {
// ...
function test_some_invariant(uint256 someValue) public {
// ...
hevm.prank(newSender);
otherContract.someFunction(someValue); // This call's msg.sender will be newSender
// 此调用的 msg.sender 将是 newSender
otherContract.someFunction(someValue); // This call's msg.sender will be address(this)
// 此调用的 msg.sender 将是 address(this)
// ...
}
// ...
}
可以在奖杯页面上找到使用这些属性发现的安全漏洞列表。
欢迎贡献!你可以在 CONTRIBUTING.md 文件中阅读有关贡献准则和目录结构的更多信息。
- 原文链接: github.com/crytic/proper...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!