跳转至

Solidity 地址类型 & 常见用法全攻略

1. Solidity 地址类型

地址类型(address)用于存储一个 20 字节的值(以太坊地址的大小)。它不仅仅是一个存储单位,还提供了与以太坊账户交互的多种方式,对于编写智能合约至关重要。

注:这些账户可以是外部账户(由用户控制的账户,拥有私钥)或合约账户(由合约代码控制的账户)。

Solidity 中有两种地址类型:

  • address:普通地址,不能接收 ETH,即不能调用 transfer / send / call{value: ...} 等方法;
  • address payable:能接收 ETH,支持调用 transfer / send / call{value: ...} 等方法;

注:与 ETH 相关的操作(转账、收款),都必须使用 address payable。

对于 address 类型,常用的属性包括:

  • balance:获取地址中的 ETH 余额;
  • code(Solidity ^0.8.0):获取地址中存储的合约代码(如果有的话),此属性在 Solidity 0.8.0 以后版本中新增;
  • codehash(Solidity ^0.8.0):获取地址的合约代码的哈希值(如果地址为合约地址),如果地址是外部账户或合约中没有代码,则返回零哈希;

对于 address payable 类型,除 address 类型的属性外,还包括三种与 ETH 转账相关的方法:

  • transfer(uint256 amount):固定 2300 gas 供对方 fallback/receive 使用,失败会直接 revert,新版本里因为 gas 政策不稳定,官方已经不推荐用 transfersend
  • send(uint256 amount) returns (bool):2300 gas 限制,返回一个布尔值表示成功与否,不会在失败时 revert,自己决定要不要 require
  • call(bytes memory data) returns (bool, bytes memory):向地址发送 ETH,可同时调用其合约函数,返回一个布尔值表示成功与否和返回数据。
函数 是否推荐 失败是否 revert Gas 限制
transfer ❌ 不推荐 ✔️ 自动 revert 2300 gas
send ❌ 不推荐 ❌ 返回 false 2300 gas
call{value:...} ✔️ 推荐标准 ❌ 需手动 require 无限制

需要转账时,建议使用 call 方法,因为 transfersend 方法存在 Gas 限制(只提供2300 Gas,这足以记录事件,但不足以进行更复杂的操作),而 call 比较灵活。

2. 地址与外部合约交互(接口类型转换)

2.1 为什么转换成接口类型?

以太坊世界里:

  • 每个合约 = 一段部署在链上的代码
  • 每个合约 = 一个独立的地址(address)

address 本质只是 20 字节数字,并没有“函数信息”,因此:

❌ address 不知道这个地址对应哪种合约

❌ address 不知道里面有哪些函数

❌ address 不能直接调用 transfer、approve、swap 等函数

例如:

address token = 0x...;
token.transfer(to, amount); // ❌ 编译错误

因为 address 类型没有 transfer 方法。

Solidity 是强类型语言,必须明确告诉它:这个 address 是什么类型的合约!

所以我们才需要:

✅ 先定义接口(Interface)——描述合约函数

✅ 再把 address 强制转换成接口类型

✅ 最后用接口来调用函数

2.2 地址转接口并调用函数(核心语法)

典型写法:IERC20(tokenAddress).transfer(to, amount)

代码含义:

  1. tokenAddress —— 这是 ERC20 合约部署在链上的地址;
  2. IERC20(tokenAddress) —— 告诉 Solidity:按 ERC20 接口的规范访问这个 address
  3. .transfer(...) —— 编译器检查参数类型、返回值;
  4. 编译后执行低级 call(ABI 编码,EVM 执行);

2.3 最常用场景:把 address 储存起来,然后交互

例如 DEX、Vault、Staking 合约都这么写:

contract Test {
  address public token; // 保存代币合约地址

  function setToken(address _token) external {
    token = _token;
  }

  function transferOut(address to, uint256 amount) external {
    // address → IERC20
    bool ok = IERC20(token).transfer(to, amount);
    require(ok, "transfer failed");
  }
}

为什么不能直接用 address?因为 address 没有 transfer 函数。

2.4 安全注意事项

1)确保转换的 address 是期望的合约地址

如果用户传入恶意地址:IERC20(fakeAddress).transfer(...),会调到错误逻辑 或 fallback,触发恶意代码。

所以常见做法:

  • ✔ 白名单;
  • ✔ constructor 里写死地址;
  • ✔ 使用 immutable 常量;
  • ✔ 仅 owner 可修改 token 地址;

2)使用 try/catch(适合外部合约不稳定时)

try IERC20(token).transfer(to, amount) returns (bool ok) {
  require(ok, "failed");
} catch {
  revert("token call failed");
}

3)永远不要用 address 再去调用未知合约的 send/transfer(旧写法)

旧写法有风险:payable(addr).transfer(amount); // 可能失败

改用:

(bool ok,) = payable(addr).call{value: amount}("");
require(ok);

总结:

步骤 目的
🔹 写接口 Interface 告诉 Solidity 这个合约有哪些函数
🔹 用 IERC20(tokenAddress) 把 address 转成接口类型
🔹 用接口访问函数 安全、类型检查、自动 ABI 编码
🔹 用地址保存合约地址 合约间交互

3. 地址 + 映射 / 数组:记账 & 关系

3.1 地址 + 映射

mapping(address => uint256) 是最常见的用法之一,例如 ERC20 的余额表:

mapping(address => uint256) public balanceOf;

function mint(uint256 amount) external {
    balanceOf[msg.sender] += amount;
}

多层 mapping(比如 allowance):

mapping(address => mapping(address => uint256)) public allowance;

function approve(address spender, uint256 amount) external {
    allowance[msg.sender][spender] = amount;
}

mapping 里的 key 用 address 非常自然,而且会自动初始化为 0,不存在则返回默认值。

3.2 地址 + 数组

address[],数组里的元素可以是 addressaddress payable,看你要不要给他们转账。

比如记录所有存款过的用户:

address[] public users;
mapping(address => bool) public hasDeposited;

function deposit() external payable {
    if (!hasDeposited[msg.sender]) {
        hasDeposited[msg.sender] = true;
        users.push(msg.sender);
    }
}

4. 特殊地址

msg.sender – 当前合约的调用者,可能是外部账户,也可能是另一个合约。

tx.origin – 交易最原始发起人,容易被「中间合约」骗调用,导致钓鱼攻击,权限控制用 msg.sender 就够了。

address(this) - 合约自己的地址。