---
# System prepended metadata

title: Lottery Game

---

# Lottery Game
* PancakeSwap Lottery - 跟一般認知的樂透很像
![](https://i.imgur.com/AvBTgrF.png)


* Pool Together - 主打不會輸錢的遊戲，每周開獎一次，把大家的錢拿去集資放貸，贏家獲得放貸的利息
![](https://i.imgur.com/0U2LNBo.png)
    * APR 已經是穩定幣可以獲得的最高收益了
    * 這期沒中獎會自動進入下一期 (彩票遞延)
    * 防止開獎前才進場，兩個禮拜內提款有懲罰
    * blockhash 作為隨機數的缺點是 blockhash 是已知的
    
<!-- * Curve
    ![](https://i.imgur.com/HWcs32C.png)
    ![](https://i.imgur.com/lXK2lAR.png)
    * https://yearn.finance/#/vaults
    * https://www.convexfinance.com/stake
    * Mochi 戰爭

* FOMO3D 非常有名的資金盤遊戲 - Key 的價格大約依照 Key 發行量的平方根遞增
![](https://i.imgur.com/pADmIO2.png)
    * 被破解方式是占用區塊讓時間內沒有人可以買 key
    
* Olympus Dao (3, 3) - https://app.olympusdao.finance/#/dashboard
    * 算法穩定幣的一種，其他類別都陷入死亡螺旋，OHM 算堅持比較久
    * OHM is backed by DAI but not pegged
    * 價格低於一美元，會回購並銷毀代幣，高於一美元會大量增發給 OHM 質押者，增發的基礎來自於高溢價，用大量增發 OHM 增加供給

    * DeFi 2.0: 控制流動性
    * 用 LP token 折價買入 Bond，使協議可以控制流動性 (Bond 有幾天的鎖定期)，為協議提供收入同時減少 OHM 流通
    ![](https://i.imgur.com/70AOZOE.png) -->


# Pool Together 運作 (不含跨鏈)
1. 存款時，與 AAVE 互動，將 USDC 存入 AAVE 並質押 AAVE。
2. 每周開獎一次，開獎需在兩周內領取，否則存入下一期(充公)。
3. 中獎機率的計算方式根據 TWAB(time weighted average balance)，如果存款 500 一半的時間，平均下來就是 250 。
4. TWAB 越大會有越多的 picks 提供兌獎。
5. 中獎計算方式: 
![](https://i.imgur.com/IwQtP9b.png)
5. 獎金分布: 不同 tier 可能中獎人數不同，依照每個 tier 的分布決定該 tier 中獎的人可以分多少。
6. Pool 代幣，只能影響 governance (合約相關參數、或是要不要新開質押池之類的)，質押 Pool 或 Pool/ETH LP 會有更多 Pool。但是 Pool 本身沒有價值基礎，一直增發只會提高賣壓 (上限 10m)。

# Pool Together Code Review
- V4 的合約有分成 core, periphery, Timelocks, TWAB delegator, RNG services。Core 的部分是核心，負責定義整個項目的核心規則，其餘的比較像是輔助執行核心的計算工具，也就是其實可以另外寫合約呼叫core。

- Pool Together 的運作有幾個重點要搞清楚:
![](https://i.imgur.com/6f2f4Yd.png)

    - 存 USDC 進入 YieldSourcePrizePool, 會獲得1:1的 tickets 作為憑證
    - prizeSplitStrategy 計算 PrizePool 裡面的價值，大於 tickets 的部分是獎金，因此把作為獎金的 tickets 轉移到 reserve。
    - 然後 reserve 再把 tickets 轉移到 prizeDistributor
    - 使用者可以從 prizeDistributor 那裡獲得作為獎金的 tickets

到目前為止有個疑點是為甚麼中間會有 reserve 存在，還有就是每一輪的抽獎金額不同，這些資訊被寫進 prizeDistribution，具體怎麼紀錄的還不清楚。

- 假設 tickets 已經到達 prizeDistributor，中獎的人要怎麼提領 tickets 呢?

![](https://i.imgur.com/PurdOZG.png)

- 紀錄過去的提領紀錄，確保不會重複提領

```solidity=
    /// @notice Maps users => drawId => paid out balance
    mapping(address => mapping(uint256 => uint256)) internal userDrawPayouts;
```

- 輸入中獎序號，drawCalculator 負責計算金額，確定大於之前提領的總額，然後取得多的部分
    
```solidity=
    /* ============ External Functions ============ */

    /// @inheritdoc IPrizeDistributor
    function claim(
        address _user,
        uint32[] calldata _drawIds,
        bytes calldata _data // 中獎序號的部分
    ) external override returns (uint256) {
        
        (uint256[] memory drawPayouts, ) = drawCalculator.calculate(_user, _drawIds, _data); // neglect the prizeCounts since we are not interested in them here

        // 迴圈是因為可以一次提領好幾輪的獎金
        for (uint256 payoutIndex = 0; payoutIndex < drawPayoutsLength; payoutIndex++) {
            ...

            // helpfully short-circuit, in case the user screwed something up.
            require(payout > oldPayout, "PrizeDistributor/zero-payout");

            unchecked {
                payoutDiff = payout - oldPayout;
            }

            _setDrawPayoutBalanceOf(_user, drawId, payout);

            totalPayout += payoutDiff;

            ...
        }

        _awardPayout(_user, totalPayout);

        return totalPayout;
    }
```

- drawCalculator 根據 PrizeDistributionSource 取得獎池資訊

```solidity=
    struct PrizeDistribution {
        uint8 bitRangeSize; // 一個彩票號碼佔多少 bit，ex: 4 bits
        uint8 matchCardinality; // 有幾個彩票號碼，ex: 8
        uint32 startTimestampOffset; // 計算 TWAB 開始時間
        uint32 endTimestampOffset; // 計算 TWAB 結束時間
        uint32 maxPicksPerUser; // 一個人最多幾張彩票
        uint32 expiryDuration; // 兌獎時間最多距離該輪開始多久
        uint104 numberOfPicks; // 獎池最多可以有幾張彩票
        uint32[16] tiers; // 獎項等級，最多 16 級
        uint256 prize; // 獎池大小
    }
```

- drawCalculator 計算環節，
1. 把 _pickIndicesForDraws decode 成 pickIndices，是雙重陣列是因為不同 draw 會有不同組序號
2. 計算在不同 draw 的 TWAB，並 normalized 成比例
3. 雜湊使用者的地址

```solidity=
    function calculate(
        address _user,
        uint32[] calldata _drawIds,
        bytes calldata _pickIndicesForDraws
    ) external view returns (uint256[] memory, bytes memory) {
        
        // 解碼抽獎序號
        uint64[][] memory pickIndices = abi.decode(_pickIndicesForDraws, (uint64 [][]));
        ...
        
        // 取得獎金分發資訊
        IPrizeDistributionSource.PrizeDistribution[] memory _prizeDistributions = prizeDistributionSource
            .getPrizeDistributions(_drawIds);

        // 取得 normalized 過的 TWAB
        uint256[] memory userBalances = _getNormalizedBalancesAt(_user, draws, _prizeDistributions);

        // 雜湊使用者的錢包地址
        bytes32 _userRandomNumber = keccak256(abi.encodePacked(_user));
        
        //計算獎金
        return _calculatePrizesAwardable(
                userBalances,
                _userRandomNumber,
                draws,
                pickIndices,
                _prizeDistributions
            );
    }
```

- _calculatePrizesAwardable 計算 totalUserPicks，是 user 在每一個 draw 中能提交序號的最大上限
```solidity=
 function _calculatePrizesAwardable(
        uint256[] memory _normalizedUserBalances,
        bytes32 _userRandomNumber,
        IDrawBeacon.Draw[] memory _draws,
        uint64[][] memory _pickIndicesForDraws,
        IPrizeDistributionSource.PrizeDistribution[] memory _prizeDistributions
    ) internal view returns (uint256[] memory prizesAwardable, bytes memory prizeCounts) {
        
        // 獎金
        uint256[] memory _prizesAwardable = new uint256[](_normalizedUserBalances.length);
        // 獎項等級
        uint256[][] memory _prizeCounts = new uint256[][](_normalizedUserBalances.length);

        // calculate prizes awardable for each Draw passed
        for (uint32 drawIndex = 0; drawIndex < _draws.length; drawIndex++) {
            
            // 計算 user 在每一個 draw 中能提交序號的最大上限
            uint64 totalUserPicks = _calculateNumberOfUserPicks(
                _prizeDistributions[drawIndex],
                _normalizedUserBalances[drawIndex]
            );
            
            ...
            
            (_prizesAwardable[drawIndex], _prizeCounts[drawIndex]) = _calculate(
                _draws[drawIndex].winningRandomNumber, // 每一輪抽獎的中獎數字
                totalUserPicks, // 序號上限
                _userRandomNumber, // user 地址雜湊出的隨機數
                _pickIndicesForDraws[drawIndex], // 提交序號
                _prizeDistributions[drawIndex] //  獎金分配
            );
        }
    }
```

- _calculateNumberOfUserPicks 利用TWAB計算提交序號上限
```solidity=
    function _calculateNumberOfUserPicks(
        IPrizeDistributionSource.PrizeDistribution memory _prizeDistribution,
        uint256 _normalizedUserBalance
    ) internal pure returns (uint64) {
        return uint64((_normalizedUserBalance * _prizeDistribution.numberOfPicks) / 1 ether);
    } // 1 ether = 1e18

    /**
     * @notice Calculates the normalized balance of a user against the total supply for timestamps
     * @param _user The user to consider
     * @param _draws The draws we are looking at
     * @param _prizeDistributions The prize tiers to consider (needed for draw timestamp offsets)
     * @return An array of normalized balances
     */
    // 計算標準化的 TWAB
    function _getNormalizedBalancesAt(
        address _user,
        IDrawBeacon.Draw[] memory _draws,
        IPrizeDistributionSource.PrizeDistribution[] memory _prizeDistributions
    ) internal view returns (uint256[] memory) {
        
        ...
        
        // 先算使用者的 TWAB
        uint256[] memory balances = ticket.getAverageBalancesBetween(
            _user,
            _timestampsWithStartCutoffTimes,
            _timestampsWithEndCutoffTimes
        );
        
        // 再算全部使用者的 TWAB
        uint256[] memory totalSupplies = ticket.getAverageTotalSuppliesBetween(
            _timestampsWithStartCutoffTimes,
            _timestampsWithEndCutoffTimes
        );

        uint256[] memory normalizedBalances = new uint256[](drawsLength);

        // divide balances by total supplies (normalize)
        // balances / supplies 就是 normalizedBalances 
        for (uint256 i = 0; i < drawsLength; i++) {
            if(totalSupplies[i] == 0){
                normalizedBalances[i] = 0;
            }
            else {
                normalizedBalances[i] = (balances[i] * 1 ether) / totalSupplies[i]; // 乘 1 ether 是因為 solidity 沒有浮點數，之後要除掉
            }
        }

        return normalizedBalances;
    }
```

- _calculate 計算獲得獎金和獎項等級
```solidity=
    function _calculate(
        uint256 _winningRandomNumber,
        uint256 _totalUserPicks,
        bytes32 _userRandomNumber,
        uint64[] memory _picks,
        IPrizeDistributionSource.PrizeDistribution memory _prizeDistribution
    ) internal pure returns (uint256 prize, uint256[] memory prizeCounts) {

        // create bitmasks for the PrizeDistribution
        // 把中獎號碼做成 masks，給兌獎時需要
        uint256[] memory masks = _createBitMasks(_prizeDistribution);
        
        ...

        // for each pick, find number of matching numbers and calculate prize distributions index
        for (uint32 index = 0; index < picksLength; index++) {
            // 注意這兩個條件，序號要小於 _totalUserPicks，並且都是遞增，代表如果計算來的 _totalUserPicks 是 20，那麼能提交的序號最多是 1-20
            require(_picks[index] < _totalUserPicks, "DrawCalc/insufficient-user-picks");

            if (index > 0) {
                require(_picks[index] > _picks[index - 1], "DrawCalc/picks-ascending");
            }

            // hash the user random number with the pick value
            // 把序號和 _userRandomNumber 雜湊成兌獎號碼
            uint256 randomNumberThisPick = uint256(
                keccak256(abi.encode(_userRandomNumber, _picks[index]))
            );
            
            // 計算該號碼中幾等獎
            uint8 tiersIndex = _calculateTierIndex(
                randomNumberThisPick,
                _winningRandomNumber,
                masks
            );

            ...
        }

        ...

        // return the absolute amount of prize awardable
        // div by 1e9 as prize tiers are base 1e9
        // 獎項累積分成 * 獎池
        prize = (prizeFraction * _prizeDistribution.prize) / 1e9;
    }
```

- 兌獎方式

![](https://i.imgur.com/4IAwEEM.png)

![](https://i.imgur.com/NEs7oys.png)

```solidity=
    function _createBitMasks(IPrizeDistributionSource.PrizeDistribution memory _prizeDistribution)
        internal
        pure
        returns (uint256[] memory)
    {
        uint256[] memory masks = new uint256[](_prizeDistribution.matchCardinality);
        masks[0] =  (2**_prizeDistribution.bitRangeSize) - 1;

        for (uint8 maskIndex = 1; maskIndex < _prizeDistribution.matchCardinality; maskIndex++) {
            // shift mask bits to correct position and insert in result mask array
            masks[maskIndex] = masks[maskIndex - 1] << _prizeDistribution.bitRangeSize;
        }

        return masks;
    }
```

```solidity=
    function _calculateTierIndex(
        uint256 _randomNumberThisPick,
        uint256 _winningRandomNumber,
        uint256[] memory _masks
    ) internal pure returns (uint8) {
        uint8 numberOfMatches = 0;
        uint8 masksLength = uint8(_masks.length);

        // main number matching loop
        for (uint8 matchIndex = 0; matchIndex < masksLength; matchIndex++) {
            uint256 mask = _masks[matchIndex];
            
            // 如果兩個不相等就跳出迴圈，相等就繼續下一輪
            if ((_randomNumberThisPick & mask) != (_winningRandomNumber & mask)) {
                // there are no more sequential matches since this comparison is not a match
                ...
            }

            // else there was a match
            numberOfMatches++;
        }

        return masksLength - numberOfMatches;
    }
```

- 對於 prizeDistributor 來說，只要有 drawBuffer 和 prizeDistribution source 就可以正常發放獎勵。drawBuffer 負責透過 drawBeacon 向 chainlink 取得隨機數字做為中獎號碼，中獎號碼必須隱私，因為 hash 的結果是固定的，如果中獎號碼公開，作弊者可以先計算需要多少 pick 就能獲得多少獎項。

    - 任何人都可以啟動 startDraw，startDraw 必須準備 link 代幣支付預言機取得 rng 的費用

    ![](https://i.imgur.com/tIBQ5YB.png)

```solidity=

    /**
     * @notice RNG Request
     * @param id          RNG request ID
     * @param lockBlock   Block number that the RNG request is locked
     * @param requestedAt Time when RNG is requested
     */
    struct RngRequest {
        uint32 id;
        uint32 lockBlock;
        uint64 requestedAt; //要求時間
    }

    /// @inheritdoc IDrawBeacon
    function startDraw() external override requireCanStartDraw {
        
        // 取得代幣地址和要求數量
        (address feeToken, uint256 requestFee) = rng.getRequestFee();
        
        // 送錢，allowance 代表不是立即轉帳，而是先授權給 rngService 使用
        if (feeToken != address(0) && requestFee > 0) {
            IERC20(feeToken).safeIncreaseAllowance(address(rng), requestFee);
        }

        // 送出請求
        (uint32 requestId, uint32 lockBlock) = rng.requestRandomNumber();
        rngRequest.id = requestId; // ID
        rngRequest.lockBlock = lockBlock; // ??
        rngRequest.requestedAt = _currentTime(); //要求時間

        emit DrawStarted(requestId, lockBlock);
    }
```

- 在啟動新的 draw 之前，要先確認幾件事情
```solidity=
    // modifier 是修飾子，_ 代表函式的執行步驟，可以看到在進行真正的執行步驟前，會進行兩項審查
    modifier requireCanStartDraw() {
        
        // 上個 beacon 必須結束
        require(_isBeaconPeriodOver(), "DrawBeacon/beacon-period-not-over");
        // 不可以有 request 過的紀錄
        require(!isRngRequested(), "DrawBeacon/rng-already-requested");
        _;
    }

    function _isBeaconPeriodOver() internal view returns (bool) {
        return _beaconPeriodEndAt() <= _currentTime();
    }

    function isRngRequested() public view override returns (bool) {
        return rngRequest.id != 0;
    }
```

- 向 rng 取得 randomNumber 不是立即的，也有可能會索取失敗。失敗的話，需要取消這次的 request 並且重新發送一個。
```solidity=
    /// @inheritdoc IDrawBeacon
    function cancelDraw() external override {
        // 超過 rng 的等待時間了
        require(isRngTimedOut(), "DrawBeacon/rng-not-timedout");
        uint32 requestId = rngRequest.id;
        uint32 lockBlock = rngRequest.lockBlock;
        delete rngRequest; // 取消 request
        emit DrawCancelled(requestId, lockBlock);
    }
```

- 如果 rng 成功，該 draw 就算完成。
```solidity=
    /// @inheritdoc IDrawBeacon
    function completeDraw() external override requireCanCompleteRngRequest {
        uint256 randomNumber = rng.randomNumber(rngRequest.id); // 取得隨機數字
        ...

        // create Draw struct
        IDrawBeacon.Draw memory _draw = IDrawBeacon.Draw({
            winningRandomNumber: randomNumber, // 中獎號碼
            drawId: _nextDrawId, // draw 編號
            timestamp: rngRequest.requestedAt, // must use the startAward() timestamp to prevent front-running
            // 發送要求時間
            beaconPeriodStartedAt: _beaconPeriodStartedAt,
            // 這個 beacon 可以開始的時間
            beaconPeriodSeconds: _beaconPeriodSeconds
            // beacon 持續時間
        });

        drawBuffer.pushDraw(_draw); // 把新的 draw 推上 drawBuffer

        // to avoid clock drift, we should calculate the start time based on the previous period start time.
        // 計算下個 beacon 可以開始的時間
        uint64 nextBeaconPeriodStartedAt = _calculateNextBeaconPeriodStartTime(
            _beaconPeriodStartedAt,
            _beaconPeriodSeconds,
            _time
        );
        beaconPeriodStartedAt = nextBeaconPeriodStartedAt;
        nextDrawId = _nextDrawId + 1; // 更新 draw 編號

        // Reset the rngRequest state so Beacon period can start again.
        // 刪除舊的 request
        delete rngRequest;

        emit DrawCompleted(randomNumber);
        emit BeaconPeriodStarted(nextBeaconPeriodStartedAt);
    }
```

## Yield Source 流程
* 當使用者將 USDC 存入 prizePool 之後，會獲得 ticket 作為憑證，ticket 同時也是中獎可以領取的獎勵。prizePool 獲得 usdc 後存入 AtokenYieldSource，再由 YieldSource 存入 AAVE 的 lendingPool。AtokenYieldSource 會給 prizePool shares 作為憑證。YieldSource 不能更改，意味只能以 AAVE USDC 作為收益來源。

![](https://i.imgur.com/GOE7zQf.png)

* 從 etherscan 的交易紀錄來看
![](https://i.imgur.com/f8k7ig6.png)

* code
```solidity=
1. PrizePool
// 起始的 function
function depositTo(address _to, uint256 _amount)
    external
    override
    nonReentrant
    canAddLiquidity(_amount)
{
    _depositTo(msg.sender, _to, _amount);
}


function _depositTo(address _operator, address _to, uint256 _amount) internal
{
    require(_canDeposit(_to, _amount), "PrizePool/exceeds-balance-cap");

    ITicket _ticket = ticket;
    
    // user 存入 USDC
    _token().safeTransferFrom(_operator, address(this), _amount);
    // 發 ticket 給 user
    _mint(_to, _amount, _ticket);

    _supply(_amount);

    emit Deposited(_operator, _to, _ticket, _amount);
}

function _supply(uint256 _mintAmount) internal override {
    _token().safeIncreaseAllowance(address(yieldSource), _mintAmount);
    yieldSource.supplyTokenTo(_mintAmount, address(this)); // 呼叫 yieldSource
}

// 任何人都可以呼叫這個函數，但不透過 prizePool 就不能參加抽獎
2. yieldSource
function supplyTokenTo(uint256 mintAmount, address to) external override nonReentrant {
uint256 shares = _tokenToShares(mintAmount); // 計算分發多少 share

require(shares > 0, "ATokenYieldSource/shares-gt-zero");
_depositToAave(mintAmount); // 存 USDC 進入 AAVE
_mint(to, shares); // 

emit SuppliedTokenTo(msg.sender, shares, mintAmount, to);
}

function _depositToAave(uint256 mintAmount) internal {
address _underlyingAssetAddress = _tokenAddress(); // USDC address
ILendingPool __lendingPool = _lendingPool(); // AAVE lending pool
IERC20 _depositToken = IERC20(_underlyingAssetAddress);

// 從 prizePool 取得 USDC
_depositToken.safeTransferFrom(msg.sender, address(this), mintAmount); 
// 存入 lending pool 獲得 aUSDC
__lendingPool.deposit(_underlyingAssetAddress, mintAmount, address(this), REFERRAL_CODE);
}

```

* 相反的，如果使用者想退出，就呼叫 withdrawFrom ，然後銷毀 ticket
```solidity=
function withdrawFrom(address _from, uint256 _amount)
    external
    override
    nonReentrant
    returns (uint256)
{
    ITicket _ticket = ticket;

    // burn the tickets
    _ticket.controllerBurnFrom(msg.sender, _from, _amount);

    // redeem the tickets
    uint256 _redeemed = _redeem(_amount);

    _token().safeTransfer(_from, _redeemed);

    emit Withdrawal(msg.sender, _from, _ticket, _amount, _redeemed);

    return _redeemed;
}
```
* 可以發現，每一次有人要參加遊戲，就只是把 USDC 存進 AAVE 裡，那這樣的報酬應該跟 AAVE 的借貸利率一樣。但是...

![](https://i.imgur.com/MZX1wCk.png)

![](https://i.imgur.com/44Bz2Ya.png)

這邊 Pool Together 計算出來的報酬率 13.67%，明顯高於 AAVE。而文件寫的APR計算方式是:

![](https://i.imgur.com/WlQISh1.png)



## 分獎勵給 prizeDistributor
* 所有的獎金都是以 ticket 的形式從 prizeDistributor 發送出去。透過 periphery/PrizeFlush 呼叫 prizeSplitStrategy 把 ticket 從 prizePool 送到 reserve 然後再到 prizeDistributor。

* 每次 ticket 經過 reserve，都會觸發 _checkpoint()，並更新 reserve accumulators。便可以計算兩個時間點中間，prize pool 總共獲得的多少收益。我猜測項目是以 reserve accumulators 的紀錄計算每一輪次可以發放多少獎金。

![](https://i.imgur.com/E1bxVtB.png)

```solidity=
1. PrizeFlush
// 呼叫 prizeSplitStrategy，之後計算 reserve 有的 token 數量，並轉移到 prize Distributor
function flush() external override onlyManagerOrOwner returns (bool) {
    // Captures interest from PrizePool and distributes funds using a PrizeSplitStrategy.
    // 獎金送到 reserve
    strategy.distribute();

    // After funds are distributed using PrizeSplitStrategy we EXPECT funds to be located in the Reserve.
    // 計算 reserve 有多少 ticket
    IReserve _reserve = reserve;
    IERC20 _token = _reserve.getToken();
    uint256 _amount = _token.balanceOf(address(_reserve));

    // IF the tokens were succesfully moved to the Reserve, now move them to the destination (PrizeDistributor) address.
    if (_amount > 0) {
        ...

        // Create checkpoint and transfers new total balance to PrizeDistributor
        // reserve 轉移 ticket 到 prizeDistributor
        _reserve.withdrawTo(_destination, _amount);

        emit Flushed(_destination, _amount);
        return true;
    }

    return false;
}

2. prizeSplitStrategy
// 計算 prizePool 累積多少 interest 了，如果有多的，就轉移到 reserve
// 
function distribute() external override returns (uint256) {
    uint256 prize = prizePool.captureAwardBalance();

    ...
    
    // 讓 prize pool 鑄造 ticket 給 reserve
    // 值得注意的是 這裡並不的將 prize 分量的 ticket 分出去，而是 (prize - 總發放 ticket)
    uint256 prizeRemaining = _distributePrizeSplits(prize);

    emit Distributed(prize - prizeRemaining);

    return prize;
}

3. prizePool
function captureAwardBalance() external override nonReentrant returns (uint256) {
    ...

    // 計算 yieldSource 等值多少 USDC
    uint256 currentBalance = _balance();
    
    // 計算總利息利息
    uint256 totalInterest = (currentBalance > ticketTotalSupply)
        ? currentBalance - ticketTotalSupply
        : 0;
    
    // 計算比上次計算多出來的利息
    uint256 unaccountedPrizeBalance = (totalInterest > currentAwardBalance)
        ? totalInterest - currentAwardBalance
        : 0;

    ...
    
    // 回傳總共的利息
    // 不回傳多出來的利息是因為每次鑄造新的 ticket 給 reserve，currentAwardBalance 都會降低
    return currentAwardBalance;
}

4. prizeSplit & PrizeSplitStrategy
function _distributePrizeSplits(uint256 _prize) internal returns (uint256) {
    ...

    for (uint256 index = 0; index < prizeSplitsLength; index++) {
        
        ...

        // 呼叫 prizePool 鑄造 tickets 給 reserve
        // 只有 prizePool 有 ticket 的控制權
        _awardPrizeSplitAmount(split.target, _splitAmount);

        // Update the remaining prize amount after distributing the prize split percentage.
        _prizeTemp -= _splitAmount;
    }

    return _prizeTemp;
}

function _awardPrizeSplitAmount(address _to, uint256 _amount) internal override {
    ...
    prizePool.award(_to, _amount);
    ...
}

6. prizePool
function award(address _to, uint256 _amount) external override onlyPrizeStrategy {
    
    ...

    uint256 currentAwardBalance = _currentAwardBalance;

    ...
    
    // 計算總發放 interest
    unchecked {
        _currentAwardBalance = currentAwardBalance - _amount;
    }

    ITicket _ticket = ticket;

    _mint(_to, _amount, _ticket);

    emit Awarded(_to, _ticket, _amount);
}

5. reserve
/// @inheritdoc IReserve
// 建立 checkpoint，並轉移 ticket 到 prize distributor
function withdrawTo(address _recipient, uint256 _amount) external override onlyManagerOrOwner {
    _checkpoint();

    withdrawAccumulator += uint224(_amount);

    token.safeTransfer(_recipient, _amount);

    emit Withdrawn(_recipient, _amount);
}

/// @notice Records the currently accrued reserve amount.
// 每次 ticket 經過 reserve 都會更新 reserveAccumulators
// reserveAccumulators 紀錄該時間點的總 ticket 數量，包含之前時間點的加總
function _checkpoint() internal {
    ...

    /**
     * IF tokens have been deposited into Reserve contract since the last checkpoint
     * create a new Reserve balance checkpoint. The will will update multiple times in a single block.
     */
    // amount 必須增加
    if (_balanceOfReserve + _withdrawAccumulator > newestObservation.amount) {
        uint32 nowTime = uint32(block.timestamp);

        // checkpointAccumulator = currentBalance + totalWithdraws
        uint224 newReserveAccumulator = uint224(_balanceOfReserve) + _withdrawAccumulator;

        // IF newestObservation IS NOT in the current block.
        // CREATE observation in the accumulators ring buffer.
        // 時間必須增加
        if (newestObservation.timestamp != nowTime) {
            reserveAccumulators[_nextIndex] = ObservationLib.Observation({
                amount: newReserveAccumulator,
                timestamp: nowTime
            });
            ...
        }
        // ELSE IF newestObservation IS in the current block.
        // UPDATE the checkpoint previously created in block history.
        else {
            reserveAccumulators[newestIndex] = ObservationLib.Observation({
                amount: newReserveAccumulator,
                timestamp: nowTime
            });
        }
        ...
    }
}

```

## 取得每一期獎金資訊 (還不太清楚)
* PrizeTierHistory 紀錄每個 drawId 的獎金, PrizeDistributionFactory 結合 PrizeTier 的資料推到 PrizeDistributionBuffer 上面。但是誰把資料傳給 PrizeTierHistory 的還不清楚。

```solidity=
// @inheritdoc IPrizeTierHistory
// 只有 owner 能 push 資料
function push(PrizeTier calldata nextPrizeTier) override external onlyManagerOrOwner {
    _push(nextPrizeTier);
}
```

owner address: [0xDa63D70332139E6A8eCA7513f4b6E2E0Dc93b693](https://etherscan.io/address/0xda63d70332139e6a8eca7513f4b6e2e0dc93b693)

這是一個 proxy factory，還不清楚是幹嘛的

## [Launch Architecture](https://v4.docs.pooltogether.com/assets/images/V4_Launch_Architecture-353dc5476e169fd1d97c5ef24fdc9f0b.png)


## Yield Source Design
- 流程介紹: 在 venus 存入 BUSD 可以獲得利息，同時 venus 會給與 XVS 作為獎勵，把 XVS 拿去 pancake swap 換成 BUSD 後重複存入 venus。

    ![](https://i.imgur.com/vZR8XG7.png)

    1. send BNB to VENUS VBNB and  receive vBNB ([bscscan])(https://bscscan.com/tx/0x90d52854f04bbb2f5745545c6f9f4f64e2d44eb87460fae4ba85f6f1ceeda578)
        ![](https://i.imgur.com/GfDulpU.png)
    2. claim XVS ([bscscan])(https://bscscan.com/tx/0x82518876d6c485b1498dd399d3a7d88a9bfae6f8e6f42cfbb50b5de3834581a7)
    在不同 pool 進行 supply 和 borrow 會分開轉帳，可能有多筆
        ![](https://i.imgur.com/HmqDhtl.png)
    3. swap XVS for BUSD via PancakeSwap ([bscscan])(https://bscscan.com/tx/0xec6a84c97baa4871368fbdb52f7db0b3b75fbada48f3e9e3be98702fe31c07d2)
        ![](https://i.imgur.com/cfEHfWG.png)

```solidity=
- VENUS/Vtoken
// mint -> mintInternal -> mintFresh
function mintFresh(address minter, uint mintAmount) internal returns (uint, uint) {
        
        ...

        /*
         *  We call `doTransferIn` for the minter and the mintAmount.
         *  Note: The vToken must handle variations between BEP-20 and BNB underlying.
         *  `doTransferIn` reverts if anything goes wrong, since we can't be sure if
         *  side-effects occurred. The function returns the amount actually transferred,
         *  in case of a fee. On success, the vToken holds an additional `actualMintAmount`
         *  of cash. 同時處理 BNB 和 BEP20 情況
         */
    
        // 存入代幣，回傳實際收到的數值
        vars.actualMintAmount = doTransferIn(minter, mintAmount);

        /*
         * We get the current exchange rate and calculate the number of vTokens to be minted:
         *  mintTokens = actualMintAmount / exchangeRate
         */
    
        /*    Exp 是一個 struct，專門處理浮點數運算??
            struct Exp {
            uint mantissa;
            }
            
            基本上精確到 18 位，先做分子的相乘在乘以 1e18 在除分母，處理 solidity 沒有浮點數的問題
        */ 
        (vars.mathErr, vars.mintTokens) = divScalarByExpTruncate(vars.actualMintAmount, Exp({mantissa: vars.exchangeRateMantissa}));
        require(vars.mathErr == MathError.NO_ERROR, "MINT_EXCHANGE_CALCULATION_FAILED");

        ...

        /* We write previously calculated values into storage */
        totalSupply = vars.totalSupplyNew;
        accountTokens[minter] = vars.accountTokensNew;

        /* We emit a Mint event, and a Transfer event */
        emit Mint(minter, vars.actualMintAmount, vars.mintTokens);
        emit Transfer(address(this), minter, vars.mintTokens);

        ...

        return (uint(Error.NO_ERROR), vars.actualMintAmount);
    }

- VENUS/comptroller
    // 對不同的 holders、對不同的 vtoken pool、對 borrow&supply 分開給予 XVS
    function claimVenus(address[] memory holders, VToken[] memory vTokens, bool borrowers, bool suppliers) public {
        ...
        for (uint i = 0; i < vTokens.length; i++) {
            ...
            if (borrowers) {
                ...
                for (j = 0; j < holders.length; j++) {
                    // 把可以分發的 XVS 紀錄進 venusAccrued[address] 裡面
                    distributeBorrowerVenus(address(vToken), holders[j], borrowIndex);
                    // 根據 venusAccrued 給予 XVS，grant成功回傳0
                    venusAccrued[holders[j]] = grantXVSInternal(holders[j], venusAccrued[holders[j]]);
                }
            }
            if (suppliers) {
                ...
                // same as top
                }
            }
        }
    }

- PancakeSwap
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,// 多筆 path 給予最好的交易路徑
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        amounts = PancakeLibrary.getAmountsOut(factory, amountIn, path);
        
        // 限制滑價或是交易路徑必須比直接換好
        require(amounts[amounts.length - 1] >= amountOutMin, 'PancakeRouter: INSUFFICIENT_OUTPUT_AMOUNT');
        ...
        _swap(amounts, path, to);
    }

- yearn finance/harvest
    // 領取 staking 的獎勵並換成想要的代幣
    function harvest() public {
        ...
        // 收割 voter 的獎勵
        IVoterProxy(proxy).harvest(gauge);
        
        // 計算獲得的 crv
        uint256 _crv = IERC20(crv).balanceOf(address(this));
        if (_crv > 0) {
            
            // 10% 拿去重複投票
            uint256 _keepCRV = _crv.mul(keepCRV).div(FEE_DENOMINATOR);
            IERC20(crv).safeTransfer(voter, _keepCRV);
            _crv = _crv.sub(_keepCRV);

            // 90% 換成 USDC
            IERC20(crv).safeApprove(dex, 0);
            IERC20(crv).safeApprove(dex, _crv);
            
            // 路徑被決定好 CRV-> WETH -> USDC
            address[] memory path = new address[](3);
            path[0] = crv;
            path[1] = weth;
            path[2] = usdc;
            
            // 不限制滑價
            Uni(dex).swapExactTokensForTokens(_crv, uint256(0), path, address(this), now.add(1800));
        }
        
        ...
        
        // USDC 換成 EURS
        uint256 _usdc = IERC20(usdc).balanceOf(address(this));
        if (_usdc > 0) {
            
            ...

            address[] memory path = new address[](2);
            path[0] = usdc;
            path[1] = eurs;

            Uni(uniswap).swapExactTokensForTokens(_usdc, uint256(0), path, address(this), now.add(1800));
        }
        
        // EURS 存入 curve 的 liquidity pool
        uint256 _eurs = IERC20(eurs).balanceOf(address(this));
        if (_eurs > 0) {
            IERC20(eurs).safeApprove(curve, 0);
            IERC20(eurs).safeApprove(curve, _eurs);
            ICurveFi(curve).add_liquidity([_eurs, 0], 0);
        }
        
        ...
    }
```

## 博奕遊戲構思
- 首先，pool together 的號碼機制比較像是等兌獎號碼來撞你自己的號碼。因為開獎號碼是由預言機提供亂數，理論上有無限多種可能。而使用者的 ticket 是由地址和 picks 亂數而成，如果每一期的份額不變，使用者的彩票號碼也會不變。picks 是由系統決定，是中獎號碼的組合數。

    這樣做的好處是節省 gas fee，使用除了存入 USDC 以外不需要多餘的操作，彩票號碼已經與份額綁定了。

- 大樂透的玩法是 6個 1-49 的號碼，不可重複，總共有 10,068,347,520 種組合，以 bit 來看，在 2^33 ~ 2^34 之間，最多就是 8 bytes。概念上在原本 pool together 的比對機制中，只能比較前 8 個數字，如果超過，就會發生某些數字，大樂透開獎號碼完全無法觸碰到。

- 另一個問題是怎麼解決有兩個人同時中獎的情形。其實回頭來看，pool together 並沒有預防這件事情，他在前面的步驟已經阻擋這個可能了嗎?

    keccak256 是產生 256 bits 亂數的 function，兌獎時產生 mask 只比較前 32 個 bit 是否一樣，因此這裡 picks 的供給上限就是 2^32。但是把地址和 picks 亂數產生的彩票號碼是會散在 2^256 的空間中，因此有可能發生兩個彩票號碼不一樣但是 mask 的部分完全一樣的情形(碰撞機率可以忽略, sha1 的碰撞機率 < 10e-19)。


### PancakeSwap Prediction
![](https://i.imgur.com/hWzJ6PI.png)

- 下注下個五分鐘的開始到結束的價格是上升還是下降( 非下注當下，對莊家最安全 )，價格由 chainlink 決定
- 一輪只能下一次注
- 莊家抽 3%，平均一輪下注 20BNB( 8000 USD )
- 獎金根據對手獎池決定，莊家沒有風險，抽 3% 水錢
- 價格不變得話，莊家通吃，對手盤沒下注而且輸的話，莊家通吃，贏的話還沒錢拿

```solidity=
    
    // 狀態宣告: 買漲還是買跌
    enum Position {
        Bull,
        Bear
    }

    // round 的開始、結束、停止下注、兩邊下注金額、獎池大小等資料
    struct Round {
        uint256 epoch;
        uint256 startTimestamp;
        uint256 lockTimestamp;
        uint256 closeTimestamp;
        int256 lockPrice;
        int256 closePrice;
        uint256 lockOracleId;
        uint256 closeOracleId;
        uint256 totalAmount;
        uint256 bullAmount;
        uint256 bearAmount;
        uint256 rewardBaseCalAmount;
        uint256 rewardAmount;
        bool oracleCalled;
    }
    
    // 使用者的下注方向、金額、是否領獎
    struct BetInfo {
        Position position;
        uint256 amount;
        bool claimed; // default false
    }

    // 每個輪次、每個使用者的下注狀態
    mapping(uint256 => mapping(address => BetInfo)) public ledger;

    // 每個輪次的獎池狀態
    mapping(uint256 => Round) public rounds;

    // 每個使用者參加的輪次
    mapping(address => uint256[]) public userRounds;


```

```mermaid
sequenceDiagram
participant User
participant Governor
participant Prediction
participant oracle
participant LendingPool

loop startRound
Governor ->> Prediction: executeRound()
Prediction ->> oracle: (uint80 currentRoundId, int256 currentPrice) = _getPriceFromOracle()

Prediction ->> Prediction: _safeLockRound(currentEpoch, currentRoundId, currentPrice);
Prediction ->> Prediction: _safeEndRound(currentEpoch - 1, currentRoundId, currentPrice);
Prediction ->> Prediction: _calculateRewards(currentEpoch - 1);
Prediction ->> Prediction: _safeStartRound(currentEpoch+1);
end

loop bet
User ->> Prediction: betBear(uint256 epoch) / betBull(uint256 epoch)
Prediction ->> Prediction: depositAmount = balanceOf( address(this) )
Prediction ->> Prediction: update Round Info and Betting Info
Prediction ->> LendingPool: deposit( uint256 depositAmount )
end

loop claim
User ->> Prediction: reward = claim(uint256[] calldata epochs)
Prediction ->> LendingPool: withdraw( uint256 reward / exchangeRate + 1 )
Prediction ->> User: IERC20( tokenAddress ).safeTransfer( msg.sender, reward )
end
```
        
## 運彩形式 Fixed odds vs pari mutuel
- 運彩是由項目方做莊擔保，開出賭盤，賭客下注並獲得中獎獎勵的遊戲。項目方最大的風險是開盤風險，運彩公司開出來的賠率隱含兩隊的獲勝機率，當運彩公司錯估勝率時，就會被套利。EX: 假設市場認定A隊的勝率是70%，運彩公司開出來的勝率是50%，大家會下注A隊，運彩公司無法以B隊的資金支付A隊的資金。
    - 即使運彩公司發現後改變賠率，仍然會有損失。因為下注成功的收益是根據購買當下的賠率決定，因此運彩公司也會隨時改變賠率。
    - 台灣運彩很黑，抽水大約 12%，國外5%。

- 運彩公司的收益是水錢。EX:兩邊賠率都是 1.75:1.75。隱含的獲勝機率分別都是 0.5714，相加 114%，這代表你是為 114% 的勝率支付價錢，但是現實獲勝機率是 100%。多出來的 14% 是運彩公司的收益。

- 打水: 兩間運彩公司開出來的賠率不一致時的套利方式，分別購買兩邊賠率較高的隊伍賺取收益。

    - 相關套利基本上是根據 fixed odds 規則進行的

- 賠率表示方式: 
    ![](https://i.imgur.com/e2MieKd.png)
    +: 下注 100 能贏的金額
    -: 要贏 100 必須下注的金額
    
- 盤口: 輸贏盤、大小分盤、讓分盤
- 走地盤、滾球: 賠率隨著賽況變動的盤口

### pari mutuel
- 法國開始，源自於賽馬，單純根據彩池大小分派獎金，因此下注時不知道可能獲得的收益。對於莊家風險較小。


## 美國樂透

- 領獎期限: 180 天內
- 獎金稅率: 600 以上就要繳費，稅率最高 40 %
- 種類: 
    - Mega Millions美國大百萬樂透: 
        - 一般號 70 選 5 + 特別號 25 選 1
        - 美西時間每周二、周五晚上8點，開獎當日7:45pm截止購票
        - Megaplier: 加 1 元購買，獲得翻倍的權利
        - ![](https://i.imgur.com/v4QUKkv.png)

    - PowerBall:
        - 一般號 52 選 5 + 特別號 10 選 1
        - 一注2元，加1元購買 powerplay，powerplay中獎可以翻倍
        - ![](https://i.imgur.com/p0w2egC.png)


## 新加坡樂透
- TOTO
    - 買五個號碼(49選1)，開獎開6個號碼加一個特別號
    - ![](https://i.imgur.com/iJ9mg9P.png)
- 4D
    - 0000~9999選一個號碼
    - ![](https://i.imgur.com/0F8uKBA.png)
    - 中獎拿固定獎金
    - ![](https://i.imgur.com/QCEk9Be.png)



### NFT 的形式
- 以 gas fee 來考量，Ethereum 上一張 NFT 價格 30 美金，polygon 上面 gas fee 0.03 美金， BSC 則是 0.3 美金，以這個角度 NFT 的實行還算可行

- NFT 的困難點在於沒有辦法確認各獎項的實際中獎人數。Pool Together 因為使用者不能選號，每個人的號碼是由 地址和份額 產生，確保的隨機性，用機率控制獎項的中獎人數。

    - NFT 因為有了選號的權利，因此沒有隨機性，及有可能發生同一個獎項多人領取的情形，如果系統預先辨別，會發生第一個人領完之後後面的人不能領的狀況。

    - 解決辦法分為鏈上和鏈下兩種，鏈下是由項目方先行計算可能的中獎人數，並推上區塊鏈系統，確保獎金分配。缺點是不夠自動化。

        鏈上則是由中獎的人提交證明，待之後再由系統平均分配獎金給有提交的人。缺點是會有審核期，EX: 可以提交證明的期限是一個禮拜，等一個禮拜後才能收到獎金。
        
- 如果是以玩家利息作為獎金，且 NFT 形式的情況下，如何做到時間加權的效果，可能的方式就是越晚買越貴，ex: 一張彩票開始價格 100 U，經過一半時間後價格變為200，價格漲幅和時間剩餘比例成反比，這樣才是 fair game，中獎機率和提供利息的比例相同，假設利率不變。
    - 假設三天開獎一次，4*24*3=288 個 15 分鐘，彩票金額會是一開始的 288 倍。
    - 如果搭配一個禮拜的兌獎期，其實這段時間可以白嫖利息，大概來說，一開始就買的人打 0.7 折購買彩券就可以了。
    - 以玩家獎金做為獎金不需要考慮這個問題。

```solidity=
1. Lottery.sol
    // Represents the status of the lottery
    enum Status { 
        NotStarted,     // The lottery has not started yet
        Open,           // The lottery is open for ticket purchases 
        Closed,         // The lottery is no longer open for ticket purchases
        Completed       // The lottery has been closed and the numbers drawn
    }
    // All the needed info around a lottery
    struct LottoInfo {
        uint256 lotteryID;          // ID for lotto
        Status lotteryStatus;       // Status for lotto
        uint256 prizePoolInCake;    // The amount of cake for prize money
        uint256 costPerTicket;      // 一張彩票的價格 Cost per ticket in $cake
        uint8[] prizeDistribution;  // 獎金分布 The distribution for prize money
        uint256 startingTimestamp;      // Block timestamp for star of lotto
        uint256 closingTimestamp;       // Block timestamp for end of entries
        uint16[] winningNumbers;     // 中獎號碼 The winning numbers
    }
    // Lottery ID's to info
    mapping(uint256 => LottoInfo) internal allLotteries_;

2. LotteryNFT.sol
    
    // 彩票的擁有者、數字、是否領獎、屬於哪一輪次
    struct TicketInfo {
        address owner;
        uint16[] numbers;
        bool claimed;
        uint256 lotteryId;
    }

    // 每個 ID 對應的狀態 Token ID => Token information 
    mapping(uint256 => TicketInfo) internal ticketInfo_;
    
    //
    // User address => Lottery ID => Ticket IDs
    mapping(address => mapping(uint256 => uint256[])) internal userTickets_;
```
```mermaid
sequenceDiagram
participant User
participant Governor

participant Lottery
participant LotteryNFT
participant YieldSourcePool

loop createNewLotto

Governor->>Lottery: 1. createNewLotto(<br>uint8[] calldata _prizeDistribution,<br>uint256 _prizePoolInCake,<br>uint256 _costPerTicket,<br>uint256 _startingTimestamp,<br>uint256 _closingTimestamp<br>)
Governor->>Lottery: 2. pushRealWorldLottteryNumber( uint256 _lotteryId, uint256[] lotteryNumber )
Governor->>Lottery: 3. createNewLotto(<br>uint8[] calldata _prizeDistribution,<br>uint256 _prizePoolInCake,<br>uint256 _costPerTicket,<br>uint256 _startingTimestamp,<br>uint256 _closingTimestamp<br>)

end 

loop Buy & Claim Ticket
User->>Lottery:1. batchBuyLottoTicket(<br>uint256 _lotteryId,<br>uint8 _numberOfTickets,<br>uint16[] calldata _chosenNumbersForEachTicket<br>)
Lottery ->>Lottery:costWithDiscount = costToBuyTicketsWithDiscount(_lotteryId, _numberOfTickets)
User ->>Lottery:usdc.transferFrom(<br>msg.sender, <br>address(this),<br>costWithDiscount<br>);
Lottery ->>LotteryNFT:ticketIds = batchMint(<br>msg.sender,<br>_lotteryId,<br>_numberOfTickets,<br>_chosenNumbersForEachTicket,<br>sizeOfLottery_<br>)
LotteryNFT ->>LotteryNFT:ticketInfo_[totalSupply_] = TicketInfo(<br>_to,<br>numbers,<br>false,<br>_lotteryId<br>)
Lottery ->>YieldSourcePool:deposit(address minter, uint costWithDiscount, address usdc)
User ->>Lottery:2. claimReward(uint256 _lotteryId, uint256 _tokenId)
Lottery ->>Lottery:matchingNumbers = _getNumberOfMatching(<br>nft_.getTicketNumbers(_tokenId),<br>allLotteries_[_lotteryId].winningNumbers<br>)
Lottery ->>Lottery:prizeAmount = _prizeForMatching(<br>matchingNumbers,<br>_lotteryId<br>);
User->>Lottery: 3. distributePrize(uint[] _lotteryIds )
Lottery ->>YieldSourcePool:withdraw(address minter, uint prizeAmount/exchangeRate+1, address usdc)
Lottery ->>User:usdc.safeTransfer(address(msg.sender), prizeAmount);
end
```

### 小結

|  | 樂透 | 二元選擇權 | 運彩 |
|-------- | -------- | -------- | -------- |
| 規則 | 對數字，按對中數字多寡分配獎金 | 預測價格漲跌，贏家拿走對手全部保證金 | 賭比賽結果勝負、大小分、讓分等等 |
| 頻率 | 一周 | 五分 ~ 一天 | 數天不等，看比賽 |
| 獎金來源 | 其他參賽者資金，該期彩券不保留| 對手盤資金 | 1. 固定賠率: 莊家資金<br>2. pari mutuel: 對手資金 |
| 資料源 | 現實世界樂透遊戲，項目方自行上傳 | 1. chainlink 有股市、外匯、期貨資料<br>2. DEX的時間平均價格 | chainlink: 包含賠率、比賽結果 |
| 對獎方式 | 1.鏈下計算中獎人數，官方上傳，馬上取得獎金<br>2.鏈上開獎後提供兌獎期(一周)<br>之後將獎金平均分配<br>3.頭獎類分獎池比例的有兌獎期<br>小獎固定獎金的可以馬上領走 | 立即 | 立即 |
|合約複雜程度| 中 | 低 | 高 |

成本: chainlink 一次費用 0.1 Link ( 14*0.1 )，上傳 data 費用: ETH(25), Polygon(0.03), BSC(0.3)



## 獎金分配設計
- 計算每人分到的獎金: 獎金除以人數
    - draw 178
      1(1):0
      2(4-1): 268817204 -> 11160*268817204 / 1e9 / 3 = 1000
      3(16-4): 0
      4(64-16): 215053763 -> 11160*215053763 / 1e9 / 48 = 50
      5(256-64): 172043010 -> 11160*172043010 / 1e9 / 192 = 10
      6(1024-256): 344086021 -> 11160*344086021 / 1e9 / 768 = 5
      prize: 11160000000

    - draw 180
      1(1): 139275766 -> 7180*139275766 / 1e9 / 1 = 1000
      2(4-1): 41782729 -> 7180*41782729 / 1e9 / 3 = 100
      3(16-4): 83565460 -> 7180*83565460 / 1e9 / 12 = 50
      4(64-16): 66852367 -> 7180*66852367 / 1e9 / 48 = 10
      5(256-64): 133704735 -> 7180*133704735 / 1e9 / 192 = 5
      6(1024-256): 534818942 -> 7180*534818942 / 1e9 / 768 = 5
      prize: 7180000000

- 因為每一次分配獎金的比例不是鏈上計算，看起來是項目調整過，讓每個tier的獎金維持固定金額
- 其實獎金不應該這麼乾淨，1e6以下都是0，是因為計算獎池的時候有用 MINIMUM_PICK_COST = 1000000 // 1 USDC 調整過。

## Deploy 合約的順序 ([Link](https://github.com/pooltogether/v4-mainnet/tree/main/deploy))
- BeaconTimelockTrigger 取代 L1TimelockTrigger
0. constants
```typescript=
export const DRAW_BUFFER_CARDINALITY = 255;
export const PRIZE_DISTRIBUTION_BUFFER_CARDINALITY = 180; // six months
export const PRIZE_DISTRIBUTION_FACTORY_MINIMUM_PICK_COST = 1000000 // 1 USDC
export const BEACON_START_TIME = Math.floor((new Date('2021-11-3T19:00:00.000Z')).getTime() / 1000)
export const BEACON_PERIOD_SECONDS = 86400; // one day
export const END_TIMESTAMP_OFFSET = 15 * 60 // 15 minutes
export const RNG_TIMEOUT_SECONDS = 2 * 3600 // 2 hours
export const EXPIRY_DURATION = 60 * 86400 // 2 months
export const TOKEN_DECIMALS = 6;
```

- v1.0.1
    - ATokenYieldSource
    - YieldSourcePrizePool
    - Ticket
    - PrizeSplitStrategy
    - Reserve
    - DrawBuffer
    - DrawBeacon
    - PrizeDistributionBuffer
    - DrawCalculator
    - DrawCalculatorTimelock
    - PrizeDistributor
    - PrizeFlush
    - L1TimelockTrigger : 被 BeaconTimelockTrigger 取代
    - PrizeTierHistory
- v1.1.0
    - PrizeDistributionFactory
    - BeaconTimelockTrigger
- v1.2.0:
    - TWABRewards
- v1.3.0: 
    - TWABDelegator
- v1.4.0:
    - PrizeDistributionFactory
    - drawCalculatorTimelock 
    - beaconTimelockTrigger 

