SafeCastLib.sol 提供 31 个 uint256 → uintN 安全向下转型函数,以 require(x < 1 << N) 拦截 Solidity 静默截断的高位丢失,服务于 storage packing 场景的溢出防护。
版本说明:[solmate]:main分支 [commit: 89365b8],[forge-std]:v1.15.0
源码链接:https://github.com/RevelationOfTuring/solmate/blob/main/src/utils/SafeCastLib.sol
SafeCastLib 是 solmate 的工具库,提供带溢出检查的 uint256 向下转型函数。将 uint256 安全地转换为更小的 uintN 类型(N = 8, 16, 24, ..., 248),若值超出目标类型范围则 revert。
解决的核心问题:Solidity 允许 uint256 直接截断为更小的 uintN(如 uint128(x)),但截断是静默的——高位数据被丢弃而不产生任何错误。这在数值确实超出范围时会导致严重的逻辑 bug(如金额截断后变小)。SafeCastLib 在截断前加入边界检查,确保溢出时 revert 而不是静默截断。
不安全的直接截断:
uint256 x = 2**128;
uint128 y = uint128(x); // y == 0,高位静默丢弃
SafeCastLib 安全转换:
uint256 x = 2**128;
uint128 y = x.safeCastTo128(); // x 超出 uint128 范围,引发revert
| 适合 | 不适合 |
|---|---|
将 uint256 存入更小类型的 storage slot 前做溢出保护(如 Uniswap V2 的 uint112 reserve) |
已知值一定在目标范围内的常量转换 |
接收外部输入(用户传入 uint256)后收窄存储 |
对 gas 极致敏感且已有其他机制保证范围(如被 require 预先限制) |
| Storage packing 场景中确保写入值不超出打包宽度 | 需要 int256 → intN 或 int ↔ uint 转换(应使用 OZ SafeCast) |
SafeCastLib (library)
│
└── Functions(31 个 internal pure 函数,模式完全相同)
├── safeCastTo248(uint256) → uint248
├── safeCastTo240(uint256) → uint240
├── safeCastTo232(uint256) → uint232
├── ...(每 8 位一个,共 31 个)
├── safeCastTo16(uint256) → uint16
└── safeCastTo8(uint256) → uint8
特点:无 Events、无 Constructor、无 Storage、无继承——纯函数库,31 个函数模式完全同构。
31 个函数共享完全相同的两步模式,仅目标位宽 N 不同:
function safeCastToN(uint256 x) internal pure returns (uintN y) {
// 步骤 1:边界检查 — 若 x ≥ 2^N(超出 uintN 范围),revert(无错误信息)
require(x < 1 << N);
// 步骤 2:截断赋值 — 丢弃高位,保留低 N 位
y = uintN(x);
}
参数:
| 参数 | 类型 | 含义 |
|---|---|---|
x |
uint256 |
待转换的原始值 |
返回值 y |
uintN |
安全截断后的值(N = 8, 16, 24, ..., 248) |
设计决策:
x < 1 << N vs x <= type(uintN).max:两者语义完全等价。1 << N 在编译期被优化为常量 2^N,与 type(uintN).max + 1 相同。选择 1 << N 的写法更简洁require 无错误消息:revert 时返回空 data(0 字节),比带字符串的 require 或 custom error 更省 gas,但链下无法区分具体是哪个 cast 失败internal pure:编译时内联到调用合约,零函数调用开销31 个函数覆盖所有 8 位步长的 uintN 类型:
| 函数 | 目标类型 | 最大值 |
|---|---|---|
safeCastTo248 |
uint248 |
2^248 - 1 |
safeCastTo240 |
uint240 |
2^240 - 1 |
safeCastTo232 |
uint232 |
2^232 - 1 |
safeCastTo224 |
uint224 |
2^224 - 1 |
safeCastTo216 |
uint216 |
2^216 - 1 |
safeCastTo208 |
uint208 |
2^208 - 1 |
safeCastTo200 |
uint200 |
2^200 - 1 |
safeCastTo192 |
uint192 |
2^192 - 1 |
safeCastTo184 |
uint184 |
2^184 - 1 |
safeCastTo176 |
uint176 |
2^176 - 1 |
safeCastTo168 |
uint168 |
2^168 - 1 |
safeCastTo160 |
uint160 |
2^160 - 1 |
safeCastTo152 |
uint152 |
2^152 - 1 |
safeCastTo144 |
uint144 |
2^144 - 1 |
safeCastTo136 |
uint136 |
2^136 - 1 |
safeCastTo128 |
uint128 |
2^128 - 1 |
safeCastTo120 |
uint120 |
2^120 - 1 |
safeCastTo112 |
uint112 |
2^112 - 1 |
safeCastTo104 |
uint104 |
2^104 - 1 |
safeCastTo96 |
uint96 |
2^96 - 1 |
safeCastTo88 |
uint88 |
2^88 - 1 |
safeCastTo80 |
uint80 |
2^80 - 1 |
safeCastTo72 |
uint72 |
2^72 - 1 |
safeCastTo64 |
uint64 |
2^64 - 1 |
safeCastTo56 |
uint56 |
2^56 - 1 |
safeCastTo48 |
uint48 |
2^48 - 1 |
safeCastTo40 |
uint40 |
2^40 - 1 |
safeCastTo32 |
uint32 |
2^32 - 1 |
safeCastTo24 |
uint24 |
2^24 - 1 |
safeCastTo16 |
uint16 |
2^16 - 1 = 65535 |
safeCastTo8 |
uint8 |
2^8 - 1 = 255 |
为什么没有 safeCastTo256:uint256 → uint256 不需要转换。
为什么步长是 8:Solidity 仅支持 8 位倍数的 uintN 类型(uint8, uint16, uint24, ..., uint256),不存在 uint12 或 uint252。
| 位宽 | 场景 | 源码参考 |
|---|---|---|
uint160 |
与 address 位宽相同(20 字节) |
— |
uint128 |
Uniswap V3 流动性(UniswapV3Pool.liquidity) |
https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol |
uint112 |
Uniswap V2 储备量(reserve0/reserve1,与 uint32 打包进同一 slot) |
https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol |
uint96 |
Compound COMP 代币余额与投票权(balances/Checkpoint.votes) |
https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol |
uint64 |
时间戳(block.timestamp 虽为 uint256,但实际值远小于 2^64) |
— |
uint48 |
ERC-4337 UserOperation 有效期(validUntil/validAfter) |
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/account/utils/draft-ERC4337Utils.sol |
uint32 |
block.number、Uniswap V2 blockTimestampLast |
https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol |
uint24 |
Uniswap V3 手续费(fee,单位:百万分之一) |
https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Factory.sol |
uint8 |
decimals、角色编号等 |
— |
为什么要使用更小的类型?——Storage Packing。EVM 每个 storage slot 为 256 位,多个小类型变量可以打包进同一 slot,显著节省 gas:
// Uniswap V2 经典 storage packing 示例:
uint112 private reserve0; // ┐
uint112 private reserve1; // ├── 112 + 112 + 32 = 256 位 → 1 个 slot
uint32 private blockTimestampLast; // ┘
// 如果用 uint256:
uint256 private reserve0; // slot 0(独占)
uint256 private reserve1; // slot 1(独占)
uint256 private blockTimestampLast; // slot 2(独占)
// → 3 个 slot,每次 SLOAD 2100 gas × 3 = 6300 gas
// 打包后只需 1 次 SLOAD = 2100 gas
require + 赋值internal pure,编译时内联到调用合约require 条件中的 1 << N 编译为常量,运行时仅是一次比较 + 条件跳转LT + 1 次 JUMPI| 风险 | 说明 | 建议 |
|---|---|---|
| revert 无错误信息 | require(x < 1 << N) 失败时返回空 data,链下 debugger 和事件监控无法直接区分是哪个 cast 失败 |
在关键业务逻辑中,可在调用前加注释或封装带消息的 wrapper |
| 仅支持 uint256 → uintN | 不支持 int256 → intN、int ↔ uint 等转换 |
需要有符号类型转换时使用 OpenZeppelin SafeCast |
| 截断后的语义正确性 | 库只保证截断不丢失数据,但不保证截断后的值在业务逻辑中有意义 | 调用者仍需自行验证值的业务合理性(如金额不为 0、时间戳在合理范围内等) |
| 特性 | solmate SafeCastLib | OpenZeppelin SafeCast |
|---|---|---|
| 支持的转换 | 仅 uint256 → uintN |
uint256 → uintN + int256 → intN + int256 ↔ uint256 |
| 函数数量 | 31 个 | 64 个(31 + 31 + 2) |
| 错误处理 | require 无消息(revert 返回空 data) |
custom error(如 SafeCastOverflowedUintDowncast(bits, value)),revert 返回 4+ 字节 |
| revert 路径 gas | 更低(空 data) | 略高(编码 error selector + 参数) |
| 正向路径 gas | 相同(1 次比较 + 1 次 JUMPI) | 相同 |
| 链下可调试性 | 差(无法区分具体失败) | 好(error 携带位宽和原始值) |
| 覆盖位宽 | 8 位步长(uint8 ~ uint248) | 8 位步长(uint8 ~ uint248、int8 ~ int248) |
| 代码生成 | 手写 | 脚本自动生成(scripts/generate/templates/SafeCast.js) |
| 代码量 | ~194 行 | ~1200 行 |
| 边界检查方式 | require(x < 1 << N) |
if (value > type(uintN).max) revert ... |
如何选择:
只需要 uint256 向下安全转型,追求极简和最低 gas?
→ 选 solmate SafeCastLib
需要 int ↔ uint 互转、有符号类型向下转型、或需要详细的 revert 错误信息?
→ 选 OpenZeppelin SafeCast
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {SafeCastLib} from "solmate/utils/SafeCastLib.sol";
/*
* @title StakingPool — 使用 SafeCastLib 做 storage packing 的质押池
* @notice 展示 SafeCastLib 配合 storage packing 的典型用法
*/
contract StakingPool {
using SafeCastLib for uint256;
// 打包进同一 slot(128 + 64 + 64 = 256 位)
struct StakeInfo {
uint128 amount; // 质押金额
uint64 stakedAt; // 质押时间戳
uint64 lockUntil; // 锁定截止时间
}
mapping(address => StakeInfo) public stakes;
function stake(uint256 amount, uint256 lockDuration) external payable {
require(msg.value == amount, "WRONG_AMOUNT");
stakes[msg.sender] = StakeInfo({
// safeCastTo128:确保金额不超过 uint128 范围
// 若用户传入 > 2^128 - 1 的值,会 revert 而非静默截断
amount: amount.safeCastTo128(),
// safeCastTo64:block.timestamp 为 uint256,但实际值远小于 2^64
stakedAt: block.timestamp.safeCastTo64(),
// safeCastTo64:同理,截止时间也安全收窄
lockUntil: (block.timestamp + lockDuration).safeCastTo64()
});
}
function unstake() external {
StakeInfo memory info = stakes[msg.sender];
require(info.amount > 0, "NOT_STAKED");
require(block.timestamp >= info.lockUntil, "LOCKED");
delete stakes[msg.sender];
payable(msg.sender).transfer(info.amount);
}
}
storage packing 效果:
不使用 packing(3 个 slot = 3 × 2100 gas SLOAD):
slot 0: amount (uint256)
slot 1: stakedAt (uint256)
slot 2: lockUntil (uint256)
使用 packing + SafeCastLib(1 个 slot = 1 × 2100 gas SLOAD):
slot 0: [amount uint128][stakedAt uint64][lockUntil uint64]
└──── 128 bit ──┘└── 64 bit ──┘└── 64 bit ──┘ = 256 bit
节省:4200 gas / 次读取,6600 gas / 次写入(warm slot 差异更大)
Mock 合约:https://github.com/RevelationOfTuring/foundry-solmate/blob/main/src/utils/MockSafeCastLib.sol
全部 Foundry 测试合约:https://github.com/RevelationOfTuring/foundry-solmate/blob/main/test/utils/SafeCastLib.t.sol
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
