安全 - 目录

  • crytic
  • 发布于 2025-11-27 18:33
  • 阅读 7

该文档介绍了一个用于检测智能合约漏洞、确保符合相关标准以及提供编写不变性指南的属性库,该库包含了针对ERC20、ERC721、ERC4626和ABDKMath64x64等类型的token和库的168个代码属性,并提供了使用Echidna或Medusa进行模糊测试的步骤和配置示例,以及辅助函数和HEVM作弊码的支持。

目录

属性

此仓库包含 168 个代码属性,用于:

这些属性的目标是:

  • 检测漏洞
  • 确保遵守相关标准
  • 为编写不变量提供教育指导

这些属性可以通过单元测试或通过使用 EchidnaMedusa 进行模糊测试来使用。

使用模糊测试测试属性

  1. 安装 EchidnaMedusa

  2. 将属性导入到你的项目中:

    • 如果使用 Hardhat,请使用:npm install https://github.com/crytic/properties.gityarn add https://github.com/crytic/properties.git
    • 如果使用 Foundry,请使用:forge install crytic/properties
  3. 根据所需的测试,转到特定部分:

ERC20 测试

要测试 ERC20 token,请按照以下步骤操作:

  1. 集成
  2. 配置
  3. 运行

你可以查看 符合标准的 token不符合标准的 token 的输出。

集成

确定你想要进行内部测试还是外部测试。这两种方法都有优点和缺点,你可以在此处查看有关它们的更多信息。

对于内部测试,创建一个新的 Solidity 文件,其中包含 CryticERC20InternalHarness 合约。USER1USER2USER3 常量在 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();
    }
}

对于外部测试,创建两个合约:CryticERC20ExternalHarnessTokenMock,如下所示。 在 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.yamltests/crytic/erc20/echidna-external.yaml 用于 ERC20 的 Echidna 测试。我们建议相应地修改外部测试的语料库目录配置选项。

上述配置将以断言模式启动 Echidna 或 Medusa。目标合约将从地址 0x10000 部署,并且交易将从所有者以及两个不同的用户(0x200000x30000)发送。初始限制为 100000 个测试,但根据 token 代码的复杂性,可以增加此限制。最后,一旦我们的模糊测试工具完成模糊测试活动,语料库和覆盖率结果将可在指定的语料库目录中找到。

运行

Echidna

  • 对于内部测试:echidna . --contract CryticERC20InternalHarness --config tests/crytic/erc20/echidna-internal.yaml
  • 对于外部测试:echidna . --contract CryticERC20ExternalHarness --config tests/crytic/erc20/echidna-external.yaml

Medusa

  • 转到目录 cd tests/crytic/erc20
  • 对于内部测试:medusa fuzz --target-contracts CryticERC20InternalHarness --config medusa-internal.yaml
  • 对于外部测试:medusa fuzz --target-contracts CryticERC20ExternalHarness --config medusa-external.yaml
例子:一个符合标准的 token 的输出

如果被测 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! 🎉
...
例子:一个不符合标准的 token 的输出

对于此示例,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 测试

要测试 ERC721 token,请按照以下步骤操作:

  1. 集成
  2. 配置
  3. 运行

你可以查看 符合标准的 token不符合标准的 token 的输出。

集成

确定你想要进行内部测试还是外部测试。这两种方法都有优点和缺点,你可以在此处查看有关它们的更多信息。

对于内部测试,创建一个新的 Solidity 文件,其中包含 CryticERC721InternalHarness 合约。USER1USER2USER3 常量在 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() {
    }
}

对于外部测试,创建两个合约:CryticERC721ExternalHarnessTokenMock,如下所示。 在 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.yamltests/crytic/erc721/echidna-external.yaml 用于 ERC721 的 Echidna 测试。我们建议相应地修改外部测试的 corpusDir

上述配置将以断言模式启动 Echidna。合约将从地址 0x10000 部署,并且交易将从所有者和两个不同的用户(0x200000x30000)发送。初始限制为 100000 个测试,但根据 token 代码的复杂性,可以增加此限制。最后,一旦 Echidna 完成模糊测试活动,语料库和覆盖率结果将可在 tests/crytic/erc721/echidna-corpus-internal 目录中找到。

运行

运行 Echidna:

  • 对于内部测试:echidna . --contract CryticERC721InternalHarness --config tests/crytic/erc721/echidna-internal.yaml
  • 对于外部测试:echidna . --contract CryticERC721ExternalHarness --config tests/crytic/erc721/echidna-external.yaml

最后,完成后检查 tests/crytic/erc721/echidna-corpus-internaltests/crytic/erc721/echidna-corpus-external 中的覆盖率报告。

例子:一个符合标准的 token 的输出

如果被测 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! 🎉
...
例子:一个不符合标准的 token 的输出

对于此示例,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 测试

要测试 ERC4626 token,请按照以下步骤操作:

  1. 集成
  2. 配置
  3. 运行
集成

创建一个新的 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 中的覆盖率报告。

示例仓库可用于 HardhatFoundry

一旦一切正常运行,请考虑向你的 Vault ABI 添加内部测试方法,以允许测试像舍入这样的特殊边缘情况属性。有关更多信息,请参见 ERC4626 自述文件

ABDKMath64x64 测试

Solidity 智能合约编程语言没有任何用于处理十进制数字的内置功能,因此对于处理非整数值的合约,需要第三方解决方案。ABDKMath64x64 是一个定点算术 Solidity 库,它对 64.64 位数字进行运算。

64.64 位定点数是一种数据类型,由符号位、63 位整数部分和 64 位小数部分组成。由于 EVM 中没有直接支持小数,因此存储值的底层数据类型是 128 位有符号整数。

ABDKMath64x64 库使用定点数实现 19 个算术运算,以及整数类型和定点类型之间的 6 个转换函数

我们提供了许多与浮点数的基本数学属性相关的测试。要将这些测试包含到你的仓库中,请按照以下步骤操作:

  1. 集成
  2. 运行
集成

创建一个新的 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:可用于在模糊测试中记录值的事件。提供了 stringuint256address 记录器。在 Echidna 的断言模式下,当检测到断言违规时,将打印调用序列中发出的所有事件。
  • assertXxx:断言满足某个条件,并记录违规。提供了用于相等、不相等、大于、大于等于、小于和小于等于比较的断言,并且支持用户提供的消息进行日志记录。
  • clampXxx:将 int256uint256 限制在特定范围内。提供了用于小于、小于等于、大于、大于等于和范围的钳位。

使用示例

日志

记录一个值以进行调试。当断言被违反时,将打印 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);

    // ...
  }

  // ...
}

HEVM 作弊码支持

自 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
crytic
crytic
江湖只有他的大名,没有他的介绍。