Solidity-102摘要记录

课程主页:https://www.wtf.academy/zh/course/solidity102

  1. solidity函数重载一般指函数名相同,参数类型或个数不同,但返回指不同似乎也可以
  2. 库合约Library可以用于减少代码的复用性和减少gas
  3. 库合约和普通合约有几点不同:
    1. 不能存在状态变量
    1. 不能继承或被继承
    1. 不能接收以太币
    1. 不可以被销毁
  1. 库合约中的函数可见性如果被设置为publicexternal,则在调用函数时会触发一次delegatecall
  2. 库合约定义
library Strings {
    // ...
}
  1. 库合约的使用:1. 使用using for指令, using libraryA for typeB,添加完指令后,库A中的函数会自动添加为B类型成员变量,在调用的时候,这个变量会被当做第一个参数传递给函数。2. 通过库合约名称调用函数
using Strings for uint256;

  1. 导入合约import用法
    1. 通过源文件相对位置导入
import "./A.sol";
    1. 通过源文件网址导入网上的合约的全局符号
// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
    1. 通过npm的目录导入
import '@openzeppelin/contracts/access/Ownable.sol';
    1. 通过指定全局符号导入合于特定的全局符号
import {Strings} from './strings.sol';
  1. Solidity支持两种特殊的回调函数,receive()fallback()。主要用于:接收ETH;处理合约中不存在的函数调用
  • receive(): 在合约收到ETH转账时被调用,不需要function关键字,必须包含externalpayable。如果别人用sendtransfer发送ETH的话,gas会限制在2300receive()太复杂的可能会触发Out of gas错误;如果用call调用就可以自定义gas执行更复杂逻辑。/有些恶意合约可能故意失败或复杂代码,导致退款或转账逻辑的合约不能正常工作。/
evnt Received(address Sender, uint Value);
receive() external payable {
    emit Received(msg.sender, msg.value);
}
  • fallback(): 在函数调用合约不存在的函数时被触发。可用于接受ETH,也可以用于代理合约proxy contact。必须有external修饰,一般也用payable修饰。
event fallbackCalled(address Sender, uint Value, bytes Data);
fallback() external payable {
    emit fallbackCalled(msg.sender, msg.value, msg.data);
}
  • 合约接收ETH时,msg.data为空且存在receive()时,会触发receive()msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable

  1. Solidity有三种方法向其他合约发送ETH,分别是transfer(), send()call(),其中call()是比较被鼓励的用法
  2. 发送ETH
  • transfer
    • 用法是接收方地址.transfer(发送ETH数额)
    • transfer()gas限制是2300,所以fallback()receive()函数不能实现太复杂的逻辑
    • transfer()如果转账失败,会自动revert回滚交易
  • send
    • 用法是接收方地址.send(发送ETH数额)
    • send()gas限制是2300
    • send()如果转账失败,不会revert
    • send()的返回值是bool,代表着转账成功或失败,需要额外代码处理revert
error SendFailed();
function sendETH(address payable _to, uint256 amount) external payable {
    bool success = _to.send(amount);
    if(!success) {
        revert SendFailed();
    }
}
  • call
    • 用法是接收方地址.call{value:发送ETH数额}("")
    • call()没有gas限制,支持对方合约fallback()receive()函数实现复杂逻辑
    • call()如果转账失败,不会revert
    • call()的返回值是(bool, bytes),其中bool代表转账成功或失败,失败需要额外代码处理
error CallFailed();
function callETH(address payable _to, uint256 aount) external payable {
    (bool success,) = _to.call{value: amount}("");
    if(!success) {
        revert CallFailed();
    }
}

  1. 调用其它合约(已知合约代码或接口和地址的情况下)
    1. 传入合约地址
function callSetX(address _Address, uint256 x) external {
    OtherContract(_Address).setX(x);
}
    1. 传入合约变量
function callGetX(OtherContract _Address) external view returns(uint x) {
    x = _Address.getX();
}
    1. 创建合约变量
function callGetX2(address _Address) external view returns(uint x) {
    OtherContract oc = OtherContract(_Address);
    x = oc.getX();
}
    1. 调用合约并发送ETH
function setXTransferETH(address otherContract, uint256 x) payable external {
    OtherContract(otherContract).setX{value: msg.value}(x);
}

  1. calladdress类型的低级成员函数,用来与其他合约交互。它的返回值为(bool, bytes memory),分别对应call是否成功以及目标函数返回值.
  • callsolidity官方推荐的通过触发fallbackreceive函数发送ETH的方法
  • 不推荐用call来调用另一个合约,因为当你调用不安全合约函数时,主动权为不安全合约函数
  • 当不知道对方合约的源代码或ABI时,就没法生成合约变量,这时,仍可以通过call调用对方合约的函数
  1. call的使用规则如下:目标合约地址.call(字节码), 字节码利用结构化编码函数abi.encodeWithSignature获得: abi.encodeWithSignature("函数签名", 逗号分隔的具体参数),例如abi.encodeWithSignature("f(uint256,address)", _x, _addr),另外可以以在调用合约时指定交易发送的ETH数额和gas数额: 目标合约地址.call{value: 发送ETH数额, gas: gas数额}(字节码)
(bool success, bytes memory data) = _addr.call{value: msg.value}(abi.encodeWithSignature("setX(uint256)", x));
(bool success, bytes memory data) = _addr.call(abi.encodeWithSignature("getX()"));
uint256 res = abi.decode(data, (uint256))
  1. call调用不存在的函数时,那么目标合约的fallback函数会被触发

  1. calldelegatecall
  • 当用户A通过合约Bcall合约C的时候,执行的是合约C的函数,msg.senderB的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上, msg.value也是B给的
  • 当用户A通过合约Bdelegatecall合约C的时候,执行的是C的函数,但是上下文仍是合约B的,msg.senderA的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量
  1. delegatecall调用方式:目标合约地址.delegatecall(二进制编码),二进制编码利用结构化编码函数获得abi.encodeWithSignatre("函数签名", 逗号分隔的具体参数)delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额。
  2. delegatecall主要有两个应用场景:
  • 代理合约Proxy Contract(B)和Logic Contract(C)分开,代理合约存储所有相关的变量,并且保存逻辑合约的地址;所有函数在逻辑合约里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可
  • EIP-2535 Diamonds. /more/

  1. create合约就是new一个合约,并传入新合约构造函数所需的参数。如果构造函数时payable,可以创建时转入value数量的ETH
Contract x = new Contract{value: _value}{params}

create计算哈希地址

新地址 = hash(创建者地址, nonce)

  1. create2计算地址
新地址 = hash("0xFF", 创建者地址, salt, initcode)
  1. 如何使用create2创建合约
Contract x = new Contract{salt: _salt, value: _value}(params)
  1. 事先计算创建合约的地址
  • 构造的新合约不带参数
function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress) {
    require(tokenA != tokenB, 'IDENTICAL_ADDRESS');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
        bytes1(0xff),
        address(this),
        salt,
        keccak256(type(Pair).creationCode)
    )))));
}
  • 构造的新合约带参数
function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress) {
    require(tokenA != tokenB, 'IDENTICAL_ADDRESS');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
        bytes1(0xff),
        address(this),
        salt,
        keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
    )))));
}

  1. selfdestruct使用方式如下。_addr是接收合约剩余ETH的地址,_addr地址不需要有receive()fallback()也能接收ETH
selfdestruct(_addr);
  1. 在以太坊Cancun升级中,已经部署的合约无法被销毁,除非在同一笔交易中创建并销毁。但转账ETH还存在

  1. Solidity中,ABI编码有4个函数: abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelectorABI解码有1个函数: abi.decode
  • abi.encode: 将每个参数填充为32字节的数据,与合约交互,要用的就是abi.encode
function encode() public view returns(bytes memory result) {
    result = abi.encode(x, addr, name, array);
}
  • abi.encodePacked:将给定参数根据其所需最低空间编码。比如向省空间,并且不与合约交互的时候,可以使用abi.encodePacked,例如算一些数据的hash。因为不会做填充,所以不同的输入在拼接后可能会产生相同的编码结果,可能会带来潜在风险.
function encodePacked() public view returns(bytes memory result) {
    result = abi.encodePacked(x, addr, name, array);
}
  • abi.encodeWithSignature: 第一个参数为函数签名,当调用其他合约的时候可以使用.
function encodeWithSignature() public view returns(bytes memory result) {
    result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}
  • abi.encodeWithSelector: 与abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名``keccak哈希的前4个字节
function encodeWithSelector() public view returns(bytes memory result) {
    result = abi.encodeWithSelector(
        bytes4(keccak256("foo(uint256,address,string,uint256[2])")),
        x,
        addr,
        name,
        array
        );
}
bytes4 selector = contract.getValue.selector;

bytes memory data = abi.encodeWithSelector(selector, _x);
(bool success, bytes memory returnedData) = address(contract).staticcall(data);
require(success);

return abi.decode(returnedData, (uint256));
  • abi.decode: 用于解码abi.encode生成的二进制编码,将它还原成原本的参数
function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
    (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
}

  1. hash的性质
  • 单向性:从输入的消息到它的哈希的正向运算简单且唯一确定,而反过来非常难,只能靠暴力枚举
  • 灵敏性:输入的消息改变一点,对它的哈希值改变很大
  • 高效性:从输入的消息到哈希的运算高效
  • 均一性:每个哈希值被取到的概率应该基本相等
  • 抗碰撞性:
    • 弱抗碰撞性:给定一个消息x,找到另一个消息y,是的hash(x)==hash(y)是困难的
    • 强抗碰撞性:找到任意xy,使得hash(x)==hash(y)是困难的
  1. keccak256sha3sha3keccak标准化而来,但标准化之后,两者计算结果有点不一样

  1. 当调用智能合约时,本质上是向目标合约发送了一段calldata。在remix中发送一次交易后,可以在详细信息中看见input即为此次交易的calldata。发送calldata中的前4个字节是selector
  2. msg.dataSolidity中的一个全局变量,值为完整的calldata(调用函数时传入的数据)
this.functionName.selector
(bool success1, bytes memory data1) = address(this).call(abi.encodeWithSelector(0x3ec37834, 1, 0));

  1. try-catch只能被用于external函数或public函数或创建合约时contructor调用,基本语法如下:
try externalContract.f() {
    // call成功的情况下 运行一些代码
} catch {
    // call失败的情况下 运行一些代码
}

如果调用的函数有返回值,那么必须try之后声明returns(returnType val),并且在try模块中可以使用返回的变量。
30. catch模块支持捕获特殊的异常原因

try externalContract.f() returns(returnType){
    // call成功的情况下 运行一些代码
} catch Error(string memory /*reason*/) {
    // 捕获revert("reasonString") 和 require(false, "reasonString")
} catch Panic(uint /*errorCode*/) {
    // 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界
} catch (bytes memory /*lowLevelData*/) {
    // 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支
    // 例如revert() require(false) revert自定义类型的error
}
posted @ 2025-08-11 14:45  Nameless_gb  阅读(16)  评论(0)    收藏  举报