本文作者:小驹[1]
在计算机编程中,当在特定范围(代码块、方法或内部类)中声明的变量与在外部范围中声明的变量具有相同的名称时,就会发生变量隐藏。变量隐藏在多种计算机语言中都存在,并不仅仅是 Solidity 语言独有的特性。
我们把这种同一个合约可能存在多个具有相同名称的变量,这种变量称为影子变量。
Solidity 支持继承,继承的引入可能会给 Solidity 的状态变量进行影子变量的问题。如想像一下这种场景:
带有变量 x 的合约 A 可以继承同样定义了状态变量 x 的合约 B,这将导致 x 变量 的两个不同版本。其中一个从合约 A 访问,另一个从合约 B 访问。
这种场景下极容易出现变量隐藏的安全问题。
在 Solidity 编码中,变量隐藏常出现的场景包括:
在编写更复杂的合约系统时,要时刻注意上面的两种场景,可能会由于忽视并随后导致变量隐藏安全问题。对于这两种场景分别用下面的代码进行演示。
pragma solidity 0.4.24;
contract ShadowingInFunctions {
uint n = 2;
uint x = 3;
function test1() constant returns (uint n) {
return n; // Will return 0
}
function test2() constant returns (uint n) {
n = 1;
return n; // Will return 1
}
function test3() constant returns (uint x) {
uint n = 4;
return n+x; // Will return 4
}
}上面的三个函数的输出内容为:
instance deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
test1:0
test2:1
test3:4在这段代码中,涉及到变量 n 的地方有多个,总结起来代码中变量 n 有三种变量类型,分别是:
在 test1 和 test2 函数中,返回值类型的 n 覆盖了合约的状态变量 n。所以函数返回的内容都是返回值类型的 n 的值。
在 test3 函数中,位于函数内的局部变量 n 覆盖了合约状态变量 n。所以函数返回的内容是局部变量计算出来的 n 的值。
可以分析下面的代码,看能否看出哪里有问题?
演示代码 Children.sol:
代码的本意是:Children 合约继承自 Base 合约,同时 Children 合约的 withdraw 取款操作具有 onlyOwner 修饰符,期望withdraw取款操作只能由Children合约的部署者才能调用。
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.4.0;
import "hardhat/console.sol";
contract Base {
address public owner;
modifier onlyOwner() {
require(msg.sender==owner,"Only Owner can call the function");
_;
}
}
contract Children is Base {
uint public totalSupply = 100;
address public owner;
constructor() public {
owner = msg.sender;
console.log("\\nChildren constructor:%s",owner);
}
function withdraw(uint _amount) public onlyOwner {
totalSupply = totalSupply - _amount;
// console.log(“aaa”);
}
}下面是 attack.ts 模拟的测试过程,通过 npx hardhat run script/attack.ts 进行测试。模拟的操作为:
user1部署Children合约,并输出合约部署的地址,合约的 owner,totalSupply 信息。user1调用合约的withdraw方法。import { ethers, waffle } from "hardhat";
async function main() {
let user1, user2;
[user1, user2] = await ethers.getSigners();
const Children = await ethers.getContractFactory("Children",user1);
const children = await Children.deploy();
await children.deployed();
console.log("deploy at:%s", children.address);
console.log("contract owner:%s", await children.owner());
console.log("totalSupply:%s", await children.totalSupply());
await children.connect(user1).withdraw(10);
console.log("totalSupply:%s", await children.totalSupply());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});在 user1 调用 withdraw 方法时,却失败了。错误提示如下:

0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 地址是 Children 合约的部署者,却无法调用 withdraw 方法。
返回源代码分析,Base 合约和 Children 合约都具有 owner 状态变量,却只有 Base 合约有 onlyOwner 修饰符。
这样的结果就是导致调用Children合约的 withdraw 方法时,使用的是Base合约的 onlyOwner 的修饰符,而 Base 合约的 onlyOwner 的修饰符检查的自然是Base合约的owner,也就是 0x0000000000000000000000000000000000000000。因此就导致了 Children 合约无法调用 withdraw()方法。
所以说,能调用 Children 合约 withdraw()方法的地址只能是 0x0000000000000000000000000000000000000000 黑洞地址。
本质上还是因为父子合约中都使用了 owner 变量,使 owner 合约变成了影子变量,程序员在实现功能时混淆了 owner 变量的指代。
理解了上面的原因,很简单的两种修改方法:
给 Base 合约加上 owner 的赋值。
constructor() public {
owner = msg.sender;
console.log("\\nBase constructor:%s",owner);
}给 Children 合约加上 OnlyOwner 修饰符。
modifier onlyOwner() {
require(msg.sender==owner,"Only Owner can call the function");
_;
}变量隐藏常出现的两类场景:
不同特定作用范围的变量。继承关系的多个合约中,不同合约中具有相同名称的变量。对于场景 1, 在开发环境中,编辑器会提示如下的影子变量的风险。如在代码开发过程中,遇到下面的提示,建议移除影子变量或重命名影子变量。

对于 场景 2,编译器不会有影子变量的提示,更需要依赖于仔细检查您的合约代码的存储变量的定义,尽量消除任何歧义。
Smart Contract Weakness Classification and Test Cases 中对变量隐藏的解释
https://swcregistry.io/docs/SWC-119[2]
[1]
小驹: https://learnblockchain.cn/people/9625
[2]
https://swcregistry.io/docs/SWC-119: https://swcregistry.io/docs/SWC-119