Solidity分析仪
静态代码分析是在不执行代码的情况下通过检查代码进行调试的过程。
Solidity Analyzers
插件将三种分析工具整合在一起,对 Solidity 智能合约进行静态分析。每个工具都会检查安全漏洞和不良开发实践等问题。可从 Remix 的 “插件管理器 “中激活。
可以通过在 Remix
主页选项卡的特色插件部分点击 Solidity
图标来加载 Solidity Analyzers
。该按钮会加载以下插件:Solidity编译器、Solidity单元测试和静态分析器。
“Solidity分析仪” 使用这些工具:
Remix分析:基本分析工具
Solhint linter:用于代码和样式指南验证的 Solidity linter
Slither静态分析:全面的静态分析工具
注意: Slither 只能在 Remix 通过 [Remixd](remix.html)连接到本地计算机文件系统时使用。
如何使用
在运行分析之前,必须先编译合约 。
在面板顶部,选中要使用的工具。
错误和警告
默认情况下, Solidity Analyzers
将同时显示错误和警告。错误和警告的总数显示在该工具选项卡的标记中。
如果选中 “隐藏警告”,警告将被隐藏,你只能看到错误。
注意: Remix分析不会标记错误,只会显示警告,因此如果勾选 “隐藏警告”,Remix分析选项卡中将不会显示任何内容。
来自外部库的警告
默认情况下,不显示来自外部库的警告。 如果选中 “显示外部库的警告 “复选框,工具也会分析外部库的警告。
Slither
要使用该插件运行 Slither,需要使用 Remixd将 Remix IDE 连接到文件系统。 Remixd 运行后,Slither 会自动加载。
Solhint
Solhint 内核程序无需将 Remix 连接到文件系统即可运行。
Remix分析
Remix 分析有 4 个类别:安全”、”Gas与经济”、”ERC “和 “杂项”。
以下是每个类别下的模块列表,以及示例代码, 在开发过程中应该避免使用或非常小心地使用 :
类别:安全
交易来源:使用’tx.origin’
tx.origin仅在非常特殊的情况下有用。如果您将其用于身份验证,通常应该将其替换为 “msg.sender” ,否则你调用的任何合约都可以代表你执行操作。
示例:
require(tx.origin == owner);
检查影响:潜在的可重入错误
Checks-Effects-Interaction 模式的潜在违反可能导致重入漏洞。
示例:
// sending ether first
msg.sender.transfer(amount);
// updating state afterwards
balances[msg.sender] -= amount;
内联汇编:使用内联汇编
仅在极少数情况下建议使用内联汇编。
示例:
assembly {
// retrieve the size of the code, this needs assembly
let size := extcodesize(_addr)
}
区块时间戳:语义可能不清晰
now
不表示当前时间。now
是block.timestamp
的别名。block.timestamp
在一定程度上可以受到矿工的影响,请小心。
示例:
// using now for date comparison
if(startDate > now)
isStarted = true;
// using block.timestamp
uint c = block.timestamp;
低级调用:语义可能不清晰
尽可能避免调用低级别的call
、callcode
或delegatecall
。当send
不成功时,不会抛出异常,请确保您相应地处理失败情况。如果想让转移的以太币在交易失败失败时回滚,请使用transfer。
示例:
x.call('something');
x.send(1 wei);
区块哈希用法:语义可能不清晰
blockhash
用于访问最近256个区块的哈希值。矿工通过“总结”当前挖掘的区块中的信息来计算区块哈希。通过巧妙地总结区块中的信息,矿工可以尝试影响当前区块中交易的结果。
示例:
bytes32 b = blockhash(100);
自毁:小心调用合约
selfdestruct
可能会意外地阻止调用合约的操作。特别是当该合约计划被其他合约使用(例如库合约、交互合约)时,需要特别小心。被调合约的自毁操作可能会使其调用方处于无法操作的状态。
示例:
selfdestruct(address(0x123abc..));
类别:gas与经济
Gas费用:函数的Gas消耗过高
如果一个函数的gas需求高于block gas limit,它就不能被执行。 请避免在修改大面积存储的函数或操作中出现循环
示例:
for (uint8 proposal = 0; proposal < proposals.length; proposal++) {
if (proposals[proposal].voteCount > winningVoteCount) {
winningVoteCount = proposals[proposal].voteCount;
winningProposal = proposal;
}
}
关于This的本地调用:通过“this”调用本地函数
永远不要使用this
调用同一个合约中的函数,这样做只会比正常调用消耗更多的gas。
示例:
contract test {
function callb() public {
address x;
this.b(x);
}
function b(address a) public returns (bool) {}
}
动态数组的删除:适当使用 require/assert
在Solidity中,当对动态数组执行删除操作时,会生成代码来删除其中包含的每个元素。如果数组很大,此操作可能会超过区块的GAS限制并引发OOG异常。同样,嵌套的动态对象也可能产生相同的结果。
示例:
contract arr {
uint[] users;
function resetState() public{
delete users;
}
}
For循环遍历动态数组:根据动态数组的大小进行迭代
没有固定迭代次数的循环,例如依赖于存储值的循环,必须谨慎使用:由于区块gas限制,交易只能消耗一定量的gas。 循环中的迭代次数可能会超过区块gas限制,这可能会在某个点停止完整的合约。 此外,使用无界循环会产生大量可避免的 gas 成本。 仔细测试您最多可以将多少项传递给此类函数,以确保成功执行。
示例:
contract forLoopArr {
uint[] array;
function shiftArrItem(uint index) public returns(uint[] memory) {
for (uint i = index; i < array.length; i++) {
array[i] = array[i+1];
}
return array;
}
}
循环中转移ETH:在for/while/do-while循环中转移ETH
以太币支付不应循环进行。 由于区块gas限制,交易只能消耗一定量的gas。 循环中的迭代次数可能会超过区块gas限制,这可能导致整个合约在某个点停滞不前。 如果需要,请确保迭代次数较少并且您信任所涉及的每个地址。
示例:
contract etherTransferInLoop {
address payable owner;
function transferInForLoop(uint index) public {
for (uint i = index; i < 100; i++) {
owner.transfer(i);
}
}
function transferInWhileLoop(uint index) public {
uint i = index;
while (i < 100) {
owner.transfer(i);
i++;
}
}
}
类别:ERC
ERC20:’decimals’ 应该是 ‘uint8’类型
ERC20合约的decimals
函数应该返回uint8
类型。
示例:
contract EIP20 {
uint public decimals = 12;
}
类别:其它
Constant/View/Pure 函数:可能的Constant/View/Pure函数
它对那些可能应该是 constant/view/pure 的方法进行警告,但实际上并非如此。
示例:
function b(address a) public returns (bool) {
return true;
}
相似的变量名称:变量名称过于相似
它警告使用类似的变量名。
示例:
// Variables have very similar names voter and voters.
function giveRightToVote(address voter) public {
require(voters[voter].weight == 0);
voters[voter].weight = 1;
}
没有返回:’returns’的函数没有返回
它对那些定义了返回类型但从未显式返回值的方法进行警告。
示例:
function noreturn(string memory _dna) public returns (bool) {
dna = _dna;
}
守卫条件:适当使用“require”和“assert”
如果您永远不希望x在任何情况下为false(除了你代码中的错误),请使用 assert(x)
。如果由于无效的输入或错误的外部组件等原因,x可能为false,请使用rrequire(x)
。
示例:
assert(a.balance == 0);
结果未使用:操作的结果没有被使用
二元运算产生的值在下文中未使用。 这通常是由混淆的assignment (=) 和 comparison (==) 引起的。
示例:
c == 5;
or
a + b;
字符串长度:字节长度 != 字符串长度
字节和字符串长度不相同,因为字符串被采用UTF-8编码(根据ABI定义),因此一个字符不一定被编码为一个字节的数据。
示例:
function length(string memory a) public pure returns(uint) {
bytes memory x = bytes(a);
return x.length;
}
从动态数组中删除元素:对数组使用 ‘delete’ 会在数组中留下一个间隙
使用 delete 对数组进行删除操作,会在数组中留下一个间隙。这时数组的长度仍然保持不变。如果想要移除这个空白的位置,需要手动将后面的元素往前移,并更新数组的长度属性。
示例:
contract arr {
uint[] array = [1,2,3];
function removeAtIndex() public returns (uint[] memory) {
delete array[1];
return array;
}
}
数据截断:int/uint 值的除法会截断结果
整数值的除法再次产生整数值。 这意味着例如 10 / 100 = 0 而不是 0.1,因为结果又是一个整数。 这不适用于(仅)文字值的除法,因为它们产生有理常数。
示例:
function contribute() payable public {
uint fee = msg.value * uint256(feePercentage / 100);
fee = msg.value * (p2 / 100);
}
Remix-分析器
Remix-analyzer` 是在 Remix 分析工具下工作的库。
remix-analyzer
是一个NPM包。它可在支持 node.js 的解决方案中作为库使用。想了解使用方面的更多信息请查看 remix-analyzer repository。