在计算机编程中,当在特定范围(代码块、方法或内部类)中声明的变量与在外部范围中声明的变量具有相同的名称时,就会发生变量隐藏。变量隐藏在多种计算机语言中都存在,并不仅仅是Solidity语言独有的特性。
在计算机编程中,当在特定范围(代码块、方法或内部类)中声明的变量与在外部范围中声明的变量具有相同的名称时,就会发生变量隐藏
。变量隐藏在多种计算机语言中都存在,并不仅仅是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变量的指代。
理解了上面的原因,很简单的两种修改方法:
constructor() public {
owner = msg.sender;
console.log("\\nBase constructor:%s",owner);
}
modifier onlyOwner() {
require(msg.sender==owner,"Only Owner can call the function");
_;
}
变量隐藏常出现的两类场景:
不同特定作用范围的变量
。继承关系
的多个合约中,不同合约中具有相同名称的变量。对于场景1, 在开发环境中,编辑器会提示如下的影子变量的风险。如在代码开发过程中,遇到下面的提示,建议移除影子变量或重命名影子变量。
对于 场景2,编译器不会有影子变量的提示,更需要依赖于仔细检查您的合约代码的存储变量的定义,尽量消除任何歧义。
Smart Contract Weakness Classification and Test Cases中对变量隐藏的解释
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!