【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇

这篇具有很好参考价值的文章主要介绍了【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

上篇我们主要讲了 UniswapV2 整体分为了哪些项目,并重点讲解了 uniswap-v2-core 的核心代码实现;中篇主要对 uniswap-v2-periphery 的路由合约实现进行了剖析;现在剩下 V2 系列的最后一篇,我会介绍剩下的一些内容,主要包括:TWAP、FlashSwap、质押挖矿

TWAP

TWAP = Time-Weighted Average Price,即时间加权平均价格,可用来创建有效防止价格操纵的链上价格预言机

TWAP 的实现机制其实很简单。首先,在配对合约里会存储三个相关变量:

  • price0CumulativeLast

  • price1CumulativeLast

  • blockTimestampLast

前两个变量是两个 token 的累加价格,最后一个变量则用来记录更新的区块时间。我们可以直接来看看其代码实现:

【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇

这是 UniswapV2Pair 合约的 _update 函数,每次 mintburnswapsync 时都会触发更新。实现逻辑很容易理解,主要就以下几步:

  1. 读取当前的区块时间 blockTimestamp

  2. 计算出与上一次更新的区块时间之间的时间差 timeElapsed

  3. 如果 timeElapsed > 0 且两个 token 的 reserve 都不为 0,则更新两个累加价格

  4. 更新两个 reserve 和区块时间 blockTimestampLast

有些人可能还是不太理解累加价格的意义,要把它理解透彻,先从当前时刻的价格说起,即 token0 和 token1 的当前价格,其实可以根据以下公式计算所得:

price0 = reserve1 / reserve0
price1 = reserve0 / reserve1

比如,假设两个 token 分别为 WETH 和 USDT,当前储备量分别为 10 WETH 和 40000 USDT,那么 WETH 和 USDT 的价格分别为:

price0 = 40000/10 = 4000 USDT
price1 = 10/40000 = 0.00025 WETH

现在,再加上时间维度来考虑。比如,当前区块时间相比上一次更新的区块时间,过去了 5 秒,那就可以算出这 5 秒时间的累加价格:

price0Cumulative = reserve1 / reserve0 * timeElapsed = 40000/10*5 = 20000 USDT
price1Cumulative = reserve0 / reserve1 * timeElapsed = 10/40000*5 = 0.00125 WETH

假设之后再过了 6 秒,最新的 reserve 分别变成了 12 WETH 和 32000 USDT,则最新的累加价格变成了:

price0CumulativeLast = price0Cumulative + reserve1 / reserve0 * timeElapsed = 20000 + 32000/12*6 = 36000 USDT
price1CumulativeLast = price1Cumulative + reserve0 / reserve1 * timeElapsed = 0.00125 + 12/32000*6 = 0.0035 WETH

这就是合约里所记录的累加价格了。

另外,每次计算时因为有 timeElapsed 的判断,所以其实每次计算的是每个区块的第一笔交易。而且,计算累加价格时所用的 reserve 是更新前的储备量,所以,实际上所计算的价格是之前区块的,因此,想要操控价格的难度也就进一步加大了。

【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇

有了前面的基础,接下来就可以计算 TWAP 即时间加权平均价格了。计算公式也很简单,如下图:

【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇

代入我们的例子,为了简化,我们将前面 5 秒时间的时刻记为 T1,累加价格记为 priceT1,而 6 秒时间后的时刻记为 T2,累加价格记为 priceT2。如此,可以计算出,在后面 6 秒时间里的平均价格:

twap = (priceT2 - priceT1)/(T2 - T1) = (36000 - 20000)/6 = 2666.66

在实际应用中,一般有两种计算方案,一是固定时间窗口的 TWAP,二是移动时间窗口的 TWAP。在 uniswap-v2-periphery 项目中,examples 目录下提供了这两种方案的示例代码,分为是 ExampleOracleSimple.sol 和 ExampleSlidingWindowOracle.sol,具体代码就不展开讲解了。

现在,Uniswap TWAP 已经被广泛应用于很多 DeFi 协议,很多时候会结合 Chainlink 一起使用。比如 Compound 就使用 Chainlink 进行喂价并加入 Uniswap TWAP 进行边界校验,防止价格波动太大。

FlashSwap

FlashSwap,翻译过来就是闪电兑换,和闪电贷(FlashLoan) 有点类似。

从代码层面来说,闪电兑换的触发在 UniswapV2Pair 合约的 swap 函数里的,该函数里有这么一行代码:

if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);

这行代码主要说明了三个信息:

  1. to 地址是一个合约地址

  2. to 地址的合约实现了 IUniswapV2Callee 接口

  3. 可以在 uniswapV2Call 函数里执行 to 合约自己的逻辑

一般情况下的兑换流程,是先支付 tokenA,再得到 tokenB。但闪电兑换却可以先得到 tokenB,最后再支付 tokenA。如下图:

【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇

即是说,通过闪电兑换,可以实现无前置成本的套利。

比如,在 Uniswap 上可以用 3000 DAI 兑换出 1 ETH,而在 Sushi 上可以将 1 ETH 兑换成 3100 DAI,这就存在 100 DAI 的套利空间了。但是,如果用户钱包里没有 DAI 的话,该怎么套利呢?通过 Uniswap 的闪电兑换,就可以先获得 ETH,再将 ETH 在 Sushi 卖出得到 DAI,最后支付 DAI 给到 Uniswap,这样就实现了无需前置资金成本的套利了。

理论上,只要利润空间能覆盖两边的交易手续费和 GAS,就值得执行套利。这种套利行为能使得不同 DEX 之间的价格趋于一致。

闪电兑换还可以应用于另一种场景。假设用户想在 Compound 抵押 ETH 借出 DAI,再用借出的 DAI 到 Uniswap 兑换成 ETH,再抵押到 Compound 借出更多 DAI,如此重复操作,从而提高做多 ETH 的杠杆率。这么做的效率非常低。而使用闪电兑换,可以大大提高交易效率:

  1. 先从 Uniswap 得到 ETH

  2. 将用户的 ETH 和从 Uniswap 得到的 ETH 抵押进 Compound

  3. 从 Compound 借出 DAI

  4. 在 Uniswap 支付 DAI

上述步骤也不需要重复执行,一次流程就实现了用户想要的杠杆率,相比之下,明显高效很多。

在 uniswap-v2-periphery 项目中,examples 目录下有个 ExampleFlashSwap.sol,就是实现闪电兑换的一个示例,实现的是在 UniswapV1 和 UniswapV2 之间套利。

质押挖矿

质押挖矿项目也同样很小,这是项目的 github 地址:

  • https://github.com/Uniswap/liquidity-staker

总共只有四个 sol 文件:

  • IStakingRewards.sol

  • RewardsDistributionRecipient.sol

  • StakingRewards.sol

  • StakingRewardsFactory.sol

IStakingRewards.sol 是一个接口文件,定义了质押合约 StakingRewards 需要实现的一些函数,其中,Mutative 函数只有四个:

  • stake:充值,即质押

  • withdraw:提现,即解质押

  • getReward:提取奖励

  • exit:退出

剩下的则都是 View 函数:

  • lastTimeRewardApplicable:有奖励的最近区块数

  • rewardPerToken:每单位 Token 奖励数量

  • earned:用户已赚但未提取的奖励数量

  • getRewardForDuration:挖矿奖励总量

  • totalSupply:总质押量

  • balanceOf:用户的质押余额

RewardsDistributionRecipient.sol 则是一个抽象合约,跟常用的 Ownable 合约类似,我们可以直接看看其代码实现:

【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇

总共就 12 行代码,rewardsDistribution 其实就是管理员地址,还有一个 onlyRewardsDistribution 的 modifier,这不就是和我们熟知的 Ownable 一样的功能嘛。另外,还定义了一个抽象函数 notifyRewardAmount,所以实际上这就是一个抽象合约。而继承了该合约的是 StakingRewards 合约,后面再细说。

StakingRewards.sol 留到最后再说,先来看看 StakingRewardsFactory.sol,这是一个工厂合约,主要就是用来部署 StakingRewards 合约的。

StakingRewardsFactory

工厂合约里定义了四个变量:

  • rewardsToken:用作奖励的代币,其实就是 UNI 代币

  • stakingRewardsGenesis:质押挖矿开始的时间

  • stakingTokens:用来质押的代币数组,一般就是各交易对的 LPToken

  • stakingRewardsInfoByStakingToken:一个 mapping,用来保存质押代币和质押合约信息之间的映射

质押合约信息则是一个数据结构:

struct StakingRewardsInfo {
    address stakingRewards;
    uint rewardAmount;
}

其中,stakingRewards 其实就是 StakingRewards 合约(即质押合约)地址,rewardAmount 则是该质押合约每周期的奖励总量。

rewardsToken 和 stakingRewardsGenesis 在工厂合约的构造函数里就初始化的。除了构造函数,工厂合约还有三个函数:

  • deploy

  • notifyRewardAmounts

  • notifyRewardAmount

deploy 就是部署 StakingRewards 合约的函数,其代码实现如下:

function deploy(address stakingToken, uint rewardAmount) public onlyOwner {
    StakingRewardsInfo storage info = stakingRewardsInfoByStakingToken[stakingToken];
    require(info.stakingRewards == address(0), 'StakingRewardsFactory::deploy: already deployed');

    info.stakingRewards = address(new StakingRewards(address(this), rewardsToken, stakingToken));
    info.rewardAmount = rewardAmount;
    stakingTokens.push(stakingToken);
}

两个入参,stakingToken 就是质押代币,一般为 LPToken;rewardAmount 则是奖励数量。

实现逻辑,先从 mapping 中读取出 info,如果 info 的 stakingRewards 不为零地址说明该质押代币的质押合约已经部署过了,不能重复部署。接着,用 new 的方式创建了 StakeingRewards 合约,并将合约地址赋值给 info.stakingRewards,将合约地址保存起来。之后,再保存 rewardAmount。最后,将 stakingToken 加到质押代币数组里。至此,质押合约的部署工作就完成了。

部署合约之后,下一步应该将用来挖矿的代币转入到质押合约中,这就要通过 notifyRewardAmount 函数了,其代码实现如下:

function notifyRewardAmount(address stakingToken) public {
    require(block.timestamp >= stakingRewardsGenesis, 'StakingRewardsFactory::notifyRewardAmount: not ready');

    StakingRewardsInfo storage info = stakingRewardsInfoByStakingToken[stakingToken];
    require(info.stakingRewards != address(0), 'StakingRewardsFactory::notifyRewardAmount: not deployed');

    if (info.rewardAmount > 0) {
        uint rewardAmount = info.rewardAmount;
        info.rewardAmount = 0;

        require(
        IERC20(rewardsToken).transfer(info.stakingRewards, rewardAmount),
        'StakingRewardsFactory::notifyRewardAmount: transfer failed'
        );
        StakingRewards(info.stakingRewards).notifyRewardAmount(rewardAmount);
    }
}

调用该函数之前,其实还有一个前提条件要先完成,那就是需要先将用来挖矿奖励的 UNI 代币数量先转入该工厂合约。有个这个前提,工厂合约的该函数才能实现将 UNI 代币下发到质押合约中去。

代码逻辑就很简单了,先是判断当前区块的时间需大于等于质押挖矿的开始时间。然后读取出指定的质押代币 stakingToken 映射的质押合约 info,要求 info 的质押合约地址不能为零地址,否则说明还没部署。再判断 info.rewardAmount 是否大于零,如果为零也不用下发奖励。if 语句里面的逻辑主要就是调用 rewardsToken 的 transfer 函数将奖励代币转发给质押合约,再调用质押合约的 notifyRewardAmount 函数触发其内部处理逻辑。另外,将 info.rewardAmount 重置为 0,可以避免向质押合约重复下发奖励代币。

而 notifyRewardAmounts 函数,则是遍历整个质押代币数组,对每个代币再调用 notifyRewardAmount,实现逻辑非常简单。

至此,工厂合约的代码逻辑就讲完了。下面,就来看看 StakingRewards 合约了。

StakingRewards

StakingRewards 合约会继承 RewardsDistributionRecipient 合约和 IStakingRewards 接口。

StakingRewards 存储的变量则比较多,除了继承自 RewardsDistributionRecipient 抽象合约里的 rewardsDistribution 变量之外,还有 11 个变量:

  • rewardsToken:奖励代币,即 UNI 代币

  • stakingToken:质押代币,即 LPToken

  • periodFinish:质押挖矿结束的时间,默认时为 0

  • rewardRate:挖矿速率,即每秒挖矿奖励的数量

  • rewardsDuration:挖矿时长,默认设置为 60 天

  • lastUpdateTime:最近一次更新时间

  • rewardPerTokenStored:每单位 token 奖励数量

  • userRewardPerTokenPaid:用户的每单位 token 奖励数量

  • rewards:用户的奖励数量

  • _totalSupply:私有变量,总质押量

  • _balances:私有变量,用户质押余额

前面讲工厂合约的 notifyRewardAmount 函数时,提到最后其实会调用到 StakingRewards 合约的 notifyRewardAmount 函数,我们就来看看这个函数是如何实现的:

function notifyRewardAmount(uint256 reward) external onlyRewardsDistribution updateReward(address(0)) {
    if (block.timestamp >= periodFinish) {
    	 rewardRate = reward.div(rewardsDuration);
    } else {
        uint256 remaining = periodFinish.sub(block.timestamp);
        uint256 leftover = remaining.mul(rewardRate);
        rewardRate = reward.add(leftover).div(rewardsDuration);
    }

    // Ensure the provided reward amount is not more than the balance in the contract.
    // This keeps the reward rate in the right range, preventing overflows due to
    // very high values of rewardRate in the earned and rewardsPerToken functions;
    // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow.
    uint balance = rewardsToken.balanceOf(address(this));
    require(rewardRate <= balance.div(rewardsDuration), "Provided reward too high");

    lastUpdateTime = block.timestamp;
    periodFinish = block.timestamp.add(rewardsDuration);
    emit RewardAdded(reward);
}

该函数由工厂合约触发执行,而且根据工厂合约的代码逻辑,该函数也只会被触发一次。

由于 periodFinish 默认值为 0 且只会在该函数中更新值,所以只会执行 block.timestamp >= periodFinish 的分支逻辑,将从工厂合约转过来的挖矿奖励总量除以挖矿奖励时长,得到挖矿速率 rewardRate,即每秒的挖矿数量。理论上,else 分支是执行不到的,除非以后工厂合约升级为可以多次触发执行该函数。之后,读取 balance 并校验下 rewardRate,可以保证收取到的挖矿奖励余额也是充足的,rewardRate 就不会虚高。最后,更新 lastUpdateTime 和 periodFinish。periodFinish 就是在当前区块时间上加上挖矿时长,就得到了挖矿结束的时间。

接着,再来看看几个核心业务函数的实现,包括 stake、withdraw、getReward。

stake 就是质押代币的函数,实现代码如下:

function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
    require(amount > 0, "Cannot stake 0");
    _totalSupply = _totalSupply.add(amount);
    _balances[msg.sender] = _balances[msg.sender].add(amount);
    stakingToken.safeTransferFrom(msg.sender, address(this), amount);
    emit Staked(msg.sender, amount);
}

函数体内的代码逻辑很简单,将用户指定的质押量 amount 增加到 _totalSupply(总质押量)和 _balances(用户的质押余额),最后调用 stakingToken 的 safeTransferFrom 将代币从用户地址转入当前合约地址。

withdraw 则是用来提取质押代币的,代码实现也同样很简单,_totalSupply 和 _balances 都减掉提取数量,且将代币从当前合约地址转到用户地址:

function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
    require(amount > 0, "Cannot withdraw 0");
    _totalSupply = _totalSupply.sub(amount);
    _balances[msg.sender] = _balances[msg.sender].sub(amount);
    stakingToken.safeTransfer(msg.sender, amount);
    emit Withdrawn(msg.sender, amount);
}

getReward 是领取挖矿奖励的函数,内部逻辑主要就是从 rewards 中读取出用户有多少奖励并清零和转账给到用户:

function getReward() public nonReentrant updateReward(msg.sender) {
    uint256 reward = rewards[msg.sender];
    if (reward > 0) {
        rewards[msg.sender] = 0;
        rewardsToken.safeTransfer(msg.sender, reward);
        emit RewardPaid(msg.sender, reward);
    }
}

这几个核心业务函数体内的逻辑都非常好理解,值得一说的其实是每个函数声明最后的 updateReward(msg.sender),这是一个更新挖矿奖励的 modifer,我们来看其代码:

modifier updateReward(address account) {
    rewardPerTokenStored = rewardPerToken();
    lastUpdateTime = lastTimeRewardApplicable();
    if (account != address(0)) {
        rewards[account] = earned(account);
        userRewardPerTokenPaid[account] = rewardPerTokenStored;
    }
    _;
}

主要逻辑就是更新几个字段,包括 rewardPerTokenStored、lastUpdateTime 和用户的奖励相关的 rewards[account] 和 userRewardPerTokenPaid[account]。

其中,还调用到其他三个函数:rewardPerToken()、lastTimeRewardApplicable()、earned(account)。先来看看这三个函数的实现。最简单的就是 lastTimeRewardApplicable:

function lastTimeRewardApplicable() public view returns (uint256) {
    return Math.min(block.timestamp, periodFinish);
}

其逻辑就是从当前区块时间挖矿结束时间两者中返回最小值。因此,当挖矿未结束时返回的就是当前区块时间,而挖矿结束后则返回挖矿结束时间。也因此,挖矿结束后,lastUpdateTime 也会一直等于挖矿结束时间,这点很关键。

rewardPerToken 函数则是获取每单位质押代币的奖励数量,其实现代码如下:

function rewardPerToken() public view returns (uint256) {
    if (_totalSupply == 0) {
        return rewardPerTokenStored;
    }
    return
        rewardPerTokenStored.add(
            lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate).mul(1e18).div(_totalSupply)
        );
}

这其实就是用累加计算的方式存储到 rewardPerTokenStored 变量中。当挖矿结束后,则不会再产生增量,rewardPerTokenStored 就不会再增加了。

earned 函数则是计算用户当前的挖矿奖励,代码实现也只有一行代码:

function earned(address account) public view returns (uint256) {
    return _balances[account].mul(rewardPerToken().sub(userRewardPerTokenPaid[account])).div(1e18).add(rewards[account]);
}

其逻辑也是计算出增量的每单位质押代币的挖矿奖励,再乘以用户的质押余额得到增量的总挖矿奖励,再加上之前已存储的挖矿奖励,就得到当前总的挖矿奖励。

至此,StakingRewards 合约的主要实现逻辑也都讲解完了。

总结

至此,所有 UniswapV2 的合约项目就都讲解完了。虽然分为了好几个小项目,但从架构设计上来说,能够大大减低不同模块之间的耦合性,不同项目也可以由不同的小团队单独维护,而且项目小而简单,那出 BUG 的概率也会更低。所以,这样的架构设计其实更适合 Dapp。文章来源地址https://www.toymoban.com/news/detail-468157.html

到了这里,关于【区块链 | Uniswap】3.剖析DeFi交易产品之Uniswap:V2下篇的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Solidity Uniswap V2 Pair中添加流动性

            添加流动性的功能的用户入口,UniswapV2在UniswapV2Router中实现,它用来计算新的流动性并发行LP-Token,流动性管理简单地视为LP-Token管理。当你为一个pair增加流动性时,合约会创造LP Token;当你移除流动性时,LP-Token就会被销毁。pair合约中的添加流动性函数,是只执行

    2024年04月17日
    浏览(33)
  • 探索Uniswap v4核心:构建去中心化交易所的利器

    项目地址:https://gitcode.com/Uniswap/v4-core Uniswap是 Ethereum 上最知名的去中心化交易协议之一,其v4版本的核心代码库在 GitCode 上开放源码。这篇技术文章将深入解析Uniswap v4的技术特性,探讨它可以用来做什么,并阐述为什么它对DeFi开发者具有吸引力。 Uniswap v4是Uniswap协议的最新迭

    2024年04月16日
    浏览(42)
  • 探索Uniswap-Python: 去中心化交易所的编程接口

    项目地址:https://gitcode.com/uniswap-python/uniswap-python 在区块链的世界中,Uniswap是一个知名的去中心化交易协议,它以其流动性挖矿和自动做市商(AMM)机制而闻名。现在,有了uniswap-python项目,开发人员可以更加便捷地与Uniswap交互,构建自己的DeFi应用。本文将带你深入了解这个

    2024年04月22日
    浏览(36)
  • Polkadot + DeFi | 透明公平、高效交易的去中心化金融未来可期

    拥有投资理财意愿的人士,对金融领域的关注热度一直居高不下。从传统的金融产品,到去中心化金融产品的体验与尝试,借助区块链技术实现资产存储、资金交易行为范式的变革。无论是股票、期权,还是其他金融资产形式,DeFi(去中心化金融)不断实现对传统中心化金融

    2024年01月25日
    浏览(49)
  • [区块链安全-Damn_Vulnerable_DeFi]区块链DeFi智能合约安全实战(V3.0.0)(已完结)

    很抱歉,很久没有更新了。这段时间,经历了孩子出生、出国执行项目等诸多事情,心里也比较乱,也没有思绪去完成挑战。最近总算闲下来了,不过打开一看,发现[Damn-Vulnerable-DeFi]已经执行到v3.0.0了,很多东西都发生了变化,为什么不重头做一下呢?不过这次我可能会比较

    2024年02月07日
    浏览(52)
  • 区块链DeFi项目不可忽略的指标

    转载原文链接:http://www.btcwbo.com/5006.html 区块链的世界打破了数据源的障碍。它就像桌面上的一本大书。数据是完全开放和透明的。然而,显然更难在大量模糊的数据中找到有价值的信息。 数据可以被认为是任何项目中最重要的资产。数据分析可以了解过去、现状,甚至预测

    2023年04月08日
    浏览(45)
  • 区块链技术 swap 池子交换 token代币 defi应用 部署区块链项目

    服务器部署区块链应用环境 这篇文章教你如何在服务器里部署区块链应用环境

    2024年02月11日
    浏览(39)
  • Solana之旅6:Solana存储费与交易剖析

    请参考:https://blog.csdn.net/chhaozeng/article/details/116810006下面一段话 按这个信息,按SOL价格$100算,1KB的花费在:(100 x 364 x 0.01 )/ 2 = $182/年。 参考:https://blog.csdn.net/mutourend/article/details/119776339,它的计算更细一些 按这个信息,按SOL价格$100算,两年15KB的花费,应在$11。后面我们

    2024年01月19日
    浏览(44)
  • 对比传统交易模式与基于区块链的交易模式

    随着科技的不断进步,交易模式也在持续革新。传统交易模式与基于区块链的交易模式,作为两种截然不同的交易方式,各有其特点与影响。本文将对这两种交易模式进行详尽的对比,从多个维度揭示它们之间的差异。 传统交易模式通常依赖于中央机构或第三方来进行交易验

    2024年04月27日
    浏览(44)
  • 基于Android农产品商城交易设计与实现,毕业设计

    摘  要 人们生活水平随着发展不断的提升,人们对生鲜产品消费比越来越依赖,都希望吃到新鲜的食品。消费的加大给生鲜了全新的供应链及销售模式,那种传统的生鲜配送模式也在发生着变化。生鲜系统电商平台在我国目前是属于盛行的电商行业,快递物流配送冷链仍存在

    2024年02月03日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包