Skip to main content

Solidity 地址类型

地址类型(address)是 Solidity 独有的一种类型。它被用来存放账户地址。在给其他账户转账,或者与其他合约交互的时候都需要使用到地址类型。好比如你要向别人的银行账户转账时需要知道对方的账户号码一样,Solidity的地址类型也扮演着类似的角色。

Solidity的地址类型用关键字 address 表示。它占据20bytes (160bits),默认值为 0x0 ,表示空地址。地址类型可以再细分为两种:

  • address : 普通地址类型(不可接收转账)
  • address payable : 可收款地址类型(可接收转账)

这两种地址类型的主要的区别在于 address payable 能接受转账,而 address 不能。接下来我们先介绍完如何定义地址类型的变量,然后再介绍为什么要区分这两种地址类型。

定义地址类型变量

我们可以按照下面的格式定义地址类型变量:

定义地址类型变量
address addr = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990;
address payable addr_pay = payable(0x8306300ffd616049FD7e4b0354a64Da835c1A81C);
试一试 »

上面我们定义了两个地址类型变量:addraddr_pay。并且直接将它们初始化为一个地址常量(也称地址字面值)。在 Solidity 中使用「地址字面值」初始化「地址类型变量」的时候不需要加 "" 或者 ’’

注意到在定义 addr_pay 的时候我们使用了一个 payable() 的函数,这是用来将地址字面值显式转换成 address payable 类型的。下面会有个小节讨论地址类型的转换。

为什么要区分 address 和 address payable

到现在为止,我们已经认识了地址类型的基本作用就是用来存放账户地址的。但是始终有个问题萦绕在我们心中:addressaddress payable 看起来差不不大,为何要对它们进行区分?直接统一使用 address 类型,想要转账就转账,不想转账就不转不就可以了吗?

Solidity 之所以要进行这样的区分是为了提高合约安全性。避免转账进某些合约后转不出来了,永远锁住在里面。

在此之前,我们要先了解 Solidity 的账户是有两种的:外部账户(external owned address),简写为 EOA;以及合约账户(contract address),简写为 CA。其中 EOA 就是我们平常在 MetaMask 上创建的那些账户。而合约账户则是在部署合约完成后返回的合约账户地址。

当我们把 Ether 转进 EOA 账户后,只要我们控制了 EOA 的私钥,我们就可以把 Ether 再转出来。而在 CA 账户情况则大为不同。CA 账户是由合约控制的,合约只能执行它定义过的操作。所以我们必须要在合约实现一个函数,定义好如何将账户下的 Ether 转出才行。否则这些 Ether 会被永远锁住在 CA 账户里面。

因此每次要向 CA 账户转账的时候我们都必须问自己:这个合约有没有定义好了把 Ether 转出的逻辑。使用 address payable 就是明确告诉编译器你已经确认转账到这个地址是安全的。这提高了合约安全性,也更方便开发者进行 Debug。

类型转换

addressaddress payable 之间可以互相进行类型转换。主要遵循两条规则。

  1. address payable 可以隐式地被转换成 address
  2. address 需要显式地使用 payable(addr) 函数转换成 address payable

Untitled

隐式类型转换(address payable to address)
address payable addr_pay = payable(0x8306300ffd616049FD7e4b0354a64Da835c1A81C);
address addr = addr_pay; // **隐式类型转换**
试一试 »
显式类型转换(address to address payable)
address addr = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990;
address payable addr_pay = payable(addr); // **显式类型转换**
试一试 »

成员变量

地址类型有三个成员变量,分别为:

  • balance :该地址的账户余额,单位是Wei
  • code :该地址的合约代码,EOA 账户为空,CA 账户为非空
  • codehash :该地址的合约代码的hash值
获取成员变量值

下面展示了如何获取地址的成员变量。其中 this 代表的是当前合约。

function get_balance() public view returns(uint256) {
return address(this).balance; //获取地址账户余额
}

function get_code() public view returns(bytes memory) {
return address(this).code; //获取合约代码
}

function get_codehash() public view returns(bytes32) {
return address(this).codehash; //获取合约代码的hash值
}
试一试 »

成员函数

地址类型有五个成员函数:

  • transfer(uint256 amount): 向指定地址转账,不成功就抛出异常(仅address payable可以使用)
  • send(uint256 amount): 与 transfer 函数类似,但是失败不会抛出异常,而是返回布尔值 (仅address payable可以使用)
  • call(...): 调用其他合约中的函数
  • delegatecall(...): 与 call 类似,但是使用当前合约的上下文来调用其他合约中的函数,修改的也是当前合约的数据存储
  • staticcall(...): 于 call 类似,但是不会改变链上状态

transfer

函数签名

transfer(uint256 amount)

transfer 函数可以向目标地址转账。可以指定转账数量为 amount,单位为 Wei 。如果发送失败,直接 revert。执行 transaction 的 Gas 固定为2300。注意 transfer 函数仅 address payable 可以使用。

Untitled

send

函数签名

send(uint256 amount) returns (bool)

send 函数可以向目标地址转账。可以指定转账数量为 amount,单位为 Wei 。如果发送失败,返回 false 。注意到 sendtransfer 的区别是 send 返回 false ,而 transfer 直接 revert 。执行 transaction 的 Gas 同样固定为2300。注意 send 函数仅 address payable 可以使用。

Untitled

transfer和send应该使用哪一个

transfersend 都可以用来向另一个地址转账,那么我们应该选择用哪一个呢?答案是没有非常必要的理由,一律选 transfer 。因为 transfersend 的改进版,目的是在转账不成功的时候直接终止交易。而使用 send 时,你需要检查返回值看是否成功再决定做后续处理。有人可能忘记检查是否成功就进行下一步处理,导致合约有被攻击的风险。

但是根据最新的分析(2023年1月),这两个函数都不安全,都不推荐继续使用。更安全的方法是使用 call 函数来转账。如果你想要了解具体的原因,可以参看下面的这些讨论:

讨论内容链接
讨论transfer与send的区别https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage
讨论增加transfer来取代sendhttps://github.com/ethereum/solidity/issues/610
stackexchange讨论transfer是否安全https://ethereum.stackexchange.com/questions/78124/is-transfer-still-safe-after-the-istanbul-update
consensys建议停止使用transferhttps://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/

call

函数签名

call(bytes memory) returns (bool, bytes memory)

使用 call 函数你可以与合约地址进行交互,调用其函数,或者直接向其转账。它有两个返回值,第一个是 bool ,显示函数调用是否成功。第二个是 bytes memory ,这个返回值是调用对方合约所返回的结果。与 sendtransfer 所不同的是, call 函数可以指定Gas。使用 call 函数,我们也可以给其他地址转账,具体如何操作我们会在 call 函数的单独章节进行讨论。

Untitled

delegatecall

函数签名

delegatecall(bytes memory) returns (bool, bytes memory)

delegatecall 是实现代理模式的手段。通过使用 delegatecall 你可以让当前合约只使用给定地址的代码,而使用当前合约的存储(如状态变量,账户余额等)。Delegate这个词就有代表,委派的意思。例如A合约委派B合约做一些操作,B合约只是代劳这些操作,操作的最后状态变更都需要记录到A合约上面。基于 delegatecall 的这种特性,Openzeppelin提出了代理模式,让你可以升级你的合约。详情我们会在 delegatecall 的单独章节展开。

Untitled

staticcall

staticcallcall 非常类似。它们的唯一区别就是 staticcall 不会改变合约的状态(包括当前合约和外部合约),一旦在调用的过程中改变了合约的状态(包括状态变量变更,账户余额改变等),那么会直接终止交易。引入 staticcall 提高了合约的安全性,因为只要你使用了 staticcall,你就可以肯定调用任何外部合约的函数不会对状态产生任何影响。而在引入 stacticall 之前,这是要通过阅读外部合约的代码来确保的。

Untitled

小结

  1. Solidity 中的地址类型是用于转账和与其他合约交互的
  2. 地址类型用 address 表示,占据20bytes (160bits)。默认值为0x0
  3. 地址类型有两种:普通地址类型和可收款地址类型
  4. 可收款地址类型可以接受转账,而普通地址类型不能
  5. 可以使用 payable() 函数将地址字面值显式转换为可收款地址类型
  6. balance:可以获取地址余额
  7. transfer():可以向指定地址转账
  8. send():与 transfer() 函数类似,但是如果转账失败会抛出异常
  9. call():可以调用其他合约中的函数
  10. delegatecall():与 call() 函数类似,但是使用当前合约的上下文来调用其他合约中的函数
  11. staticcall(): 与 call() 函数类似,但是不会允许有改变状态变量的操作
  12. transfer()send() 函数只能在 address payable 类型中使用

参考资料

https://docs.soliditylang.org/en/v0.8.17/types.html#address https://docs.soliditylang.org/en/v0.8.17/units-and-global-variables.html#address-related] https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies https://eips.ethereum.org/EIPS/eip-214 https://cryptoguide.dev/post/guide-to-solidity's-staticcall-and-how-to-use-it