Allureluoli 二月FEBRUARY

区块链国赛笔记 06|Solidity 存证合约:存储、接口与工具库

把状态保存、业务入口和公共工具分开,是阅读题目和补全合约的起点。本篇保留不同版本的学习示例,不把它们拼成一个工程。

本文由个人备赛旧笔记整理,整理日期为 2026-11-03。命令、输出和测试记录属于当时的实验环境;本文未重新搭建整套环境验证。

阅读前先确认

链上历史可追溯不代表状态变量不能修改。extcodesize 也不能作为完整的可信身份判断。示例中缺少的外部接口和 Ownable 等依赖需要单独准备。

存储合约设计

存储合约设计

基本要求

  • 数据持久化

    • 数据(永存概念) 存储在区块链
    • 仅可修改状态不可篡改
  • 状态变量存储

    • uint256 public data;          // 简单数值
      mapping(address => uint) balances; // 映射结构
      string[] public records;      // 动态数组
      
      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

      - Gas成本考虑

      - 邮费昂贵

      基本结构

      - 存储层
      - 业务逻辑层
      - 权限控制层

      ```solidity
      pragma solidity ^0.4.25;

      contract SimpleStorage {
      // 合约所有者
      address public owner;

      // 存储的数据
      uint256 private storedData;

      // 修改数据的事件日志
      event DataStored(uint256 newValue, address indexed setBy);

      // 构造函数,设置合约部署者为所有者
      constructor() public {
      owner = msg.sender;
      }

      // 权限修饰符:仅所有者可调用
      modifier onlyOwner() {
      require(msg.sender == owner, "Only owner can call this function");
      _;
      }

      // 存储数据函数(仅所有者)
      function set(uint256 data) public onlyOwner {
      storedData = data;
      emit DataStored(data, msg.sender);
      }

      // 读取数据函数(公开)
      function get() public view returns (uint256) {
      return storedData;
      }

      // 转移所有权(仅所有者)
      function transferOwnership(address newOwner) public onlyOwner {
      require(newOwner != address(0), "Invalid address");
      owner = newOwner;
      }
      }

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

## 接口合约设计

### 接口合约设计

接口合约是只包含函数声明而不包含实现的抽象合约,用于定义合约之间的交互标准。

设计原则:

- 强制标准化:过接口约束外部合约的调用规范,例如代币协议(ERC20/ERC721)必须实现标准函数。
- 无状态性:接口内禁止定义状态变量或构造函数,仅声明函数签名。
- 全外部可见性:**外部可见性**:所有函数必须为 `external`;实现合约中可以用 public 函数实现对应的 external 接口。

- 明确职责分离
- 保持接口最小化

---

接口类型:

- 代币标准接口
- Defi协议接口
- Oracle 预言机接口

- 错误处理接口
- 跨链桥接口
- 多继承接口
- 治理接口
- 实用工具接口
- 跨链桥接口


接口代码示例

```solidity
// 支持多继承的Difi组合接口
interface IAvancedDefi is IERC20, IUniswapV2Pair {
// 闪电贷功能(代币和NFT混合抵押)
function flashLoan(
address asset,
uint256 amount,
bytes calldata data
) external payable returns (bool);

// 跨链调用扩展 (需要预言机)
function crossChainCall(
uint256 targetChainId,
address targetContract,
bytes calldata payload
) external payable;
}

ERC20标准合约

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// ERC20标准合约示例
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns(bool);

}

接口伪装攻击防御:
在实现合约中验证调用者接口的完整性:

1
2
3
4
5
6
7
require(

IInterface(target).supportsInterface(type(IStandard).interfaceId),

"Invalid interface"

);

工具合约解析

工具合约解析

WeBASE-Front 工具箱中自带有工具合约

以下是代码解析:

Address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.24;

library Address {


// 检查地址是否为合约
function isContract(address addr) internal view returns(bool) {
uint256 size;
assembly { size := extcodesize(addr) }
// 这里使用了 assembly 对 EVM 进行字节码级操作
// extcodesize是 EVM 内置函数,返回指定地址的合约代码大小
// 如果 size > 0 那说明这个地址的确是个合约
return size > 0;
}

function isEmptyAddress(address addr) internal pure returns(bool){
return addr == address(0); // 一目了然 检查合约是不是空地址
}
}

注意一点,使用这个函数时,在构造函数中调用会返回false,因为合约还未部署到链上。

LibString 这个library 有 690行 太长了,只挑几个解析

1
2
3
4
5
6
7
function bytesToString(bytes memory _bytes) internal pure returns (string memory){
bytes memory bytesArray = new bytes(_bytes.length);
for (uint256 i; i < _bytes.length; i++) {
bytesArray[i] = _bytes[i];
}
return string(_bytes); // ❌ 这里应该是return string(bytesArray);
}

有点难绷了,为啥第一个函数就出现了错误,我有点不信任这个工具合约了

fix

去除了 bytesToString() 中不必要的部分

修正了 bytes32ToString() 并对逻辑进行重写

原代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function bytesToString(bytes memory _bytes) internal pure returns (string memory){
bytes memory bytesArray = new bytes(_bytes.length); // 无用的复制
for (uint256 i; i < _bytes.length; i++) {
bytesArray[i] = _bytes[i];
}
return string(_bytes);
}
function bytes32ToString(bytes32 _bytes32) internal pure returns(string memory){
bytes memory bytesString = new bytes(32);
uint charCount = 0 ;
for(uint i = 0 ; i<32;i++){
byte char = byte(bytes32(uint(_bytes32) *2 **(8*i))); //错误的位操作** (8 * i)))
if(char !=0){
bytesString[charCount] = char;
charCount++;
}
}
bytes memory bytesStringTrimmed = new bytes(charCount);
for(uint j=0;j<charCount;j++){
bytesStringTrimmed[j]=bytesString[j];
}
return string(bytesStringTrimmed);
}

修正代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function bytesToString(bytes memory _bytes) internal pure returns (string memory){
return string(_bytes);
}
function bytes32ToString(bytes32 _bytes32) public pure returns (string) {
uint8 length;
while (length < 32 && _bytes32[length] != 0) {
length++;
}
bytes memory bytesString = new bytes(length);
for (uint i = 0; i < length; i++) {
bytesString[i] = _bytes32[i];
}
return string(bytesString);
}

已进行的测试:

1
2
3
4
5
6
7
function testLetterA() public pure returns (string result, string correct, bool isEqual) {
bytes32 input = bytes32(uint(65)); // 'A'
result = bytes32ToString(input);
correct = bytes32ToStringCorrect(input);
isEqual = keccak256(result) == keccak256(correct);
}

output:

1
2
3
4
5
[
"A",
"",
"false"
]

核对资料

接口检测只说明对方声称支持某个接口,不能替代授权检查;原笔记的“接口伪装攻击防御”片段仅可作为接口探测思路。

复现时的自查

  • 记录自己实际使用的版本、配置和缺失依赖。
  • 对照本篇关键步骤,保存命令或调用结果,并解释结果代表什么。
  • 对失败或与原记录不一致的情况保留错误信息,不把历史输出当成自己的验证结果。

返回系列导读 · 上一篇 · 下一篇

评论COMMENTS