Virtuals Protocol 是 Base 链上一个类似于 Pump.fun 的发币平台。这篇文章主要来讲讲 Virtuals 的[合约代码](https://github.com/Virtual-Protocol/protocol-contracts),学习他们的整个发币流程和细节。 ## 基本逻辑 与 Pump 类似,Virtuals 上发币也分两个阶段,分别是内盘和外盘。内盘是指在合约内的 Bonding Curve 曲线上进行交易的阶段。外盘是指在 DEX,例如 Uniswap 上交易的阶段。当内盘销售结束时(一般是达到某市值,或者是预设数量的 Token 已经售完),合约将自动在 DEX 上创建交易对,此时要交易该 Token 便只能在 DEX 中进行。 Virtuals 中发行的币在内盘阶段只能通过 VITRUAL Token 购买。在 DEX 阶段,一般也是的 VIRTUAL-TOKEN 的交易对。这段时间 Virtuals 比较火,因此 VITRUAL 需求量大,涨势很好。 ## 内盘 内盘部分主要涉及下面这些合约: + Bonding + FERC20 + FFactory + FPair + FRouter 看过 Uniswap 代码的朋友应该对这一套架构比较熟悉,Virtuals 在内盘代码的架构上借鉴了 Uniswap 的逻辑。其中一个区别是,Virtuals 的内盘和外盘发行的是两个 Token。这里的 FERC20 是内盘阶段交易的 MEME Token,仅在内盘阶段使用,当进入到外盘后,会变成另一个 Token。用户可以以 1: 1 的比例将内盘的 MEME 兑换成外盘的 MEME。 ### Bonding 用户操作接口在 `Bonding` 合约中,主要有下面三个方法: + launch,发币 + buy,买币 + sell,卖币 + unwrapToken,将内盘的 MEME 兑换成外盘的 MEME #### launch 先来看 `launch` 方法。 ```solidity= // Bonding.sol function launch( string memory _name, string memory _ticker, uint8[] memory cores, string memory desc, string memory img, string[4] memory urls, uint256 purchaseAmount ) public nonReentrant returns (address, address, uint) { ``` 传入一些基本的 Token 信息。 ```solidity= // Bonding.sol // fee = 100 VIRTUAL require( purchaseAmount > fee, "Purchase amount must be greater than fee" ); // VIRTUAL address assetToken = router.assetToken(); require( IERC20(assetToken).balanceOf(msg.sender) >= purchaseAmount, "Insufficient amount" ); uint256 initialPurchase = (purchaseAmount - fee); IERC20(assetToken).safeTransferFrom(msg.sender, _feeTo, fee); IERC20(assetToken).safeTransferFrom( msg.sender, address(this), initialPurchase ); ``` 发币是要收费的,之前是 10 VIRTUAL,最近改成了 100 VIRTUAL。传入的 `purchaseAmount` 中减去 100,剩余的数量就是发币者自己购买的初始数量。 ```solidity= // Bonding.sol FERC20 token = new FERC20(string.concat("fun ", _name), _ticker, initialSupply, maxTx); uint256 supply = token.totalSupply(); ``` `initialSupply` 是由管理员设置的,目前是 10 亿,也就是说目前所有通过 Virtuals 发行的 Token,总供应量都是 10 亿。此时发行的 MEME 的 owner 是当前 `Bonding` 合约。 这里的 `maxTx` 是一个限制内盘 MEME 每笔转账数量的变量,目前没有限制,可以先不管。 ```solidity= // Bonding.sol address _pair = factory.createPair(address(token), assetToken); ``` 通过 `factory` 合约创建一个 Pair,也就是前面说的 `FPair`。 ```solidity= // FFactory.sol function _createPair( address tokenA, address tokenB ) internal returns (address) { require(tokenA != address(0), "Zero addresses are not allowed."); require(tokenB != address(0), "Zero addresses are not allowed."); require(router != address(0), "No router"); FPair pair_ = new FPair(router, tokenA, tokenB); _pair[tokenA][tokenB] = address(pair_); _pair[tokenB][tokenA] = address(pair_); pairs.push(address(pair_)); uint n = pairs.length; emit PairCreated(tokenA, tokenB, address(pair_), n); return address(pair_); } ``` 类似于 Uniswap 的写法,记录 Token 对应的 Pair。 `FPair` 的构造方法: ```solidity= // FPair.sol constructor(address router_, address token0, address token1) { require(router_ != address(0), "Zero addresses are not allowed."); require(token0 != address(0), "Zero addresses are not allowed."); require(token1 != address(0), "Zero addresses are not allowed."); router = router_; tokenA = token0; tokenB = token1; } ``` 注意 tokenA 总是新创建的 MEME,tokenB 总是 VIRTUAL。 再回到 `Bonding` 合约。 ```solidity= // Bonding.sol // 给 router 合约授权,后面 addInitialLiquidity 需要转入 bool approved = _approval(address(router), address(token), supply); require(approved); // k = 3_000_000_000_000 * 10000 / 5000 = 6_000_000_000_000 uint256 k = ((K * 10000) / assetRate); // 6000000000000000000000 = 6000 ether uint256 liquidity = (((k * 10000 ether) / supply) * 1 ether) / 10000; router.addInitialLiquidity(address(token), supply, liquidity); ``` 这里先计算出来一个 `liquidity` 变量,实际值是 6000 ether。然后我们再来看 `addInitialLiquidity` 的代码: ```solidity= // FRouter.sol function addInitialLiquidity( address token_, uint256 amountToken_, uint256 amountAsset_ ) public onlyRole(EXECUTOR_ROLE) returns (uint256, uint256) { require(token_ != address(0), "Zero addresses are not allowed."); address pairAddress = factory.getPair(token_, assetToken); IFPair pair = IFPair(pairAddress); IERC20 token = IERC20(token_); // 将初始的 1b MEME 转移到 pair 合约 token.safeTransferFrom(msg.sender, pairAddress, amountToken_); // 这里的初始数量就是 1b,6000,单位都是 ether pair.mint(amountToken_, amountAsset_); return (amountToken_, amountAsset_); } ``` 方法参数分别是: + amountToken_ = 1B ether + amountAsset_ = 6000 ether 再来看 Pair 合约的 `mint` 方法: ```solidity= // FPair.sol function mint( uint256 reserve0, uint256 reserve1 ) public onlyRouter returns (bool) { require(_pool.lastUpdated == 0, "Already minted"); // 这里的初始数量就是 1b,6000,单位都是 ether // 那么 k 就是 6000b ether _pool = Pool({ reserve0: reserve0, reserve1: reserve1, k: reserve0 * reserve1, lastUpdated: block.timestamp }); emit Mint(reserve0, reserve1); return true; } ``` 这里的计算就是 Bonding Curve 的核心。Virtuals 采用的是曲线模型是 > X * Y = K 其中 K 就是上面代码中的 `reserve0 * reserve1`,即 6000B ether。 也就是说内盘阶段的买卖都是在 `X * Y = K` 这条曲线上进行的,这和 Uniswap 是一致的。 注意这里的 `mint` 其实就是第一次生成 pair 的一个操作,同时初始化 reserve0 和 reserve1,k 的数值,并不是实际意义上的 mint token。 这时我们再回到 `Bonding` 的这行代码: ```solidity= // Bonding.sol router.addInitialLiquidity(address(token), supply, liquidity); ``` 其实就是提供了公式中 `X` 和 `Y` 的初始值。 接着来看 `launch` 方法。 ```solidity= // Bonding.sol Data memory _data = Data({ token: address(token), name: string.concat("fun ", _name), _name: _name, ticker: _ticker, supply: supply, price: supply / liquidity, marketCap: liquidity, liquidity: liquidity * 2, volume: 0, volume24H: 0, prevPrice: supply / liquidity, lastUpdated: block.timestamp }); Token memory tmpToken = Token({ creator: msg.sender, token: address(token), agentToken: address(0), pair: _pair, data: _data, description: desc, cores: cores, image: img, twitter: urls[0], telegram: urls[1], youtube: urls[2], website: urls[3], trading: true, // Can only be traded once creator made initial purchase tradingOnUniswap: false }); tokenInfo[address(token)] = tmpToken; tokenInfos.push(address(token)); bool exists = _checkIfProfileExists(msg.sender); // 记录用户创建的 MEME if (exists) { Profile storage _profile = profile[msg.sender]; _profile.tokens.push(address(token)); } else { bool created = _createUserProfile(msg.sender); if (created) { Profile storage _profile = profile[msg.sender]; _profile.tokens.push(address(token)); } } uint n = tokenInfos.length; emit Launched(address(token), _pair, n); ``` 这部分主要是记录 MEME 和用户的相关信息。 ```solidity= // Bonding.sol // Make initial purchase IERC20(assetToken).forceApprove(address(router), initialPurchase); router.buy(initialPurchase, address(token), address(this)); token.transfer(msg.sender, token.balanceOf(address(this))); return (address(token), _pair, n); ``` 最后这部分,实现了部署者购买初始数量的功能。注意这里的数量是已经减去部署费用之后的 VIRTUAL 数量。 这里的 `router.buy()` 方法最后会将购买到的 MEME 全部转给该合约,因此最后需要再将其转给 `msg.sender`,即部署者。 router 合约的内容我们后面再看,先把 `Bonding` 合约的其它部分看完。 #### buy 来看看 `buy` 方法的函数签名: ```solidity= // Bonding.sol function buy( uint256 amountIn, address tokenAddress ) ``` 参数分别是要购买的 MEME 和数量,也就是说在内盘阶段,所有 MEME 的购买都是通过该方法进行。 ```solidity= // Bonding.sol require(tokenInfo[tokenAddress].trading, "Token not trading"); // 获取根据(VIRTUAL,MEME)创建的 pair 地址 address pairAddress = factory.getPair( tokenAddress, router.assetToken() ); IFPair pair = IFPair(pairAddress); // A 是 MEME,B 是 VIRTUAL (uint256 reserveA, uint256 reserveB) = pair.getReserves(); (uint256 amount1In, uint256 amount0Out) = router.buy( amountIn, tokenAddress, msg.sender ); ``` 这里校验的 `trading`,在内盘阶段为 true,外盘阶段为 false,也就是说这里的 `buy` 是只能在内盘阶段被调用。 `reserveA` 和 `reserveB` 分别是 MEME 和 VIRTUAL 的数据,类似于 Uniswap 中的 reserve。 `buy` 方法的两个返回值分别是实际花费的 VIRTUAL 数量和购买获得的 MEME 数量。返回值的 `amount1In` 与参数的 `amountIn` 的区别是前者扣除了手续费。 接着是更新 MEME 的相关信息,这里省略: ```solidity= // Bonding.sol tokenInfo[tokenAddress] = ...; ``` ```solidity= // Bonding.sol // gradThreshold = 0.125 b if (newReserveA <= gradThreshold && tokenInfo[tokenAddress].trading) { _openTradingOnUniswap(tokenAddress); } ``` 最后这里,当 Pair 中 Token 的数据小于一个阈值,也就是 Pair 中的 Token 的余额已经所剩不多时,在 DEX(Uniswap)中开一个新的交易对,即开外盘。 #### sell `sell` 方法与 `buy` 方法大同小异,只是换了交易方向,大家自己看看理解就好。 #### _openTradingOnUniswap 整体逻辑比较简单,核心主要在于这部分: ```solidity= // Bonding.sol // graduate 的作用是将 VIRTUAL 转移到本合约 router.graduate(tokenAddress); // 授权 VIRTUAL 到 agent factory 合约 IERC20(router.assetToken()).forceApprove(agentFactory, assetBalance); uint256 id = IAgentFactoryV3(agentFactory).initFromBondingCurve( string.concat(_token.data._name, " by Virtuals"), _token.data.ticker, _token.cores, // 线上数据 // 0xa7647ac9429fdce477ebd9a95510385b756c757c26149e740abbab0ad1be2f16 _deployParams.tbaSalt, // 0x55266d75d1a14e4572138116af39863ed6596e7f _deployParams.tbaImplementation, // 259200 = 3 days _deployParams.daoVotingPeriod, // 0 _deployParams.daoThreshold, assetBalance ); address agentToken = IAgentFactoryV3(agentFactory) .executeBondingCurveApplication( id, // 每个 MEME 都一样,1B // 1_000_000_000 _token.data.supply / (10 ** token_.decimals()), // 不同的 MEME 不一样 tokenBalance / (10 ** token_.decimals()), pairAddress ); _token.agentToken = agentToken; router.approval( pairAddress, agentToken, address(this), IERC20(agentToken).balanceOf(pairAddress) ); token_.burnFrom(pairAddress, tokenBalance); ``` 这里的主要难点在于调用了 `agentFactory` 的两个方法: + initFromBondingCurve + executeBondingCurveApplication `initFromBondingCurve` 可以简单理解为生成了一个流程对象,并返回该对象的 id。其将内盘 Pair 中的所有 VIRTUAL 余额全部转入了 `agentFactory` 合约中 `executeBondingCurveApplication` 中包含了创建新的外盘 MEME,添加流动性等逻辑。并给 Pair 合约 mint 了一些数量的外盘 MEME。 ### FRouter router 合约有这几个主要方法: + buy + sell + addInitialLiquidity + getAmountsOut #### getAmountsOut 我们先来看 `getAmountsOut`: ```solidity= // FRouter.sol function getAmountsOut( address token, address assetToken_, uint256 amountIn ) public view returns (uint256 _amountOut) { require(token != address(0), "Zero addresses are not allowed."); address pairAddress = factory.getPair(token, assetToken); IFPair pair = IFPair(pairAddress); (uint256 reserveA, uint256 reserveB) = pair.getReserves(); uint256 k = pair.kLast(); uint256 amountOut; if (assetToken_ == assetToken) { uint256 newReserveB = reserveB + amountIn; uint256 newReserveA = k / newReserveB; amountOut = reserveA - newReserveA; } else { uint256 newReserveA = reserveA + amountIn; uint256 newReserveB = k / newReserveA; amountOut = reserveB - newReserveB; } return amountOut; } ``` 该方法的功能是计算买卖某 Token 时,能够获得多少数量。我们前面说过, `reserveA` 和 `reserveB` 分别是 MEME 和 VIRTUAL 的数据。那么当 `assetToken_` 传 VIRTUAL 的时候,代表买入(因为这里的逻辑是 reserveB 增多)。传其它地址的时候,代表卖出。 这里使用 `X * Y = K` 的公式,通过从 pair 中获得的 reserve 数据,来计算最终所能得到的数量。 #### buy `buy` 的代码如下: ```solidity= // FRouter.sol function buy( uint256 amountIn, address tokenAddress, address to ) public onlyRole(EXECUTOR_ROLE) nonReentrant returns (uint256, uint256) { require(tokenAddress != address(0), "Zero addresses are not allowed."); require(to != address(0), "Zero addresses are not allowed."); require(amountIn > 0, "amountIn must be greater than 0"); address pair = factory.getPair(tokenAddress, assetToken); // 目前线上是 1 uint fee = factory.buyTax(); // 也就是 1% uint256 txFee = (fee * amountIn) / 100; address feeTo = factory.taxVault(); uint256 amount = amountIn - txFee; IERC20(assetToken).safeTransferFrom(to, pair, amount); IERC20(assetToken).safeTransferFrom(to, feeTo, txFee); uint256 amountOut = getAmountsOut(tokenAddress, assetToken, amount); IFPair(pair).transferTo(to, amountOut); IFPair(pair).swap(0, amountOut, amount, 0); return (amount, amountOut); } ``` 购买的时候需要付 1% 的手续费。这里要注意的是,`X * Y = K` 公式中的 `reserveA` 和 `reserveB` 是存储在 Pair 合约中的,但实际变化数量的计算是在 router 合约的,也就是说 Pair 合约中只存储 reserve 的值,但是并没有计算过程。当变化的数量在 router 中通过 `getAmountsOut` 方法计算好之后被传入到 Pair 中通过 `swap` 方法进行增减。 #### sell 与 `buy` 相似的逻辑。 #### addInitialLiquidity ```solidity= // FRouter.sol function addInitialLiquidity( address token_, uint256 amountToken_, uint256 amountAsset_ ) public onlyRole(EXECUTOR_ROLE) returns (uint256, uint256) { require(token_ != address(0), "Zero addresses are not allowed."); address pairAddress = factory.getPair(token_, assetToken); IFPair pair = IFPair(pairAddress); IERC20 token = IERC20(token_); // 将初始的 1B 新 MEME 转移到 pair 合约 token.safeTransferFrom(msg.sender, pairAddress, amountToken_); // 这里的初始数量就是 1b,6000,单位都是 ether pair.mint(amountToken_, amountAsset_); return (amountToken_, amountAsset_); } ``` 注意这里的添加流动性并不是在 Uniswap 中添加,而是添加到内盘中的虚拟流动性,也就是说只是为了提供了 `XYK` 公式的初始数量。十亿的内盘 MEME 转入 Pair 合约,但是 VIRTUAL 并没有转入,只是提供了 6000 这样一个数量而已。 `mint` 方法我们前面也已经看过,主要是记录 `Bonding Curve` 公式的初始数量 `X`, `Y`, `K`。 ### FPair 前面已经介绍过,Pair 中主要是为了存储 `Bonding Curve` 公式值。 #### mint 已经介绍过 #### swap ```solidity= // FPair.sol // 1,4 为 0 -> buy // 2,3 为 0 -> sell function swap( uint256 amount0In, uint256 amount0Out, uint256 amount1In, uint256 amount1Out ) public onlyRouter returns (bool) { // 这里的 in out,都是 router 计算好了,传进来直接更新 // 而不是在这里通过公式计算的 uint256 _reserve0 = (_pool.reserve0 + amount0In) - amount0Out; uint256 _reserve1 = (_pool.reserve1 + amount1In) - amount1Out; _pool = Pool({ reserve0: _reserve0, reserve1: _reserve1, k: _pool.k, lastUpdated: block.timestamp }); emit Swap(amount0In, amount0Out, amount1In, amount1Out); return true; } ``` 我们在前面的 router 合约中看到对于 `swap` 方法的调用是: + buy -> pair.swap(0, amountOut, amount, 0); + sell -> pair.swap(amountIn, 0, 0, amountOut); 也就是说 `swap` 的四个参数,要么 1,4 同时为空,要么 2,3 同时为空。并且这里的参数都是在 router 中已经计算好的,直接 set 即可。 ### FFactory factory 合约主要包含的就是一个 `createPair` 方法,我们之前已经介绍过。 ### FERC20 FERC20 是内盘阶段使用的 Token,是一个简单的 ERC20,唯一多出一个功能就是可以限制用户每笔交易可以转移的数量,也就是前面的 `maxTx` 变量的功能。不过目前没有限制,可以不管。 ## 外盘 外盘部分的合约比较多,我们主要研究下面这些涉及发币逻辑的合约: + AgentFactoryV3 + AgentToken ### AgentFactoryV3 前面在 `Bonding` 合约的 `_openTradingOnUniswap` 方法中,调用到了 `agentFactory` 合约的 `initFromBondingCurve` 和 `executeBondingCurveApplication` 方法,我们就先从这两个方法入手来看。 #### initFromBondingCurve ```solidity= // AgentFactoryV3.sol function initFromBondingCurve( string memory name, string memory symbol, uint8[] memory cores, bytes32 tbaSalt, address tbaImplementation, uint32 daoVotingPeriod, uint256 daoThreshold, uint256 applicationThreshold_ ) public whenNotPaused onlyRole(BONDING_ROLE) returns (uint256) { address sender = _msgSender(); require( IERC20(assetToken).balanceOf(sender) >= applicationThreshold_, "Insufficient asset token" ); require( IERC20(assetToken).allowance(sender, address(this)) >= applicationThreshold_, "Insufficient asset token allowance" ); require(cores.length > 0, "Cores must be provided"); IERC20(assetToken).safeTransferFrom( sender, address(this), applicationThreshold_ ); uint256 id = _nextId++; uint256 proposalEndBlock = block.number; // No longer required in v2 Application memory application = Application( name, symbol, "", ApplicationStatus.Active, applicationThreshold_, sender, cores, proposalEndBlock, 0, tbaSalt, tbaImplementation, daoVotingPeriod, daoThreshold ); _applications[id] = application; emit NewApplication(id); return id; } ``` 这里的一个主要逻辑就是将 assetToken,即 VIRTUAL 转入该合约,为后面提供 Uniswap 的流动性做准备。`Application` 可以理解为一个发币流程对象,每次发一个外盘的币,都会有一个 `Application` 生成。如果有兴趣的话可以看看这块,我们的主要目的还是来研究整个发币的逻辑。 #### executeApplication ```solidity= // AgentFactoryV3.sol function executeApplication(uint256 id, bool canStake) public noReentrant { // This will bootstrap an Agent with following components: // C1: Agent Token // C2: LP Pool + Initial liquidity // C3: Agent veToken // C4: Agent DAO // C5: Agent NFT // C6: TBA // C7: Stake liquidity token to get veToken Application storage application = _applications[id]; require( msg.sender == application.proposer || hasRole(WITHDRAW_ROLE, msg.sender), "Not proposer" ); _executeApplication(id, canStake, _tokenSupplyParams); } ``` 代码注释中已经简单介绍了该方法的功能: + C1: 发行 Agent Token,即外盘 MEME + C2: 在 DEX 中提供初始流动性 + C3: 创建 ve Token,即一种支持锁仓投票功能的 token,熟悉 Curve 的朋友应该比较了解 + C4: 创建 Dao 合约,可以支持治理等功能 + C5: 发行一个 NFT + C6: 为上一步发行的 NFT 绑定一个钱包 + C7: 将初始流动性 LP stake 为 ve Token 接下来我们来看这几个步骤的内容。 ##### C1 ```solidity= // AgentFactoryV3.sol address token = _createNewAgentToken( application.name, application.symbol, tokenSupplyParams_ ); ``` ```solidity= // AgentFactoryV3.sol function _createNewAgentToken( string memory name, string memory symbol, bytes memory tokenSupplyParams_ ) internal returns (address instance) { instance = Clones.clone(tokenImplementation); IAgentToken(instance).initialize( [_tokenAdmin, _uniswapRouter, assetToken], abi.encode(name, symbol), tokenSupplyParams_, _tokenTaxParams ); allTradingTokens.push(instance); return instance; } ``` 首先创建一个 Token,这里使用了 EIP1167 的概念,可以使用更少的 Gas 来部署,不熟悉的话可以看我之前写过这篇介绍 EIP1167 的[文章](https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8)。 这里创建的 Token 就是外盘 MEME,也就是在 Uniswap 中交易的正式 Token。接着对其进行初始化,注意由于 EIP1167 的限制,初始化只能额外调用 `initialize` 进行,而不能使用构造方法进行。 初始化的逻辑是 mint 一定数量的 MEME,以及在 DEX 中创建交易对。具体的代码逻辑我们后面再看。 ##### C2 ```solidity= // AgentFactoryV3.sol // 上一步创建的 Uniswap 交易对地址 address lp = IAgentToken(token).liquidityPools()[0]; IERC20(assetToken).safeTransfer(token, initialAmount); IAgentToken(token).addInitialLiquidity(address(this)); ``` 将之前转入的 VIRTUAL 全部转入 MEME 合约中,最后在 MEME 合约中调用 Uniswap 的 `addLiquidity` 方法提供流动性。 ##### C3 ```solidity= // C3 // AgentFactoryV3.sol address veToken = _createNewAgentVeToken( string.concat("Staked ", application.name), string.concat("s", application.symbol), lp, application.proposer, canStake ); ``` ```solidity= // AgentFactoryV3.sol function _createNewAgentVeToken( string memory name, string memory symbol, address stakingAsset, address founder, bool canStake ) internal returns (address instance) { instance = Clones.clone(veTokenImplementation); IAgentVeToken(instance).initialize( name, symbol, founder, stakingAsset, block.timestamp + maturityDuration, address(nft), canStake ); allTokens.push(instance); return instance; } ``` 这里与创建外盘 MEME 的逻辑相似,同样使用了 EIP1167 的概念。 ##### C4 ```solidity= // AgentFactoryV3.sol string memory daoName = string.concat(application.name, " DAO"); address payable dao = payable( _createNewDAO( daoName, IVotes(veToken), application.daoVotingPeriod, application.daoThreshold ) ); ``` ```solidity= // AgentFactoryV3.sol function _createNewDAO( string memory name, IVotes token, uint32 daoVotingPeriod, uint256 daoThreshold ) internal returns (address instance) { instance = Clones.clone(daoImplementation); IAgentDAO(instance).initialize( name, token, nft, daoThreshold, daoVotingPeriod ); allDAOs.push(instance); return instance; } ``` 这里是创建 Dao 合约,主要是用于治理和投票等,我们就不着重看了,感兴趣的朋友可以自行研究。 ##### C5 ```solidity= // AgentFactoryV3.sol uint256 virtualId = IAgentNft(nft).nextVirtualId(); IAgentNft(nft).mint( virtualId, _vault, application.tokenURI, dao, application.proposer, application.cores, lp, token ); application.virtualId = virtualId; ``` 这里发行的 NFT 有点类似于 Uniswap V3 中提供流动性获取的 NFT。也就是说这个 NFT 记录了一些关于这个外盘 MEME 的相关信息。例如 Token 地址,LP 地址,ve Token 地址等。 ##### C6 ```solidity= // AgentFactoryV3.sol // C6 uint256 chainId; assembly { chainId := chainid() } address tbaAddress = IERC6551Registry(tbaRegistry).createAccount( application.tbaImplementation, application.tbaSalt, chainId, nft, virtualId ); IAgentNft(nft).setTBA(virtualId, tbaAddress); ``` 为上一步 mint 的 NFT 创建一个合约钱包,这里使用的是 EIP6551 的相关逻辑,可以看我之前写的这篇[文章](https://mirror.xyz/xyyme.eth/t6vH9iMSbdM-9izoxRFyWCX5gvxs5rmNM8UkZFV4d5w)。 ##### C7 ```solidity= // AgentFactoryV3.sol IERC20(lp).approve(veToken, type(uint256).max); IAgentVeToken(veToken).stake( IERC20(lp).balanceOf(address(this)), application.proposer, defaultDelegatee ); ``` 将前面的初始流动性 LP 质押到 ve Token 合约中获取 veToken。 ### AgentToken AgentToken 就是外盘 MEME,与内盘的 MEME 1: 1 兑换。当进入外盘阶段之后,只有外盘 MEME 可以进行买卖,内盘 MEME 只能兑换成外盘 MEME。 #### initialize initialize 中主要来看这段代码: ```solidity= // AgentToken.sol // 内盘 pair 中的数量 uint256 lpSupply = supplyParams.lpSupply * (10 ** decimals()); // totalSupply - lpSupply uint256 vaultSupply = supplyParams.vaultSupply * (10 ** decimals()); _mintBalances(lpSupply, vaultSupply); ``` ```solidity= // AgentToken.sol function _mintBalances(uint256 lpMint_, uint256 vaultMint_) internal { if (lpMint_ > 0) { _mint(address(this), lpMint_); } if (vaultMint_ > 0) { _mint(vault, vaultMint_); } } ``` 根据函数传入的参数结构解析,`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 的兑换流动性。 #### _createPair 在 mint 相应的数量之后,调用 Uniswap 的方法创建交易对: ```solidity= // AgentToken.sol function _createPair() internal returns (address uniswapV2Pair_) { // 创建(外盘 MEME,VIRTUAL)的 Uniswap pair uniswapV2Pair_ = IUniswapV2Factory(_uniswapRouter.factory()).createPair( address(this), pairToken ); _liquidityPools.add(uniswapV2Pair_); emit LiquidityPoolCreated(uniswapV2Pair_); return (uniswapV2Pair_); } ``` #### addInitialLiquidity 调用了 Uniswap 的 `addLiquidity` 方法提供流动性: ```solidity= // AgentToken.sol // 授权外盘 Token 本身以及 VIRTUAL _approve(address(this), address(_uniswapRouter), type(uint256).max); // pairToken 即 VIRTUAL IERC20(pairToken).approve(address(_uniswapRouter), type(uint256).max); // Add the liquidity: (uint256 amountA, uint256 amountB, uint256 lpTokens) = _uniswapRouter .addLiquidity( address(this), pairToken, balanceOf(address(this)), IERC20(pairToken).balanceOf(address(this)), 0, 0, address(this), block.timestamp ); emit InitialLiquidityAdded(amountA, amountB, lpTokens); ``` ## 合约线上地址及一些示例交易 ### Luna(mini proxy token) https://basescan.org/address/0x55cD6469F597452B5A7536e2CD98fDE4c1247ee4 ### 原始 AgentToken https://basescan.org/address/0x082cb6e892dd0699b5f0d22f7d2e638bbada5d94#code ### Bonding https://basescan.org/address/0xF66DeA7b3e897cD44A5a231c61B6B4423d613259 ### FFactory https://basescan.org/address/0x158d7CcaA23DC3c8861c3323eD546E3d25e74309 ### FRouter https://basescan.org/address/0x8292B43aB73EfAC11FAF357419C38ACF448202C5 ### VIRTUAL Token https://basescan.org/address/0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b ### AgentFactory https://basescan.org/address/0x71B8EFC8BCaD65a5D9386D07f2Dff57ab4EAf533 ### launch 的交易 https://basescan.org/tx/0x0e1c3d6cc217e77843789a1e52e39743bc61339778d2749a23ec3cd98bb76412 ### 内盘买满,进入 uni 的交易 https://basescan.org/tx/0x9c29666227548fcdffe4f4fbad9a9e9e682790b2c9d4f7cd14030b2625fadf35 ## 总结 到此,我们学习了 Virtuals 的发币逻辑。从内盘到外盘,整体逻辑比较简单,希望大家读完之后能对其合约部分有更加深入的了解。 ## 关于我 欢迎[和我交流](https://linktr.ee/xyymeeth)