Vyper 不可重入锁漏洞事后分析技术报告
编者按:Vyper 被黑的时间线和反思[1]从审计的角度重现并提醒开发者注意考虑项目的依赖,本文从开发的角度深度分析并总结了这次漏洞的前因后果
Vperlang 团队特别感谢 Omniscia 团队
2023 年 7 月 30 日,由于 Vyper 编译器(特别是 0.2.15、0.2.16 和 0.3.0 版本)中的一个潜在漏洞,多个 Curve.Fi 流动池被利用。虽然 v0.3.1 版本发现并修补了该漏洞,但当时并未意识到该漏洞对使用易受攻击编译器的协议的影响,也未明确通知这些协议。该漏洞本身是一个有问题的重入保护,在某些特定条件下可以被绕过,我们将在本报告中深入探讨。
虽然包括 Curve.Fi 官方报告在内的其他报告已经充分报道了黑客攻击事件本身,但我们还是想深入探讨一下 Vyper 编译器本身到底出了什么问题,为什么漏洞难以被发现,以及整个生态系统能从这些事件中学到什么。
如果你熟悉区块链领域以及 Vyper 的存在原因,我们建议你跳过背景部分,因为其中包含的基本信息很可能你已经了解。
背景
Vyper
Vyper 是以以太坊虚拟机(EVM)为目标的,面向合约的,特定领域的 pythonic 编程语言。Vyper 目标和原则是语言和编译器的简单性、安全性和可审计性。
EVM:一个单线程非并发的机器
在 EVM 上部署代码的一个常见问题是重入概念。与传统程序不同的是,"区块链程序" 的控制流会被让给正在执行的 "活动" 程序。"区块链程序" 也称为合约。
详细来说,我们可以认为所有区块链程序都在一个单线程上运行,不支持并发。每当一个程序调用其他程序时,整个控制流会传递给被调用的程序。
重入:一个广泛存在于 Web 3.0 的问题
这意味着,在外部调用期间原始调用者的执行流基本上被冻结,直到被调用程序执行结束,调用者才会重新回到原来的位置。这种方式会导致不同类型的漏洞,其中最著名的就是重入漏洞。
当控制流传递让给被调用合约时,被调用的合约可以在冻结时重新进入原始调用者。容易受到此类攻击的合约会在外部合约调用的重入时更新状态更新,这意味着它们被冻结时的状态已过时且不正确。
解决办法
生态里的应用提出了两种方法来对抗重入攻击,并从根本上使重入失效:Checks-Effects-Interactions(CEI)模式和重入防护。
Checks-Effects-Interactions(CEI)模式
CEI 模式是一种编程方法,它规定函数代码应首先执行安全检查(Checks),然后执行存储中的影响(Effects)- 修改状态,最后在函数结束时执行与外部合约的交互(Interactions)。
如果严格遵守这一模式,"交互"(即传递控制流)期间的合约状态将是最新且正确的,从而使任何可能的重入合约利用变得不可能。
重入防护
在大多数情况下,CEI 模式就足够了,但 DeFi 生态系统是多方面的,函数往往依赖外部调用的结果来继续自己的执行。在这种情况下,CEI 模式就不适用了,必须设置重入防护。
安全是 Vyper 语言的核心原则之一, Vyper 决定通过特定的@nonreentrant函数修饰器在语言级别直接引入重入防护。自 Vyper 早期版本之一 v0.1.0-beta.9 发布以来,重入防护就一直是该语言的核心功能。
重入防护的核心功能是在两种状态(activated, inactive)之间设置一个存储值。当标记为@nonreentrant的函数被调用时,flag 会:
- 确保处于非激活状态
- 设置为激活状态(activated)
一旦函数调用结束后,flag :
- 设置为非激活状态(inactive)
有了这种机制,@nonreentrant用户就可以确保只有在函数结束后才能重新调用它,也就是说,无论执行何种外部调用,都不会发生重入。还有更复杂的重入攻击形式存在(如view重入、跨合约重入),但就本漏洞而言,基本情况才是最重要的。
Vyper 漏洞历史时间线
@nonreentrant基于标签的重入防护
自引入以来,@nonreentrant一直支持设置 ``[2],这与只在合约级全局应用的不可重入锁相比,它提供了更大的灵活性。
一个简单的实现是用mapping来获取key并设置相关的重入 flag,但这种方法会因mapping查找的keccak256gas 消耗而产生额外费用。
由于 Vyper 是一种不向用户提供原始存储访问的语言,因此在编译时它会完全了解合约使用的所有存储 slot 。因此,Vyper 会负责分配存储 slot ,包括确保存储变量和重入 key 锁的 slot 不会重叠。
PR#1264[3]在 Vyperv0.1.0-beta.9版本中引入了这个功能,用了一种简单的方法来确保不重叠,即在合约原始 slot 的特定偏移量(准确的说是 0xFFFFFF)处存储重入 flag 。
重构编译器
在开发新功能的同时,从 2018 年开始,Vyper 编译器开始了长达数年的重构工作[4],将当时的单通道架构重构为多通道架构,该架构将类型检查和语义分析的关注点分离到前端,与代码生成后端不同。与大多数大型重构项目一样,这项工作是渐进和零碎的,与其他错误修复和功能开发同时进行,直到 2023 年的PR#3390[5]才最终完成。
位置优化: 更智能地分配存储 slot
PR#2308[6]是 Vyperv0.2.9版本的一部分,其目的是在处理完合约的常规存储变量的所有 slot 后,利用第一个可用的未分配存储 slot ,而不是从0xFFFFFF常量开始作为重入锁 flag ,从而更智能地分配存储空间。这将节省字节码空间,因为在字节码中,加载或存储不可重入键的存储槽位置可以使用更少的字节。
避免损坏:正确的抵消计算
上述v0.2.9版本的 PR 运行良好,只要在(物理的)存储布局前面按顺序分配的变量不跨越多个顺序 slot ,就能保证重入防护 flag slot 和存储 slot 之间没有重叠。
由于 Vyper 语言和代码库当时正在进行重大重构,PR#2361[7](v0.2.13版本的一部分)引入了一种更有效的方法,优化合约中存储跨多个存储 slot (32 字节)的变量。作为更大规模重构工作的一部分,它还将常规存储变量的槽计算从代码生成(codegen)后端通道到了新的前端通道中,但保留了重入 key 的 slot 计算。由于重入 key 的 slot 计算依赖于常规存储变量的分配结果,因此最终在前端和代码生成(codegen)通道之间保留了两种不同的常规存储变量分配器实现。这导致PR#2308[8]的偏移计算不正确,需要更新。
PR#2379[9]引入了这个更新(v0.2.14版本的一部分),其目的是通过考虑在存储中声明的变量的正确大小,而不是假设所有变量都占用一个 slot (在早期实现中确实如此),来正确计算重入 flag 的存储偏移量。不过,第二次更新仍有一个 bug,源于前端和代码生成(codegen) 分配器实现之间的差异,我们将在下文中加以说明。
由于这些 bug,v0.2.13和v0.2.14版本在发布后不久就被 "撤回(yanked)" 。
简而言之,"yanking "指的是为历史目的在版本库中提供标签,但不发布和提供下载。有关详细信息,请参阅PEP-592[10]。
决定性事件:v0.2.14版本中的 "重入防护损坏"
v0.2.14发布后不久,一位 Vyper 用户在 Vyper GitHub 代码库中打开了issue #2393[11],指出在 Yearn vault 代码升级到0.2.14时,重入防护测试失败。
截取该用户提交问题时Yearn 最新可用版本[12]的快照,用v0.2.14编译,并用EtherVM 反编译器[13]检查反编译后的字节码(伪代码),会发现存储偏移storage[0x2e]被用作@nonreentrant("withdraw")的 "flag",应用于Vault.vy文件的def deposit和def withdraw实例中。
然而,合约级别的managementFee变量使用了相同的存储偏移量,这可以通过评估managementFee()getter 函数和setManagementFee()setter 函数的反编译函数来验证,这两个函数将重复使用相同的存储偏移量。
用v0.2.13版本编译相同的代码库时发现,重入防护按预期运行,并且没有出现存储重叠。不过,v0.2.14版本的PR#2379[14]并未完全解决重入防护损坏问题。
v0.2.14版本中为@nonreentrant修饰器分配存储 slot 的代码仍然会在新的前端代码和当时的 codegen 分配器之间产生不正确的交互。由于前端和 codegen 分配器之间映射类型的分配策略不同,重入 slot 最终仍会与常规存储变量重叠。**v0.2.14版的数据损坏代码如下**:
defget_nonrentrant_counter(self,key):
"""
Nonrentrantlocksuseaprefixwithacountertominimisedeploymentcostofacontract.
We'reabletosettheinitialre-entrantcounterusingthesumofthesizes
ofallthestorageslotsbecauseallstorageslotsareallocatedwhileparsing
themodule-scope,andre-entrancylocksaren'tallocateduntillaterwhenparsing
individualfunctionscopes.Thisreliesonthedeprecated_globalsattribute
becausethenewwayofdoingthings(set_data_positions)doesn'texposethe
nextunallocatedstoragelocation.
"""
ifkeyinself._nonrentrant_keys:
returnself._nonrentrant_keys[key]
else:
counter=(
sum(v.sizeforvinself._globals.values()ifnotisinstance(v.typ,MappingType))
+self._nonrentrant_counter
)
self._nonrentrant_keys[key]=counter
self._nonrentrant_counter+=1
returncounter
将其与当时计算常规存储变量存储布局的前端代码进行比较。
available_slot=0
fornodeinvyper_module.get_children(vy_ast.AnnAssign):
type_=node.target._metadata["type"]
type_.set_position(StorageSlot(available_slot))
available_slot+=math.ceil(type_.size_in_bytes/32)
虽然这段代码可以正确地消耗key值,并为相同的key值生成相同的@nonreentrant存储偏移量,但它却错误地计算了存储偏移量。
具体来说,旧的分配器没有为MappingType条目(即HashMap)分配存储 slot ,而新的分配器则分配了存储槽。MappingType存储条目永远不会被写入,而是被编译器保留(参考:Issue 2436[15])。这导致了不可重入 key 分配器与前端分配器之间的不一致,从而导致了所报告的存储损坏。
引入漏洞:v0.2.15版中失效的重入锁
在v0.2.14被撤回之后,为了修正v0.2.14版本中的重入防护损坏问题,v0.2.15版本中的PR#2391[16]通过将重入 key 移动到常规存储变量前面进行物理分配,修复了之前提到的PR#2379[17]中引入的 bug。此外,为了减少此类问题再次出现的概率,该版本还将存储 slot 分配逻辑移至前端中与常规存储变量分配相同的函数中,从而完成了将存储 slot 分配逻辑从 codegen 通道中移除的工作。不过,这样做的同时,也删除了旧的self._nonreentrant_keys数据结构,更重要的是,删除了确保每个不可重入 key 只分配一个锁的相应逻辑:
ifkeyinself._nonrentrant_keys:
#-->SAFE.onlyallocateoneslotperkey<--
returnself._nonrentrant_keys[key]
实际漏洞出现在v0.2.15版本的以下代码中:
#Allocatestorageslotsfrom0
#notestorageisword-addressable,notbyte-addressable
storage_slot=0
fornodeinvyper_module.get_children(vy_ast.FunctionDef):
type_=node._metadata["type"]
iftype_.nonreentrantisnotNone:
#-->BUG!shouldchecknonreentrantkeynotalreadyallocated<--
type_.set_reentrancy_key_position(StorageSlot(storage_slot))
#TODOuseonebyte-orbit-perreentrancykey
#requireseitheranextraSLOADorcachingthevalueofthe
#locationinmemoryatentrance
storage_slot+=1
该漏洞是由于重入 key 的storage_slot偏移忽略了@nonreentrant(<key>)修饰器的实际<key>,而只是简单的为每个看到的@nonreentrant修饰器预留一个新 slot ,而不管用的是什么 "key"。
潜伏:v0.2.15、v0.2.16和v0.3.0
v0.2.15中引入的漏洞在v0.2.16和v0.3.0临时版本中未被检测到,原因是当时 Vyper 代码库中没有足够的测试来检测该漏洞,这段时间为 2021 年 7 月 21 日至 2021 年 11 月 30 日之间的 4 个月。
所有使用v0.2.15、v0.2.16和v0.3.0版本编译的 Vyper 合约都会受到重入防护功能故障的影响。
修复:v0.3.1版
v0.3.1版通过调整编译器为合约中每个变量分配数据 slot 的方式,解决了此漏洞。该漏洞在两个不同的 PR中得到修复。
PR#2439: 修复未使用的存储 slot
第一个部分修复漏洞的 PR 是PR#2439[18],其中包含以下描述:
这不是一个语义漏洞,而是一个优化漏洞 我们分配的 slot 比实际需要的多,导致 slot 出现 "漏洞"。分配器--已分配但未使用的 slot 。
这种描述实际上并没有清楚地说明问题所在。关于 "漏洞"的描述是通过观察编译输出的layout是如何为每个重入 key 生成单个slot值而得出的。为了更好地理解发生了什么,让我们来看看v0.3.0中的数据分配函数:
fornodeinvyper_module.get_children(vy_ast.FunctionDef):
type_=node._metadata["type"]
iftype_.nonreentrantisnotNone:
type_.set_reentrancy_key_position(StorageSlot(storage_slot))
#TODOthiscouldhavebettertypingbutleaveituntypeduntil
#wenaildowntheformatbetter
variable_name=f"nonreentrant.{type_.nonreentrant}"
ret[variable_name]={
"type":"nonreentrantlock",
"location":"storage",
"slot":storage_slot,
}
#TODOuseonebyte-orbit-perreentrancykey
#requireseitheranextraSLOADorcachingthevalueofthe
#locationinmemoryatentrance
storage_slot+=1
这段代码的问题在于,它将每个type_(即单个@nonreentrantkey)的重入 key 位置设置为storage_slot的最新值,并在每次迭代时递增。这意味着@nonreentrant(<key>)的相同实例会使用不同的storage_slot值,但variable_name的ret变量在每次迭代时都会被覆盖。
因此,编译器的layout输出包含单个nonreentrant.<key>条目和单个存储偏移,这意味着检查编译器的输出似乎只是简单的 "跳过" 连续的@nonreentrant(<key>)声明的存储 slot ,这与PR 最初的逻辑[19]是一致的。
v0.3.1版本中部分修补的,无漏洞代码:
fornodeinvyper_module.get_children(vy_ast.FunctionDef):
type_=node._metadata["type"]
iftype_.nonreentrantisNone:
continue
variable_name=f"nonreentrant.{type_.nonreentrant}"
#anonreentrantkeycanappearmanytimesinamodulebutit
#onlytakesoneslot.ignoreitafterthefirsttimeweseeit.
ifvariable_nameinret:
continue
type_.set_reentrancy_key_position(StorageSlot(storage_slot))
#TODOthiscouldhavebettertypingbutleaveituntypeduntil
#wenaildowntheformatbetter
ret[variable_name]={
"type":"nonreentrantlock",
"location":"storage",
"slot":storage_slot,
}
#TODOuseonebyte-orbit-perreentrancykey
#requireseitheranextraSLOADorcachingthevalueofthe
#locationinmemoryatentrance
storage_slot+=1
现在,代码会在第一次识别到重复的重入 key 时分配一个单一的storage_slot。但是,它不会在有相同偏移量的每个type_上调用set_reentrancy_key_position函数,这就意味着除了第一个外,其他任何@nonreentrant(<key>)条目都将使用 "未定义 "的存储偏移量。
这导致编译器在尝试编译带有@nonreentrant修饰器的合约时出现 panic 。为了纠正这一问题,有必要进一步修改,以确保所有@nonreentrant修饰器都能正确认识到它们需要操作的存储 slot 。
panic: 也就是说,编译器会直接出错,而不会生成任何代码。编译器 "panic" 虽然会让用户感到恼火,但被认为是一种 "安全" 错误,因为它不会生成代码。︎
PR#2514: 修复 在使用不可重入键时 codegen 失败的问题
PR#2514[20]是缓解@nonreentrant漏洞的最后一份 PR。具体来说,它扩展了上述代码段,以确保set_reentrancy_key_position函数被正确调用,并为给定的@nonreentrant锁分配正确的 slot。
v0.3.1版本 Vyper 的最终无漏洞代码如下:
fornodeinvyper_module.get_children(vy_ast.FunctionDef):
type_=node._metadata["type"]
iftype_.nonreentrantisNone:
continue
variable_name=f"nonreentrant.{type_.nonreentrant}"
#anonreentrantkeycanappearmanytimesinamodulebutit
#onlytakesoneslot.afterthefirsttimeweseeit,donot
#incrementthestorageslot.
ifvariable_nameinret:
_slot=ret[variable_name]["slot"]
type_.set_reentrancy_key_position(StorageSlot(_slot))
continue
type_.set_reentrancy_key_position(StorageSlot(storage_slot))
#TODOthiscouldhavebettertypingbutleaveituntypeduntil
#wenaildowntheformatbetter
ret[variable_name]={
"type":"nonreentrantlock",
"location":"storage",
"slot":storage_slot,
}
#TODOuseonebyte-orbit-perreentrancykey
#requireseitheranextraSLOADorcachingthevalueofthe
#locationinmemoryatentrance
storage_slot+=1
正如我们在上述片段中看到的,set_reentrancy_key_position现在可以正确调用每个nonreentrant的type_条目,而且只要在@nonreentrant(<key>)修饰器中指定了相同的key,就会正确使用相同的storage_slot。
此外,除了上述修复之外,PR 还包含了 Vyper 代码库中一个急需但遗漏的测试;一个专门检查跨函数重入的单元测试:
@external
@nonreentrant('protect_special_value')
defprotected_function(val:String[100],do_callback:bool)->uint256:
self.special_value=val
ifdo_callback:
self.callback.updated_protected()
return1
else:
return2
@external
@nonreentrant('protect_special_value')
defprotected_function2(val:String[100],do_callback:bool)->uint256:
self.special_value=val
ifdo_callback:
#callotherfunctionwithsamenonreentrancykey
#-->(revertexpectedhere)<--
Self(self).protected_function(val,False)
return1
return2
然而,虽然在编译器代码库中发现、修复并测试了该错误,但当时并未意识到其对正式合约的影响,也没有明确通知可能使用相关编译器版本的协议。
在@nonreentrant修饰器中重复使用的相同<key>值的概念只有一个目的:跨函数重入防护。在 Vyper0.3.1版本发布之前,Vyper 存储库中一直缺少这样的测试,这也是导致该漏洞被引入并长期未被发现的原因之一。
漏洞总结
- 受影响版本:v0.2.15、v0.2.16、v0.3.0
- 根本原因:对v0.2.13中引入的重入防护数据损坏问题的补救不当
- 漏洞简介:Vyper 合约中的所有@nonreentrant修饰器都将使用唯一的存储偏移量(无论其key如何),这意味着在使用易受影响版本编译的所有合约上都可能出现跨函数重入。
利用漏洞的条件
虽然漏洞本身很容易识别,而且在各种合约中都能观察到,但其要利用它却需要满足的一系列非常特殊的条件。具体来说:
- 使用以下任一 vyper 版本编译的.vy合约:0.2.15,0.2.16,0.3.0
- 使用@nonreentrant修饰器并带有特定key的主要函数,且未严格遵循 CEI 模式(即在存储更新前有对不可信任方的外部调用)。
- 使用相同key的次要函数,会受到主函数导致的不正常状态的影响
不幸的是,这些条件正是在Curve.Fi 流动性池[21]中被利用的条件,因为它们需要在敏感的存储更新之前,在函数中执行原生ETH的分发(在 EVM 上,这只能通过执行上下文转移 CALL来完成),原本这些函数应该受到正常运行的@nonreentrant的保护。
从技术上讲,还有其他方法可以发送以太币,但在本文撰写时并不适用。EIP-5920[22]可能是这方面的一个积极进展。︎
总结和启示
对于任何大型生产型软件项目来说,错误都是一个不幸而严峻的现实。我们能做的就是尽最大可能减少错误及其相关风险。
我们可以采取几个切实可行的步骤来提高使用 Vyper 编译的智能合约的正确性:
1、改进编译器的测试,包括继续提高覆盖率、将编译器输出与语言规范进行比较,以及利用形式化验证(FV)工具进行编译器字节码验证。2、为开发人员提供工具,使他们更容易采取多方面的方法来测试代码,包括源代码和字节码级测试。3、使用 Vyper 对协议进行更严密的双向反馈
但是,仅仅关注最新版本编译器的正确性是不够的;由于智能合约的不可更改性,使用 Vyper 过去版本编译的合约可能会存在风险。
因此,确保 Vyper 过去版本的安全是另一个重要的新焦点,我们将在未来投入大量资源。这与为最新版本引入新功能、提供错误修复和重构一样重要。
最终,我们希望从最近发生的事件中吸取教训,确保 Vyper 成为世界上最稳固、最安全的智能合约语言和编译器项目。因此,这些目标将得到我们团队内外各种与安全相关的新举措的支持,这些举措包括:
- 与 Codehawks 合作进行短期竞争性审计,重点关注 Vyper 的最新版本
- 与 Immunefi 合作开展短期和长期(开放式)漏洞赏金计划,涵盖 Vyper 编译器的所有版本
- Vyper 安全联盟,这是一个协调一致的多协议悬赏计划,旨在帮助发现当前和过去的编译器漏洞,这些漏洞会影响 Vyper 的实时 TVL 安全版本
- 与包括 ChainSecurity、OtterSec、Statemind 和 Certora 在内的多家审计公司合作,审查 Vyper 过去的版本,以确保大量实时 TVL 的安全,并帮助对编译器进行持续审查。
- 扩大团队;包括一个专门的安全工程角色,旨在全面改进 Vyper 的安全工具,包括内部工具和面向用户的工具
- 与为 Solidity 提供的现有安全工具包合作,使 Vyper 生态系统受益匪浅
- 设计语言规范,这将有助于正式验证和帮助编译器本身的测试工作
1.资讯内容不构成投资建议,投资者应独立决策并自行承担风险
2.本文版权归属原作所有,仅代表作者本人观点,不代表本站的观点或立场
您可能感兴趣
-
Curve 治理权变局:1700 万 CRV 拨款提案遭否,资本方成新决策核心原文作者:CM(X:@cmdefi)前几天,Curve的一项拨款提案被否了,内容是拨给开发团队(Swiss Stake AG) 17M $CRV 开发经费,Convex和Yearn都投了反对票,而且这
-
三巨头下注 1700 万美元,FIN 强势入局跨境支付原文标题:《Pantera、Sequoia、三星联手押注,FIN 要抢传统银行的饭碗?》 原文作者:KarenZ,Foresight News在当前的全球金融体系中,大额跨境转账仍饱受「到账慢、手续费
-
Matrixport 投研:经历数月谨慎后,比特币进入结构性博弈阶段自 10 月中旬以来,比特币持续回落,市场情绪明显转向谨慎。随着市场再次讨论“四年周期”,部分交易员据此推演,2026 年或仍处于承压阶段。但从近期结构变化来看,市场正在进入一个不同于单边下行的新阶段
-
7大机构展望下的加密行业:2026,会走向哪里?作者:Viee, Amelia, Denise I Biteye 内容团队过去这一年,加密市场悄然站上了新的十字路口。 美联储政策转向、叠加x402、预测市场、链上美股、证券代币化等新叙事崛起,市场不
-
AI驱动新纪元:SunAgent以AI智能交互中枢,重塑波场TRON链上交互新范式正当人工智能以前所未有的广度和深度重塑商业与社会时,追求效率的加密世界,迎来了一个关键时刻。Fortune Business Insights 上的一项研究显示,AI与区块链交汇的市场规模正以年复合增
-
AI 驱动新纪元:SunAgent 以 AI 智能交互中枢,重塑波场 TRON 链上交互新范式
作为波场TRON生态的智能调度中枢,SunAgent通过对话式统一入口,深度聚合核心协议,重构了链上交互体验。正当人工智能以前所未有的广度和深度重塑商业与社会时,追求效率的加密世界,迎来了一个关键时刻
-
RWA 叙事正在切换:为什么代币化黄金开始被反复提及?随着现实世界资产(RWA)逐步成为加密行业的重要叙事方向,市场关注点正在发生明显变化:讨论不再停留在“哪些资产可以被代币化”,而是开始转向一个更现实的问题——哪些资产真正有机会在链上长期跑通,并形成稳
-
比特币减半后的供给变化,已被数学规则永久锁定比特币第四次减半发生于 2024 年 4 月 20 日 比特币的第四次区块奖励减半发生在 2024 年 4 月 20 日,对应区块高度 840,000,区块奖励从 6.25 BTC 降至 3.125
- 成交量排行
- 币种热搜榜
OFFICIAL TRUMP
World Liberty Financial USDv
泰达币
比特币
以太坊
USD Coin
Solana
First Digital USD
瑞波币
币安币
狗狗币
莱特币
大零币
Avalanche
艾达币
FIL
UNI
OKB
CFX
DOT
SHIB
YGG
DYDX
HT