# 红包DAPP项目分享及合约解读

By [Xing](https://paragraph.com/@xing824) · 2022-01-26

---

前言
--

鄙人上两篇关于NFT的文章被大家点赞了很多，真心感谢。从本人最开始写第一篇文章以来，初心就是想以一个builder的身份来分享自己的学习积累，让更多的人能够看到并产生兴趣，可以加入进来一起build一个更加公正透明的世界。

本人不是什么大佬，也不是所谓的科学家，本人只是从web2.0领域刚想进入web3.0领域的一个码农而已。我朋友圈里倒是有很多web2.0时代的技术大神，架构领域、安全领域、AI领域的方方面面，很多都是业内叫得上名的技术大牛。

这些大神们把精力都放在更重要的技术探索上了，所以没有时间输出内容。比如：我一个朋友可以hack了好多知名正版软件给我们炫耀，但是绝对不会放出去售卖；为了一个没人玩的iOS游戏可以满分通关，就会熬夜把游戏hack了，拿到全是满分的成绩去炫耀满足感。（其实还有太多不方便公开说的了，都是跟钱无关但是特别geek的事情，哈哈）

而我写的东西实际上都是很简单很基础的，能得到大家的认可很高兴，但是希望大家知道高手在民间，我个人的分享是希望更多的人能够参与进来，一起build。

其实在开始分享学习笔记之前，本人是发布了一个[红包DAPP](https://redenvelop.app/)项目的，之前的文章里发现很多人对智能合约解读很感兴趣，所以这里本人借自己的红包DAPP产品之名，解读一下鄙人红包DAPP的合约，顺带分享一下鄙人对此项目的思考，希望这篇文章能够帮助到：

*   **Web2.0 Developer**：本次解读也许会让你觉得solidity并不难，也许是你进入DAPP开发的一个药引子
    
*   **Investor or Trader**：未来调研某个项目的时候，也许可以从合约这个角度来窥探一下整个项目，丰富你投资决策的依据。**毕竟Codes don't lie！**
    
*   **Web3.0 Startup**：一个失败的项目案例可供参考，也许能帮助你们在成功的路上少踩一些坑。
    

红包DAPP的诞生
---------

### 产品背景

为什么会有红包DAPP这个项目呢？首要原因是鄙人为了学习合约的编程，需要一个练手的项目来实战一下，毕竟学习某个编程语言最好的方式除了看别人的代码之外就是自己上手写一个。实战出真知，就好比一个程序员如果没有大厂经验，没有经历过用户高峰带来的流量压力，你永远不知道为什么系统中有些地方需要这样设计。

第二个原因就是推特上有各种各样项目方或者大V的市场活动，比如关注、点赞就能参与抽奖，当时一般是抽各种各样的币（现在NFT的white list比较多）。整个抽奖过程都是非透明的，web3时代的从业者还用web2时代的工具进行市场活动，这个有点说不过去。

最后就是微信红包这个产品国内用户都很熟悉了，是一个非常成功的产品设计。那把微信红包这个产品设计移植到区块链上大方向应该没错，所以就有了红包DAPP这个产品。

### 功能简述

红包DAPP功能上与微信红包一样，发送者把一定数量Token放入红包中，然后指定红包分成多少份，指定哪些钱包地址可以领取，这样就创建好了一个红包。用户可以像抢微信红包那样去抢着打开这个红包，并得到一定随机金额的Token。

不同的是，微信红包24小时过期之后，未被领完的红包会被微信系统主动回退至发送者账户中，但红包DAPP上未被领完的红包，不能被主动回退到发送者钱包地址里（区块链上所有操作需要有人触发），所以这时需要发送者主动撤回他未被领完的红包。

### 发币与“经济模型”

![](https://storage.googleapis.com/papyrus_images/96f4ae8b7bcb9056acde592b89dd294b9cb4ea0f08c41212e8bdfa497389db47.webp)

看了上面这个红包DAPP介绍，大家一定会想：就这？连个币都不发，连个经济模型都没有，这还算个啥crypto项目？

所以发币和“经济模型”也是必须有的。为了红包DAPP项目鄙人也发了个曼巴币（Mamba Coin），这个曼巴币有什么用呢？作用就是可以去开上面所说的超过24小时未被领完的红包。

例如鄙人发了一个10份的红包给我的朋友，24小时过去了，还有3份没有被领完，那这个红包就变成了公开红包，其余的所有人只要花费曼巴币就可以打开这个过期红包。

**曼巴币是为了纪念科比而发的，总发行量为20200126个（科比离开的那天，也是两年前的今天），本人预留8.24%（科比曾经的两个球衣号码），其余的曼巴币全部注入到了红包DAPP合约中。当有人成功打开一个红包后，红包DAPP合约会向红包的创建者和领取者各发送8个曼巴币（科比前10年职业生涯的球衣号码），如果你想打开一个过期红包，那需要花费24个曼巴币（科比后10年职业生涯的球衣号码）。**

所以曼巴币的分配，除了本人保留的8.24%之外，其余的分配均是通过使用红包DAPP来发红包、领红包获得。

所以非要有一个经济模型的话，姑且可以认为红包DAPP的经济模型就是：发红包领红包越多，那你获得的曼巴币就越多，同时如果发红包的人越多，那一定概率下未被领完的过期红包也会越多，曼巴币因为能打开未领完的过期红包，所以曼巴币也就有了价值。而有价值的曼巴币又会吸引用户更多的去发红包，形成正向循环。

如果非要起个高大上名字的话，可以叫做“领红包即挖矿”吧。

Talk is cheap, show me the code
-------------------------------

这部分就跟大家说说代码实现吧，讲解代码是个很困难的事情，因为每个细节展开都可以延展很多，所以讲到多深很难把握。我会尽量把代码注释清楚，如果需要了解细节可能还需要自行学习一下。

鄙人从开始看教程开始到合约部署测试完成总共没超过1周，经过这段时间看过很多项目代码之后，发现该合约代码有很大的改善空间，后面会提到，所以这里展示的合约仅供入门参考。

### Mamba Coin

下面我们来先看看曼巴币的[合约](https://bscscan.com/address/0xab1ec376ccad1bfd5e3409a60bd30988af2dc765#code)：

    // 合约名称为MambaCoin，是一个ERC20的合约（继承）
    contract MambaCoin is ERC20 {
        // 调用红包DAPP合约的方法initBonusCoin，详见红包DAPP合约
        bytes4 private constant SELECTOR = bytes4(keccak256(bytes("initBonusCoin(address)")));
    
        // 初始化合约，传入红包DAPP的合约地址，以及ERC20的name和symbol
        constructor(address redenvelopAddr) public ERC20("Mamba Coin", "MAMB") {
            // 定义总发行量
            uint256 initialSupply = 20200126 ether;
    
            // 计算给团队的数量，8.24%
            uint256 toTeam = initialSupply*824/100/100;
    
            // 铸造，给团队分配其中的8.24%
            _mint(msg.sender, toTeam);
    
            // 铸造，剩余的总量分配到红包DAPP合约
            _mint(redenvelopAddr, initialSupply - toTeam);
    
            // 调用红包DAPP的initBonus方法，将曼巴币的合约地址注入进去
            (bool success, bytes memory data) = redenvelopAddr.call(abi.encodeWithSelector(SELECTOR, address(this)));
            require(success && (data.length == 0 || abi.decode(data, (bool))), "Mamba Coin: INIT_FAILED");
        }
    }
    

对，上面就是整个曼巴币的合约代码了。实际上发币这么简单是因为整个ERC20的实现直接使用了OpenZeppelin开源出来的[代码库](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20)，也就是上面“is ERC20”的部分，所有ERC20代币的功能都由这句“is ERC20”继承下来了，包括上面代码中的“\_mint”方法。

这里我们简单看看OpenZepplein中ERC20里”\_mint”方法的实现：

    contract ERC20 is Context, IERC20, IERC20Metadata {
        // 定义了一个map类型的_balances变量
        // 该变量储存了这个币里某个address有多少token的对应关系
        mapping(address => uint256) private _balances;
        
        // 定义了一个map类型的_allowances变量
        // 该变量储存了某个hodler的address，允许哪个address去使用他的多少token
        // 还记得之前说的approve方法吗？就是修改这个变量的值
        mapping(address => mapping(address => uint256)) private _allowances;
    
        // 总发行量
        uint256 private _totalSupply;
    
        ......
    
        function _mint(address account, uint256 amount) internal virtual {
            require(account != address(0), "ERC20: mint to the zero address");
            
            // 前置校验，当前可忽略
            _beforeTokenTransfer(address(0), account, amount);
    
            // 总发行量加上本次mint的数量
            _totalSupply += amount;
    
            // 在_balances的map中增加account这个地址的余额
            _balances[account] += amount;
    
            // 发送一个Event
            emit Transfer(address(0), account, amount);
    
            // 后置校验，当前可忽略
            _afterTokenTransfer(address(0), account, amount);
        }
    }
    

可以看到，\_mint这个方法就是在合约内部维护了\_balances这个map，这个\_balances记录了所有持有该币的地址以及对应的token数量。

上述代码里还可以看到“emit Transfer”这样一个发送事件的操作，这个操作在区块链上就是一个日志记录，因为如果在链上只记录当前状态，而不记录其状态如何变化的话，就不能很方便地在区块链的数据上重建其状态的变化过程，例如上述曼巴币在发行的时候两次“\_mint”的操作日志可以看[这里](https://bscscan.com/tx/0xc472e13c01b5617622fb8f12c8bbf651d7547676459cf1c9604e4482d7d147aa)，见下截图。

![](https://storage.googleapis.com/papyrus_images/74f5b8a02c7ff393b03fe2afaec68e155a2ee4456dfd6170e04bc77469b51687.png)

大家感兴趣的话可以去看看其他ERC20方法的实现，例如approve，transfer，transferFrom，balanceOf，allowance等比较关键的几个，就能知道那些价值连城的Token无外乎就是记录在链上的一个map类型的变量。

总之，一个合约主要包括三部分：

*   **状态值**
    
    例如ERC20合约前面定义的\_balances和\_allowances这样的变量值，这些变量值记录了该合约要存储的状态
    
*   **方法**
    
    例如ERC20中的各个方法，例如\_mint，approve，transfer，transferFrom等，这些方法会去修改合约中的状态值，还有一些方法例如balanceOf，allowance等，他们会向外部提供合约中状态值的查询。
    
*   **事件**
    
    例如ERC20中的各个Event，当合约中状态值发生变化时，这个Event会记录状态值变化的原因。
    

**所以完成一个合约把上面三部分想清楚就行，要记录的状态值有哪些，提供哪些方法去修改/读取状态值，修改状态值的时候定义的事件能够反应这个状态值的变化，搞定这三部分就完成了一个合约编写。**

下面我们看看红包DAPP的合约吧。

### 红包DAPP

下面开始说红包DAPP的合约，先贴[代码](https://bscscan.com/address/0x72ad42ae46be7a34e252e475743cf263c82b66e4#code)：

    // 红包DAPP合约
    contract RedEnvelop {
        // 红包过期时间定义：1天
        uint256 private constant EXPIRE_DAY = 1 days;
    
        // 赠送曼巴币数量定义：8个（1 ether = 10的18次方）
        uint256 private constant BONUS_MAMB = 8 ether;
    
        // 打开过期红包需要曼巴币数量定义：24个
        uint256 private constant LUCKY_DRAW_MAMBA = 24 ether;
    
        // 红包编号（查询红包详细信息的时候需要传入）
        uint256 public counter;
    
        // 曼巴币的地址
        address public mambaCoinAddr;
    
        // 红包详细信息，描述这一个红包的所有相关信息
        struct RedEnvelopInfo {
            // 红包的份数
            uint16 count;
    
                  // 红包剩余的份数
            uint16 remainCount;
    
            // 是否是公开红包（公开红包所有人都可以抢，非公开红包只有指定人可以抢）
            bool isPublic;
    
                    // 红包创建者地址
            address creator;
    
            // 红包中token的合约地址
            address tokenAddr;
    
            // 红包创建时间
            uint256 createTime;
    
            // 红包中总共有多少个token
            uint256 money;
    
            // 红包中还剩余多少个token
            uint256 remainMoney;
    
            // 红包候选人信息（某个地址是否允许打开该红包）
            mapping (address => bool) candidates;
    
            // 打开红包的信息（某个地址打开了这个红包并且得到了多少个token）
            mapping (address => uint256) recipientInfos;
    
            // 过期红包领取的信息（某个地址打开了过期红包并且得到了多少个token）
            mapping (address => uint256) luckydrawInfos;
        }
    
        // 红包编号与红包详细信息的对应关系（传入某个红包编号就能快速获取该红包的详细信息）
        mapping (uint256 => RedEnvelopInfo) public redEnvelopInfos;
    
        // 初始化红包合约，设置红包编号为0
        constructor() public {
            counter = 0;
        }
    
        // 初始化曼巴币的合约地址，因为创建红包合约的时候还没创建曼巴币合约
        // 所以需要告诉红包DAPP合约赠送的曼巴币的合约地址多少
        // 曼巴币合约里最后调用的方法就是这个方法
        function initBonusCoin(address initTokenAddr) external {
            // 只允许初始化一次就不允许任何人再修改这个地址了
            require(mambaCoinAddr == address(0), "Already Initialized");
            mambaCoinAddr = initTokenAddr;
        }
    
        // 创建红包事件
        event Create(uint256 envelopId, uint exprTime);
        
        // 打开红包事件
        event Open(uint256 envelopId, uint256 money, uint256 remainMoney, uint16 remainCount);
    
        // 打开过期红包事件（24小时候后，用24个曼巴币打开）
        event LuckyDraw(uint256 envelopId, uint256 money, uint256 remainMoney, uint16 remainCount);
    
        // 回撤红包事件（红包创建者在24小时候后，可以主动撤回剩余红包里的token）
        event DrawBack(uint256 envelopId, uint256 money);
    
        // 创建红包调用的方法
        function create(address tokenAddr, uint256 money, uint16 count, address[] memory candidates) external payable returns (uint256) {
            ......
        }
    
        // 打开红包调用的方法
        function open(uint256 redEnvelopId) external returns (uint256) {
            ......
        }
    
        // 打开过期红包调用的方法
        function luckydraw(uint256 redEnvelopId) external returns (uint256) {
            ......
        }
    
        // 撤回过期红包调用的方法
        function drawback(uint256 redEnvelopId) external returns (uint256) {
            ......
        }
        
        // 查询红包信息调用的方法
        function info(uint256 redEnvelopId) external view returns (address, address, uint256, uint256, uint16, uint16, bool, uint) {
            ......
        }
    
        // 查询某个用户打开红包的信息调用的方法
        function record(uint256 redEnvelopId, address candidate) external view returns (bool, uint256, uint256) {
            ......
        }
    }
    

上面就是红包DAPP的代码，最上面部分主要是定义了counter（红包编号）和redEnvelopInfos（红包编号和红包详细信息对应关系）这两个状态值。

**实际上发红包和领红包就是维护好这两个状态值即可，而下面的create（创建红包）、open（打开红包）、luckydraw（打开过期红包）和drawback（回撤过期红包）就是控制这两个状态值的四个方法，这四个可以修改红包DAPP状态值的方法又同时对应了四个Event（Create，Open，LuckyDraw，DrawBack）。**

下面我们看看create创建红包的方法

    function create(address tokenAddr, uint256 money, uint16 count, address[] memory candidates) external payable returns (uint256) {
        // 检查输入参数，红包的份数必须大于0
        require(count > 0, "Invalid count");
        // 装入红包里token的金额必须大于等于份数（实际上等于就行）
        require(money >= count, "Invalid money");
    
        // 获取红包编号
        uint256 envelopId = counter;
    
        // 保存红包详细信息
        RedEnvelopInfo storage p = redEnvelopInfos[envelopId];
        p.count = count;
        p.remainCount = count;
        p.creator = msg.sender;
        p.tokenAddr = tokenAddr;
        p.createTime = block.timestamp;
        p.money = money;
        p.remainMoney = money;
            
            // 如果传入参数里有红包候选人，则是私有红包
        if (candidates.length > 0) {
            p.isPublic = false;
            // 红包信息中的候选人状态为true，开红包的时候会检查
            for (uint i=0; i<candidates.length; i++) {
                p.candidates[candidates[i]] = true;
            }
        // 如果没有红包候选人，则设置为公开红包
        } else {
            p.isPublic = true;
        }
    
        // 红包编号+1
        counter = counter + 1;
    
        // 将红包创建者的token划转到红包合约的地址下
        if (tokenAddr != address(0)) {
        // 检查token的合约地址如果不是0的话则代表是ERC20token
            IERC20 token = IERC20(tokenAddr);
            // 检查该token是否给红包合约授权足够数量
            require(token.allowance(msg.sender, address(this)) >= money, "Token allowance fail");
            // 检查是否可以成功将该token转到红包合约地址下
            require(token.transferFrom(msg.sender, address(this), money), "Token transfer fail");
        } else {
        // 如果token合约地址是0的话，则代表是BNB或者ETH，其数量通过msg.value校验
            require(money <= msg.value, "Insufficient ETH");
        }
     
        // 发送创建红包事件
        emit Create(envelopId, p.createTime+EXPIRE_DAY);
        return envelopId;
    }
    

上面就是创建红包的方法，发送红包的人调用的就是该方法。**其本质就是创建了一个红包的详细信息（RedEnvelopInfo），同时将它和红包编号（envelopId）对应起来，在链上储存了一个红包编号与红包详细信息的对应关系（redEnvelopInfos）的记录。**

不过需要注意的是，在做任何状态值修改之前一定要做好参数的校验（代码中的require部分），因为合约是公开在链上的，任何一点没有校验的地方都容易成为“科学家”攻击的目标。

下面我们看看open打开红包的方法：

    function open(uint256 redEnvelopId) external returns (uint256) {
        // 验证是一个合法的红包编号，因为address的默认值是address(0)
          require(redEnvelopInfos[redEnvelopId].creator != address(0), "Invalid ID");
        // 验证这个红包还有剩余份数
          require(redEnvelopInfos[redEnvelopId].remainCount > 0, "No share left");
        // 验证该用户是否开过，每个人只能打开一次，且uint的默认值是0
          require(redEnvelopInfos[redEnvelopId].recipientInfos[msg.sender] == 0, "Already opened");
        
        // 如果该红包不是公开的话，还需要判断用户的钱包地址是否在候选名单中
          if (!redEnvelopInfos[redEnvelopId].isPublic) {
              require(redEnvelopInfos[redEnvelopId].candidates[msg.sender], "Invalid candidate");
          }
        
          // 计算一个开启红包的随机数（参考下面函数）
          uint256 amount = _calculateRandomAmount(redEnvelopInfos[redEnvelopId].remainMoney, redEnvelopInfos[redEnvelopId].remainCount);
        
          // 修改红包合约中的状态值
        // 减掉红包详细信息中的剩余金额
          redEnvelopInfos[redEnvelopId].remainMoney = redEnvelopInfos[redEnvelopId].remainMoney - amount;
          // 减掉红包详细信息中的剩余份数
        redEnvelopInfos[redEnvelopId].remainCount = redEnvelopInfos[redEnvelopId].remainCount - 1;
          // 维护该用户打开红包得到的token数量
        redEnvelopInfos[redEnvelopId].recipientInfos[msg.sender] = amount;
        
          // 将该数量的token转给开红包的用户
          _send(redEnvelopInfos[redEnvelopId].tokenAddr, payable(msg.sender), amount);
        
          // 如果红包合约还有足够的曼巴币的话
          if (IERC20(mambaCoinAddr).balanceOf(address(this)) >= BONUS_MAMB + BONUS_MAMB) {
            // 向领红包的人和开红包的人各发送8个曼巴币
              require(IERC20(mambaCoinAddr).transfer(msg.sender, BONUS_MAMB), "Transfer MAMB failed");
              require(IERC20(mambaCoinAddr).transfer(redEnvelopInfos[redEnvelopId].creator, BONUS_MAMB), "Transfer MAMB failed");
          }
          // 发送打开红包事件
          emit Open(redEnvelopId, amount, redEnvelopInfos[redEnvelopId].remainMoney, redEnvelopInfos[redEnvelopId].remainCount);
          return amount;
    }
    
    function _send(address tokenAddr, address payable to, uint256 amount) private {
        if (tokenAddr == address(0)) {
            // 如果是BNB或者ETH，直接发送
            require(to.send(amount), "Transfer ETH failed");
        } else {
            // 如果是其他ERC20代币，则调用ERC20的transfer方法发送
            require(IERC20(tokenAddr).transfer(to, amount), "Transfer Token failed");
        }
    }
    
    // 计算随机数算法（参考微信红包）
    function _random(uint256 remainMoney, uint remainCount) private view returns (uint256) {
       // 用了区块的时间戳，难度和高度做随机数的seed
       // 随机数算法参考微信红包，随机金额取值范围为1到平均能分到token数量的2倍
       return uint256(keccak256(abi.encode(block.timestamp + block.difficulty + block.number))) % (remainMoney / remainCount * 2) + 1;
    }
    
    function _calculateRandomAmount(uint256 remainMoney, uint remainCount) private view returns (uint256) {
        uint256 amount = 0;
        if (remainCount == 1) {
            // 如果剩余份数只剩一份，那剩下的token全给该用户
            amount = remainMoney;
        } else if (remainCount == remainMoney) {
            // 如果剩余份数=剩余的token数量，那每个人只能分到1个token
            amount = 1;
        } else if (remainCount < remainMoney) {
            // 其他情况用_random函数计算随机数
            amount = _random(remainMoney, remainCount);
        }
        return amount;
    }
    

上面就是红包DAPP的open方法了，用户领取红包调用的就是该方法。与create方法一样，也是先校验参数，然后修改合约状态值，最后转token。

在链上写合约比较爽的就是，不太需要注意事务的原子性，因为区块链已经保证了这一点。比如上述”open”方法中，如果用户同一时刻调用了两次open方法，在web2的开发中，一般做法是先开启一个transaction，对该记录加锁，判断其状态通过后再继续后面的操作，最后commit释放锁。而在合约开发中直接判断即可。

如果create和open方法你仔细看过了，后面两个luckydraw和drawback方法也大同小异，这里我就不贴出来了，大家感兴趣的自己去看。

### 测试及部署

红包合约写完之后，可以部署到本地的测试网络中去测试下，工具的选择可以参考[这里](https://ethereum.org/en/developers/local-environment/)，我本人用的是[brownie](https://eth-brownie.readthedocs.io/en/stable/)。（一开始鄙人部署的是ETH，测试了创建、打开红包的gas费太贵，后来改为部署到BSC了。部署BSC的时候我用了[BSC-Studio](https://github.com/ObsidianLabs/BSC-Studio)，它把所有测试部署相关的功能都集成好了）

合约部署完普通用户是没法直接用的，所以你需要做一个web3的网页给用户使用，同时链上的数据你可能也需要同步下来给用户做展示。因此你本地调试完合约之后，可以部署合约到公共的testnet去与web联调，testnet可以在[这里](https://ethereum.org/en/developers/docs/networks/#testnet-faucets)申请faucets（测试代币）。

关于同步链上数据，条件允许的话可以自己运行一个节点用于获取链上数据，也可以使用公共的服务，例如Ethereum可以选择[Infura](https://infura.io/)，像BSC或者Polygon都提供了自己的API服务让开发者方便的获取链上数据，大家可以官网自行查询。联调完成后部署mainnet上线即可。

### 红包DAPP网站

以红包DAPP这个项目为例，创建红包、打开红包、打开过期红包和回撤红包这几个功能页面都在[这里](https://redenvelop.app/GiftNow)了，这个页面不会去调用任何中心化服务器的接口，因为全是与链的交互。

如果要查询所有红包的列表以及某个红包的详情，可以访问这个[红包列表页面](https://redenvelop.app/RedEnvelop/)，这个页面展示的数据就是后端服务从链上同步下来数据，并且提供了接口给前端web做的展示，页面同时也提供了bscscan的链接，方便大家去与链上数据做对比。

### 代码改进

这个红包DAPP合约因为是鄙人练手写的，在这段期间看了一些项目代码之后，有几个地方觉得是可以优化的。

*   状态值的精简
    
    实际上红包DAPP需要存的状态值并不需要这么多，例如仅存一个remainCount即可，而不需要存count，因为第一次设置的remainCount就是count的值。有代码洁癖的话，其实状态值有很多都可以精简掉。
    
*   红包候选人地址列表
    
    目前哪些地址可以打开该红包，鄙人是将整个候选地址全部存入到链上，这样做实际上很浪费存储空间，也比较费gas。实际上可以用默克尔树证明，在创建红包的时候仅存入所有候选地址的默克尔树的root hash，然后open红包的方法里使用默克尔树证明即可判断该用户是否可以打开红包。（NFT whitelist的通用做法）
    
*   打开红包的时候判断是否为合约调用
    
        require(tx.origin == msg.sender, "XRC: contract is not allowed to mint.");
        
    
    上面是冷兔合约中的mint代码，通过tx.origin和msg.sender的判断即可判断是否为合约调用，如果是合约调用的话，实际上调用的合约可以拿到像返回值的一些信息来判断是否需要revert这个transaction。其实可以在红包DAPP中的每个调用方法里加上这个判断。
    
*   开红包的地方不应该return数量
    
    这个是People社区@duu的建议，如果return了数量，那别人可以设置一个合约，用来专门开这个红包，当发现打开红包的token数量没有达到预期的话，可以不停重试来保证可以开到目标数量的红包。
    
*   计算红包金额的随机函数
    
    也是People社区@duu的建议，大家可以看到上面开红包的随机函数用到的seed是“block.timestamp + block.difficulty + block.number”，理论上这三个数也可以被矿工作恶，如果创建的红包金额过大又被盯上的话，可以提前计算得到一个相对较大的红包金额。如果需要相对严谨随机的话可以考虑使用chainlink，不过使用成本又高了。
    

总之目前这个红包DAPP合约没有大问题，gas费因为在BSC上也没有多少。同时领取红包一般有候选人验证，所以科学家要搞的话首先得进候选人；即便进入了候选人，科学家费大力气能够开到最大金额的红包最多只有平均值的2倍，考虑到一般发送红包的金额都不大，所以科学家们肯定是没动力的了。

红包DAPP的反思
---------

红包DAPP项目并不成功，反思如下：

### 产品需求上

*   没有实现项目方或者大V可以通过红包DAPP公开透明的做市场活动
    

没有达成的原因是项目方并没有使用红包DAPP产品的初始动机，而有初始动机的人只是领红包的用户，只有用户关心他参与的这次活动是不是透明公正。而发红包的人却不会这么想，发红包的人首先想的就是通过DAPP发红包能带来什么利益？既然原来直接转账的活动方式就能够给我带来足够的流量，并且还保留转给指定人员的权利，那我为什么要用你这个产品呢？

*   没有实现朋友同事之间可以通过红包DAPP来抢crypto
    

如果是朋友同事之间，需要有使用场景，例如微信群那样。而红包DAPP这个项目并没有一个使用场景。并且Crypto的熟人圈子很小，大多数是telegram和discord这样的非熟人圈子，所以非熟人之间发红包的需求也不大。同时就网站而言，红包DAPP的UI和交互本人也并没有投入资源进行优化，虽然基本功能都有，但并不太方便普通用户使用。

**所以整个项目从需求上来讲就没切中要害，从产品功能去推导需求，这也算是工程师的通病吧。手里拿个锤子，眼里都是钉子。**

### 激励设计上

红包DAPP经济模型设计也比较失败，假设红包DAPP很流行，使用的人很多，曼巴币因为可以开过期红包也存在了价值。这时肯定会有很多所谓“科学家”去自动刷出很多曼巴币，再用曼巴币去开那些真正有值钱的过期红包。而此时普通持有曼巴币的用户根本开不到过期红包，就会导致曼巴币对普通用户来说失去了价值。

尽管曼巴币的发行上十分公开透明，只要领红包就可以赠送，但是完全的公开透明并不代表就是好事，而且完全从code上来控制这种薅毛的情况也做不到。

![](https://storage.googleapis.com/papyrus_images/9311c09feb01fcd48d51be5ae8d12524d882c070926aad13dd044276ca4e9f45.png)

![](https://storage.googleapis.com/papyrus_images/dea9e1ccae768f9658f703b36bfcc7ee5675ea88192b1142245d4484dda51bbe.png)

上面截图是推特上冷兔项目的易总对我的回复，起因是我在[上篇文章](https://mirror.xyz/0x8eC46A6d1AFda45f5A334Fe85016FB39D9D8Caee/hCt9gtL6GLdmrbKgtccpLGXfh1oSfA81OA-76iRSKa8)中对冷兔white list分配不透明的质疑。对于易总坦荡的回复本人表示理解与敬佩，因为一个项目的成功与否需要各方资源的支持，在centralized和decentralized中间需要找到平衡，甚至可能需要在白与黑之间找到平衡，水至清则无鱼，如果为了去中心化而去中心化，就会滑向另外一个极端，特别是在创业初期的时候更是这样。

**总之，红包DAPP只是个人的练手项目，投入的资源和精力也不多，所以总的来说还好，最关键的是能给我带来反思就好了。Web3.0机会有很多，我们所有从业者都会有美好的未来。**

最后
--

### 求关注

[本人推特](https://twitter.com/nelsonie)Follower已经超过3000了，十分感谢各位，也希望大家多多关注！

### 历史文章

**学习调研系列：**

[《Terra及Luna的经济模型简介》](https://mirror.xyz/0x8eC46A6d1AFda45f5A334Fe85016FB39D9D8Caee/7cDe57TyBH4eUg3Kfxa1ekzu8XCbmk6xDLK0rZvi8fA)

[《NFT及OpenSea交易背后的技术分享》](https://mirror.xyz/0x8eC46A6d1AFda45f5A334Fe85016FB39D9D8Caee/O3hpbibMf9vLNz6p80YUriU8Bf3bEaJWvRL49FGAgAc)

[《冷兔NFT项目发售的学习分享》](https://mirror.xyz/0x8eC46A6d1AFda45f5A334Fe85016FB39D9D8Caee/hCt9gtL6GLdmrbKgtccpLGXfh1oSfA81OA-76iRSKa8)

**也许会踩坑系列：**

[《Render Network学习记录》](https://mirror.xyz/0x8eC46A6d1AFda45f5A334Fe85016FB39D9D8Caee/JmSGCuIwrxmCUQ5gNSsFHk2_qsWxfi6VE0-VZOZ5UG8)

[《SOS项目学习记录》](https://mirror.xyz/0x8eC46A6d1AFda45f5A334Fe85016FB39D9D8Caee/eBWOsbAqp1MqhlLrLfRrGznkbcofjv_H-tTn_zUrP3c)

[《Multiverse学习记录》](https://mirror.xyz/0x8eC46A6d1AFda45f5A334Fe85016FB39D9D8Caee/Sov5G7mdXDFzyJYzAjr5yR-BW7qx1eW-SJTbMfqqF6M)

### 彩蛋

本期彩蛋就是发一些曼巴币给大家吧，纪念一下伟大的Kobe Bryant。精神上希望大家通过Mamba Coin记住Kobe Bryant和曼巴精神，实际点的话也可以用Mamba Coin去开之前我发的未开完的[红包](https://redenvelop.app/RedEnvelop/34)（里面还有400多个Doge）。

这次发曼巴这个空气币的红包，就不要大家留BSC的钱包地址了，这次红包是一个公开红包，所有人都可以领（需要一点BNB付gas费），也希望大家多帮忙转发点赞，后续我通知各位领红包地址的时候可能才会有提醒，感谢各位🙏

曼巴币的BSC上合约地址：[0xab1ec376ccad1bfd5e3409a60bd30988af2dc765](https://bscscan.com/token/0xab1ec376ccad1bfd5e3409a60bd30988af2dc765)

**Mamba Never Out!**

---

*Originally published on [Xing](https://paragraph.com/@xing824/dapp)*
