Virtuals Protocol 是 Base 链上一个类似于 Pump.fun 的发币平台。这篇文章主要来讲讲 Virtuals 的合约代码,学习他们的整个发币流程和细节。
与 Pump 类似,Virtuals 上发币也分两个阶段,分别是内盘和外盘。内盘是指在合约内的 Bonding Curve 曲线上进行交易的阶段。外盘是指在 DEX,例如 Uniswap 上交易的阶段。当内盘销售结束时(一般是达到某市值,或者是预设数量的 Token 已经售完),合约将自动在 DEX 上创建交易对,此时要交易该 Token 便只能在 DEX 中进行。
Virtuals 中发行的币在内盘阶段只能通过 VITRUAL Token 购买。在 DEX 阶段,一般也是的 VIRTUAL-TOKEN 的交易对。这段时间 Virtuals 比较火,因此 VITRUAL 需求量大,涨势很好。
内盘部分主要涉及下面这些合约:
看过 Uniswap 代码的朋友应该对这一套架构比较熟悉,Virtuals 在内盘代码的架构上借鉴了 Uniswap 的逻辑。其中一个区别是,Virtuals 的内盘和外盘发行的是两个 Token。这里的 FERC20 是内盘阶段交易的 MEME Token,仅在内盘阶段使用,当进入到外盘后,会变成另一个 Token。用户可以以 1: 1 的比例将内盘的 MEME 兑换成外盘的 MEME。
用户操作接口在 Bonding
合约中,主要有下面三个方法:
先来看 launch
方法。
传入一些基本的 Token 信息。
发币是要收费的,之前是 10 VIRTUAL,最近改成了 100 VIRTUAL。传入的 purchaseAmount
中减去 100,剩余的数量就是发币者自己购买的初始数量。
initialSupply
是由管理员设置的,目前是 10 亿,也就是说目前所有通过 Virtuals 发行的 Token,总供应量都是 10 亿。此时发行的 MEME 的 owner 是当前 Bonding
合约。
这里的 maxTx
是一个限制内盘 MEME 每笔转账数量的变量,目前没有限制,可以先不管。
通过 factory
合约创建一个 Pair,也就是前面说的 FPair
。
类似于 Uniswap 的写法,记录 Token 对应的 Pair。
FPair
的构造方法:
注意 tokenA 总是新创建的 MEME,tokenB 总是 VIRTUAL。
再回到 Bonding
合约。
这里先计算出来一个 liquidity
变量,实际值是 6000 ether。然后我们再来看 addInitialLiquidity
的代码:
方法参数分别是:
再来看 Pair 合约的 mint
方法:
这里的计算就是 Bonding Curve 的核心。Virtuals 采用的是曲线模型是
X * Y = K
其中 K 就是上面代码中的 reserve0 * reserve1
,即 6000B ether。
也就是说内盘阶段的买卖都是在 X * Y = K
这条曲线上进行的,这和 Uniswap 是一致的。
注意这里的 mint
其实就是第一次生成 pair 的一个操作,同时初始化 reserve0 和 reserve1,k 的数值,并不是实际意义上的 mint token。
这时我们再回到 Bonding
的这行代码:
其实就是提供了公式中 X
和 Y
的初始值。
接着来看 launch
方法。
这部分主要是记录 MEME 和用户的相关信息。
最后这部分,实现了部署者购买初始数量的功能。注意这里的数量是已经减去部署费用之后的 VIRTUAL 数量。
这里的 router.buy()
方法最后会将购买到的 MEME 全部转给该合约,因此最后需要再将其转给 msg.sender
,即部署者。
router 合约的内容我们后面再看,先把 Bonding
合约的其它部分看完。
来看看 buy
方法的函数签名:
参数分别是要购买的 MEME 和数量,也就是说在内盘阶段,所有 MEME 的购买都是通过该方法进行。
这里校验的 trading
,在内盘阶段为 true,外盘阶段为 false,也就是说这里的 buy
是只能在内盘阶段被调用。
reserveA
和 reserveB
分别是 MEME 和 VIRTUAL 的数据,类似于 Uniswap 中的 reserve。
buy
方法的两个返回值分别是实际花费的 VIRTUAL 数量和购买获得的 MEME 数量。返回值的 amount1In
与参数的 amountIn
的区别是前者扣除了手续费。
接着是更新 MEME 的相关信息,这里省略:
最后这里,当 Pair 中 Token 的数据小于一个阈值,也就是 Pair 中的 Token 的余额已经所剩不多时,在 DEX(Uniswap)中开一个新的交易对,即开外盘。
sell
方法与 buy
方法大同小异,只是换了交易方向,大家自己看看理解就好。
整体逻辑比较简单,核心主要在于这部分:
这里的主要难点在于调用了 agentFactory
的两个方法:
initFromBondingCurve
可以简单理解为生成了一个流程对象,并返回该对象的 id。其将内盘 Pair 中的所有 VIRTUAL 余额全部转入了 agentFactory
合约中
executeBondingCurveApplication
中包含了创建新的外盘 MEME,添加流动性等逻辑。并给 Pair 合约 mint 了一些数量的外盘 MEME。
router 合约有这几个主要方法:
我们先来看 getAmountsOut
:
该方法的功能是计算买卖某 Token 时,能够获得多少数量。我们前面说过, reserveA
和 reserveB
分别是 MEME 和 VIRTUAL 的数据。那么当 assetToken_
传 VIRTUAL 的时候,代表买入(因为这里的逻辑是 reserveB 增多)。传其它地址的时候,代表卖出。
这里使用 X * Y = K
的公式,通过从 pair 中获得的 reserve 数据,来计算最终所能得到的数量。
buy
的代码如下:
购买的时候需要付 1% 的手续费。这里要注意的是,X * Y = K
公式中的 reserveA
和 reserveB
是存储在 Pair 合约中的,但实际变化数量的计算是在 router 合约的,也就是说 Pair 合约中只存储 reserve 的值,但是并没有计算过程。当变化的数量在 router 中通过 getAmountsOut
方法计算好之后被传入到 Pair 中通过 swap
方法进行增减。
与 buy
相似的逻辑。
注意这里的添加流动性并不是在 Uniswap 中添加,而是添加到内盘中的虚拟流动性,也就是说只是为了提供了 XYK
公式的初始数量。十亿的内盘 MEME 转入 Pair 合约,但是 VIRTUAL 并没有转入,只是提供了 6000 这样一个数量而已。
mint
方法我们前面也已经看过,主要是记录 Bonding Curve
公式的初始数量 X
, Y
, K
。
前面已经介绍过,Pair 中主要是为了存储 Bonding Curve
公式值。
已经介绍过
我们在前面的 router 合约中看到对于 swap
方法的调用是:
也就是说 swap
的四个参数,要么 1,4 同时为空,要么 2,3 同时为空。并且这里的参数都是在 router 中已经计算好的,直接 set 即可。
factory 合约主要包含的就是一个 createPair
方法,我们之前已经介绍过。
FERC20 是内盘阶段使用的 Token,是一个简单的 ERC20,唯一多出一个功能就是可以限制用户每笔交易可以转移的数量,也就是前面的 maxTx
变量的功能。不过目前没有限制,可以不管。
外盘部分的合约比较多,我们主要研究下面这些涉及发币逻辑的合约:
前面在 Bonding
合约的 _openTradingOnUniswap
方法中,调用到了 agentFactory
合约的 initFromBondingCurve
和 executeBondingCurveApplication
方法,我们就先从这两个方法入手来看。
这里的一个主要逻辑就是将 assetToken,即 VIRTUAL 转入该合约,为后面提供 Uniswap 的流动性做准备。Application
可以理解为一个发币流程对象,每次发一个外盘的币,都会有一个 Application
生成。如果有兴趣的话可以看看这块,我们的主要目的还是来研究整个发币的逻辑。
代码注释中已经简单介绍了该方法的功能:
接下来我们来看这几个步骤的内容。
首先创建一个 Token,这里使用了 EIP1167 的概念,可以使用更少的 Gas 来部署,不熟悉的话可以看我之前写过这篇介绍 EIP1167 的文章。
这里创建的 Token 就是外盘 MEME,也就是在 Uniswap 中交易的正式 Token。接着对其进行初始化,注意由于 EIP1167 的限制,初始化只能额外调用 initialize
进行,而不能使用构造方法进行。
初始化的逻辑是 mint 一定数量的 MEME,以及在 DEX 中创建交易对。具体的代码逻辑我们后面再看。
将之前转入的 VIRTUAL 全部转入 MEME 合约中,最后在 MEME 合约中调用 Uniswap 的 addLiquidity
方法提供流动性。
这里与创建外盘 MEME 的逻辑相似,同样使用了 EIP1167 的概念。
这里是创建 Dao 合约,主要是用于治理和投票等,我们就不着重看了,感兴趣的朋友可以自行研究。
这里发行的 NFT 有点类似于 Uniswap V3 中提供流动性获取的 NFT。也就是说这个 NFT 记录了一些关于这个外盘 MEME 的相关信息。例如 Token 地址,LP 地址,ve Token 地址等。
为上一步 mint 的 NFT 创建一个合约钱包,这里使用的是 EIP6551 的相关逻辑,可以看我之前写的这篇文章。
将前面的初始流动性 LP 质押到 ve Token 合约中获取 veToken。
AgentToken 就是外盘 MEME,与内盘的 MEME 1: 1 兑换。当进入外盘阶段之后,只有外盘 MEME 可以进行买卖,内盘 MEME 只能兑换成外盘 MEME。
initialize 中主要来看这段代码:
根据函数传入的参数结构解析,lpSupply
是当前时刻内盘 Pair 中内盘 MEME 的数量,vaultSupply
是 totalSupply - lpSupply
,也就是在内盘阶段被买走的 MEME 数量。也就是说,lpSupply
就是仍在 Pair 中没有被买走的数量。
_mintBalances
方法是给相应的地址 mint 对应数量的外盘 MEME。给当前外盘 MEME 合约本身 mint 的数量是 lpSupply
。vault
表示内盘 Pair,给它 mint 数量为 totalSupply - lpSupply
的外盘 MEME。
这个数量是什么意思,我们来思考一下,在内盘阶段,最初始一共有十亿个内盘 MEME,即 totalSupply = 1B
,并且是全部被转给了 Pair 合约。那么在经过内盘的买卖之后,假设 Pair 合约中剩余有 lpSupply
个 Token,则意味着已经被用户买走的数量是 totalSupply - lpSupply
。
这里给当前合约 mint 的数量是 lpSupply
,实际上是合约目前拥有的 MEME 数量,后面要作为 Uniswap 的初始流动性。而给 Pair 合约 mint 的数量是 totalSupply - lpSupply
,是要给内盘阶段出售的内盘 MEME 提供 1: 1 的兑换流动性。
在 mint 相应的数量之后,调用 Uniswap 的方法创建交易对:
调用了 Uniswap 的 addLiquidity
方法提供流动性:
https://basescan.org/address/0x55cD6469F597452B5A7536e2CD98fDE4c1247ee4
https://basescan.org/address/0x082cb6e892dd0699b5f0d22f7d2e638bbada5d94#code
https://basescan.org/address/0xF66DeA7b3e897cD44A5a231c61B6B4423d613259
https://basescan.org/address/0x158d7CcaA23DC3c8861c3323eD546E3d25e74309
https://basescan.org/address/0x8292B43aB73EfAC11FAF357419C38ACF448202C5
https://basescan.org/address/0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b
https://basescan.org/address/0x71B8EFC8BCaD65a5D9386D07f2Dff57ab4EAf533
https://basescan.org/tx/0x0e1c3d6cc217e77843789a1e52e39743bc61339778d2749a23ec3cd98bb76412
https://basescan.org/tx/0x9c29666227548fcdffe4f4fbad9a9e9e682790b2c9d4f7cd14030b2625fadf35
到此,我们学习了 Virtuals 的发币逻辑。从内盘到外盘,整体逻辑比较简单,希望大家读完之后能对其合约部分有更加深入的了解。
欢迎和我交流