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 政策不稳定,官方已经不推荐用transfer和send了;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 方法,因为 transfer 与 send 方法存在 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)
代码含义:
tokenAddress—— 这是 ERC20 合约部署在链上的地址;IERC20(tokenAddress)—— 告诉 Solidity:按 ERC20 接口的规范访问这个address;.transfer(...)—— 编译器检查参数类型、返回值;- 编译后执行低级 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[],数组里的元素可以是 address 或 address 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) - 合约自己的地址。