代币权限交给 Safe 多签后,怎么查签名人和几签几
项目方说代币权限交给了 Safe 多签,先查签名人、确认门槛和已启用的模块。几签几说明常规交易要多少人同意,模块还可能有另一套执行规则。

在区块浏览器上查代币管理权限时,owner、admin 这类函数如果返回的是一个合约地址,项目方又说“权限交给了多签”,这一步其实还没查完。接下来要弄清的是:这个多签有几个签名人、要几个人同意、签名人都是哪些地址,以及有没有不经签名就能让它动手的通道。
以 Safe 多签为例,这些都能在链上直接读出来。在 Etherscan 打开那个合约地址,进 Contract 标签里的 Read as Proxy:getOwners 返回签名人列表,getThreshold 返回常规签名交易最少要几个签名人同意,两个数放在一起就是“几签几”;getModulesPaginated 告诉你有没有启用绕开签名的模块。还在排队、没执行的交易只有一部分能提前看到,原因放在后面单独说。
owner 是合约地址,怎么判断是不是 Safe 多签
本文查的是通过 SafeProxy 部署的 Safe 多签账户,它的地址本身是一个代理合约。公开源码里的 Safe 智能账户源码仓库中的 SafeProxy.sol 把它描述为一个通用代理,靠套用一份共用主合约(singleton)的代码来执行所有交易。Safe 官方文档的架构说明也写明,singleton 不能直接当账户用,只能通过代理使用,账户读数保存在代理地址上,不能拿 singleton 地址的读数代替。
这对查法的影响很直接:签名人、门槛这些函数定义在 singleton 里,代理自己几乎不定义函数,所以要用 Etherscan 的 Read as Proxy,而不是普通的 Read Contract。代理合约是什么,在源码验证那篇里有一节讲过,这里只说操作。
- 在 Etherscan 搜索 owner 函数返回的那个地址,打开 Contract 标签。
- 找 Read as Proxy。没有的话,看页面上有没有 “Is this a proxy?” 按钮,点它再点 Verify,Etherscan 会尝试找出背后的实现合约;识别成功后才会出现 Read as Proxy 和 Write as Proxy 两个标签(见 Etherscan 对代理合约的说明)。
- 在 Read as Proxy 的函数列表里找
getOwners、getThreshold、nonce、VERSION。VERSION返回版本号,源码仓库 main 分支里写的是 1.5.0(2026年10月核对),早年部署的 Safe 会是更早的版本,个别函数可能不同。 - 记下 Read as Proxy 用的实现合约地址,去和 Safe 官方的 safe-deployments 仓库比对。这个仓库按版本、按网络列出了 Safe 各合约的部署地址。
第 4 步别省。函数名谁都能照抄,一个自己写的合约完全可以也叫 getOwners、getThreshold,里面的逻辑却是另一回事。Etherscan 在说明里也提醒,代理识别只是按字节码做的检测,并不替你确认实际运行的是什么代码,恶意实现可能就藏在不显眼的地方。实现合约地址在官方部署清单里对不上,下面讲的 Safe 规则就不能直接套用,只能回到那份合约自己的源码去看。
几签几在哪看:getOwners 和 getThreshold
这两个函数都不用填参数,在 Read as Proxy 里展开就有结果。它们定义在 Safe 源码的 OwnerManager.sol 里。按接口文件 IOwnerManager.sol 的注释,getOwners 返回签名人列表,getThreshold 返回常规签名交易需要的确认数,也就是门槛。

假设 getOwners 返回 5 个地址,getThreshold 返回 3,这就是 3/5:走常规签名入口的交易,至少需要这 5 个签名人中的 3 个确认。模块交易走另一条入口,不能用这个门槛直接判断。
Read as Proxy 里和本题有关的几个读取函数:
| 函数 | 要填什么 | 返回什么 | 回答哪个问题 |
|---|---|---|---|
getOwners | 不用填 | 一组地址 | 签名人地址有哪些、一共几个 |
getThreshold | 不用填 | 一个整数 | 至少要几个签名人同意 |
isOwner | 一个地址 | true 或 false | 某个地址是不是签名人 |
nonce | 不用填 | 一个整数 | 下一笔经签名执行的交易用哪个序号 |
getModulesPaginated | 起点地址、每页数量 | 模块地址数组和下一页指针 | 有没有绕开签名的模块 |
VERSION | 不用填 | 版本号文字 | 是哪个版本的 Safe 代码 |
改签名人和门槛:常规签名与模块要分开看
OwnerManager 里加签名人(addOwnerWithThreshold)、删签名人(removeOwner)、换签名人(swapOwner)、改门槛(changeThreshold)这四个函数都带 authorized 修饰符。SelfAuthorized.sol 里这个修饰符只做一件事:调用者必须是这个合约自己,否则交易回退。接口注释写得更直白,这些操作只能通过一笔 Safe 交易完成。
也就是说,只看签名这条路,3/5 的多签要换人或者降门槛,同样得先凑够 3 个签名。模块是例外,下面单独讲。另外,你读到的只是此刻的状态。Safe 文档提到签名人和门槛的变动都会触发合约事件,所以除了读当前值,也值得翻一下这个地址的事件记录,看最近有没有人被换掉、门槛有没有被调低。
签名人地址要逐个点进去看
getOwners 给的只是一串地址。Safe 文档写明,签名人可以是普通的外部账户(EOA),也可以是别的智能合约账户,甚至是 passkey。所以列表里每个地址都要打开看一眼:
- 如果它是合约,而且 Read as Proxy 里同样有
getOwners、getThreshold,那它很可能是另一个 Safe,也就是多签套多签。先核实它的实现,再查它自己的签名验证规则;合约签名人不一定都能一路追到普通地址。假设外层是 2/3,其中一个签名人是由两个普通地址控制的 1/2 Safe,且没有额外验证限制,内层任一签名人就能代表它确认外层交易,但外层仍需另一个签名人同意。 - 项目方如果公开说过某个地址是团队成员的签名地址,拿这个地址去调
isOwner,返回 true 才说明它真在签名人里。 - 项目方对外说 3/5,链上读出来是 2/5 或 1/5,以链上读数为准,并把这个出入当成要追问项目方的问题。
1/N 不提供“必须由多个 owner 共同确认”的保护。如果 N 个签名人都是普通地址,又没有 Guard 等额外限制,任一地址都能独自满足签名门槛,包括发起更换签名人、调整门槛的交易。若签名人是另一个智能账户,就要继续查它自己的授权规则,不能直接等同于一把私钥。
门槛够高也不等于稳。3/5 的前提是这 5 个地址背后是互相独立的人或设备。假设其中三个地址其实由同一个人保管、放在同一台电脑上,那么同一人就能凑齐门槛,共用设备失陷时也可能同时暴露三把钥匙。链上没法证明一个地址归谁,读数只能告诉你结构,独立性要靠项目方公开的签名人身份和分工说明来补;没有说明的,就把签名人是否独立记为尚未确认。
另一头是门槛等于总人数,比如三个普通地址组成的 3/3。丢失其中一把钥匙后,常规签名交易就凑不齐确认;能否恢复还要查是否配置了恢复模块。门槛低于总人数时,剩余签名人若仍够数,就能通过交易更换失去访问权的地址。对持币者来说,既要看谁能动用权限,也要看权限会不会因丢钥匙而停摆。
有没有绕开签名的口子:查模块(Module)
前面读出来的“几签几”算不算数,要看这一步。Safe 的模块是挂在账户上的扩展合约,ModuleManager.sol 顶部注释的说法是:模块是对 Safe 拥有不受限访问权的扩展,可以执行任意交易,恶意模块可以完全接管一个 Safe。官方文档的模块页给的建议是:只添加可信、经过审计的模块。
看源码里模块执行交易的入口 execTransactionFromModule,Safe 在这个入口核对模块是否已启用,不走常规 owner 签名门槛。当前 main 分支的实现还会调用已配置的 Module Guard;模块自己的代码也可能要求签名、额度或等待期。若这些检查允许模块让 Safe 调用自身的管理函数,就能满足 authorized 条件,进而修改签名人和门槛。读模块权限时,应对照该账户实际部署的版本。
读法:
- 在 Read as Proxy 里找到
getModulesPaginated,它有两个参数。 start填0x0000000000000000000000000000000000000001。按接口文件 IModuleManager.sol 的注释,这是列表的起点指针,源码里叫 SENTINEL_MODULES。pageSize填一个每页数量,比如 10,然后点 Query。- 返回两项:模块地址数组和
next。数组是空的、next是0x…0001,说明没有启用任何模块;next不是0x…0001,说明还有下一页,把它填回start再查一次。
想单独核对某个地址是不是模块,用 isModuleEnabled,填地址,返回 true 或 false。启用和停用模块的函数带有 authorized,要求 Safe 自己调用自己。通过常规签名交易操作时要达到门槛,但已有模块也可能获准完成这类调用,初始化时也能配置模块;不能据此断言每次添加模块都经过了 owner 联合签名。
列表不为空,不等于有问题,但每个模块地址都要打开看:源码有没有验证、合约是做什么的、有没有审计,审计报告怎么对上合约和版本见这篇。模块用途差别很大。假设某个模块允许管理员密钥单独发出任意调用,且没有其他限制,那么它就可能绕开读出来的 3/5,形成另一条控制账户的路径。
Guard 不会绕开签名,但能把多签卡住
Safe 还有一种扩展叫 Guard(交易守卫),作用方向和模块相反:在“几签几”之上再加限制,交易执行前后各检查一次。官方文档提醒,Guard 有完全阻止 Safe 执行交易的能力,Guard 本身出错,相应的交易入口就可能无法使用。
没有公开的 getGuard 按钮,不等于 Guard 地址无法读取。GuardManager.sol 里读取 Guard 的函数是 internal 的,源码注释解释说没留公开方法是因为合约字节码大小受限,可以用公开的 getStorageAt 按该版本的存储位置读取;如果 Read as Proxy 列出了这个函数,也能在那里查询。填入位置前要对照实现版本,不能把一个版本的常量直接套到另一个版本。Guard 需要和模块一起看:普通交易守卫限制签名交易,支持 Module Guard 的版本还可另外限制模块交易。
还没执行的交易,外人能提前看到吗
能看到一部分,不能指望全部。
Read as Proxy 里的 nonce 是交易序号。Safe.sol 在执行交易时用当前 nonce 计算交易哈希,用完再加 1,所以你读到的值,就是下一笔经签名执行的交易要用的序号。按这段代码,签名所对应的 nonce 必须与执行时的当前值一致;使用更大 nonce 签出的交易,要等前面的序号被消耗。
Safe 的待执行队列主要是链下记录。提出交易、收集签名可以借助 Safe 的交易服务(Transaction Service)完成,官方说明它支持在链下收集签名。这个服务的开发工具里有一个按 Safe 地址查询“等待签名人确认的多签交易”的接口 getPendingTransactions;API 密钥说明里写着不带密钥也能做有限的探索性调用,正式使用要申请密钥。项目方提交到这个服务、且服务对外提供的待执行交易,才可能在执行前被这样查到。
查不到的情况也有:
- 合约的
execTransaction只在执行那一刻接收并核对签名,签名在哪里收集它不管。签名人完全可以在别处凑齐签名、一次提交上链,这样的交易不一定出现在 Safe 交易服务的公开队列里。 - 模块发起的交易走
execTransactionFromModule,Safe 的这条入口不消耗上述 nonce,也不检查 owner 签名;模块内部是否另有排队或延时,要看模块实现。 - 就算看到了排队交易,Safe 的常规签名门槛本身也不提供固定等待期。还要满足当前 nonce、Guard 等执行条件,才可能执行。
所以单靠一个多签,持币者拿不到可靠的预警。想要一个确定能提前看到的窗口,得看它前面有没有时间锁。
多签前面还隔着一层时间锁时,看等待期
有的项目把代币 owner 设成一个时间锁合约,再让多签去操作时间锁。这样从代币往回查,会先碰到时间锁,再在时间锁的角色里找到多签地址。
以 OpenZeppelin 的 TimelockController 为例,一个操作要先排期,过了最短等待期才能执行。能直接读的有这几样:
getMinDelay:最短等待期,单位是秒。假设读出 172800,按 172800 ÷ 3600 = 48 换算,就是 48 小时。这个值能改,但改它本身也得作为一个操作走排期流程。- 谁有权排期:proposer 角色负责提出操作。读出
PROPOSER_ROLE的值,再用hasRole填入这个值和多签地址,返回 true 就说明多签是提案方。 - 排期留下的记录:每次排期都会触发
CallScheduled事件,里面带着目标合约、调用数据和等待时间。这是链上数据,执行之前任何人都能看到。
时间锁不改变要几个人同意,等待期从操作成功在链上排期时开始计算,不能从链下凑齐签名那一刻计时。还要检查等待期是否大于零,以及代币的其他管理或升级权限能否绕过它。时间锁的实现不止 OpenZeppelin 这一种,函数名对不上时,按那份合约自己的源码去找等待期在哪。
读出来的数,怎么换算成风险判断
- 门槛是 1:任一 owner 都能满足外层确认数;它背后是否还需要多人授权,继续查该 owner。
- 签名人里有合约:继续查它的验证逻辑、恢复机制和与其他签名人的重合关系,不能只把每层门槛相乘。
- 模块列表不是空的:弄清每个模块能做什么之前,“几签几”不能当成保护来算。
- 实现合约对不上 Safe 官方部署清单:前面关于 Safe 的规则都不能直接套用。
- 没有时间锁:权限操作上链之前,你多半等不到可靠的公开预警。
如果这个多签地址自己也持有这个代币,它拿了多少、占比多高,属于持币集中度的问题,可以接着查。
上面这套 Read as Proxy 查法针对 EVM 链上通过代理部署的 Safe。Solana 上没有这套合约,比如 Squads 是另一套独立的多签程序,它的 v4 代码仓库说明里列有时间锁、支出额度、角色等功能,成员和门槛的查法与本文不同,Etherscan 上这些函数名用不上。
读完多签之后常接着冒出来的问题
代币的 owner 已经是零地址了,还需要查多签吗?
对只有一个 owner 的合约,owner 返回 0x0000000000000000000000000000000000000000 一般表示这个角色已经被放弃,不存在多签可查。但有的合约把权限拆成多个角色,铸币、暂停、升级各有各的管理地址,可升级代理的升级权限也可能单独挂在别处。owner 放弃了,不代表其他角色也没了,要逐个角色查清楚,方法见《在区块浏览器上查代币管理权限》。
同一个多签地址在另一条链上,签名人也一样吗?
不一定。Safe 可以在多条链上部署出同一个地址,但每条链上都是各自独立的合约,签名人和门槛分开存储,部署之后也可以各改各的。要在代币实际所在那条链的浏览器上读,不要拿另一条链的读数代替。
Etherscan 上既没有 Read as Proxy,也没有 Is this a proxy 按钮,怎么办?
看这个合约的源码有没有验证、Read Contract 里有没有 getOwners 和 getThreshold。缺少这些按钮或函数,也可能只是代理尚未被识别、源码或 ABI 没有被浏览器收录,不能据此判定它不是 Safe。先向项目方要实现合约地址、部署版本和验证页面,再对照官方部署清单与链上代码。资料仍不足时,把签名人和门槛记为未确认;有 ABI 和节点读取工具的人仍可直接调用链上合约。
队列里看到的一笔交易后来没执行,是被取消了吗?
有可能,也可能还在等签名或其他执行条件。Safe.sol 执行常规交易时会消耗当前 nonce;若同一序号最后执行了另一笔交易,原交易原有的签名就不能再按那个 nonce 执行。只是从某个界面删除记录,并不等于链上取消;若 nonce 尚未使用,仍须结合签名有效性、Guard 和目标合约状态判断能否执行。