# Lottery Game
* PancakeSwap Lottery - 跟一般認知的樂透很像

* Pool Together - 主打不會輸錢的遊戲,每周開獎一次,把大家的錢拿去集資放貸,贏家獲得放貸的利息

* APR 已經是穩定幣可以獲得的最高收益了
* 這期沒中獎會自動進入下一期 (彩票遞延)
* 防止開獎前才進場,兩個禮拜內提款有懲罰
* blockhash 作為隨機數的缺點是 blockhash 是已知的
<!-- * Curve


* https://yearn.finance/#/vaults
* https://www.convexfinance.com/stake
* Mochi 戰爭
* FOMO3D 非常有名的資金盤遊戲 - Key 的價格大約依照 Key 發行量的平方根遞增

* 被破解方式是占用區塊讓時間內沒有人可以買 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 流通
 -->
# Pool Together 運作 (不含跨鏈)
1. 存款時,與 AAVE 互動,將 USDC 存入 AAVE 並質押 AAVE。
2. 每周開獎一次,開獎需在兩周內領取,否則存入下一期(充公)。
3. 中獎機率的計算方式根據 TWAB(time weighted average balance),如果存款 500 一半的時間,平均下來就是 250 。
4. TWAB 越大會有越多的 picks 提供兌獎。
5. 中獎計算方式:

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 的運作有幾個重點要搞清楚:

- 存 USDC 進入 YieldSourcePrizePool, 會獲得1:1的 tickets 作為憑證
- prizeSplitStrategy 計算 PrizePool 裡面的價值,大於 tickets 的部分是獎金,因此把作為獎金的 tickets 轉移到 reserve。
- 然後 reserve 再把 tickets 轉移到 prizeDistributor
- 使用者可以從 prizeDistributor 那裡獲得作為獎金的 tickets
到目前為止有個疑點是為甚麼中間會有 reserve 存在,還有就是每一輪的抽獎金額不同,這些資訊被寫進 prizeDistribution,具體怎麼紀錄的還不清楚。
- 假設 tickets 已經到達 prizeDistributor,中獎的人要怎麼提領 tickets 呢?

- 紀錄過去的提領紀錄,確保不會重複提領
```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;
}
```
- 兌獎方式


```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 的費用

```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 作為收益來源。

* 從 etherscan 的交易紀錄來看

* 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 的借貸利率一樣。但是...


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

## 分獎勵給 prizeDistributor
* 所有的獎金都是以 ticket 的形式從 prizeDistributor 發送出去。透過 periphery/PrizeFlush 呼叫 prizeSplitStrategy 把 ticket 從 prizePool 送到 reserve 然後再到 prizeDistributor。
* 每次 ticket 經過 reserve,都會觸發 _checkpoint(),並更新 reserve accumulators。便可以計算兩個時間點中間,prize pool 總共獲得的多少收益。我猜測項目是以 reserve accumulators 的紀錄計算每一輪次可以發放多少獎金。

```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。

1. send BNB to VENUS VBNB and receive vBNB ([bscscan])(https://bscscan.com/tx/0x90d52854f04bbb2f5745545c6f9f4f64e2d44eb87460fae4ba85f6f1ceeda578)

2. claim XVS ([bscscan])(https://bscscan.com/tx/0x82518876d6c485b1498dd399d3a7d88a9bfae6f8e6f42cfbb50b5de3834581a7)
在不同 pool 進行 supply 和 borrow 會分開轉帳,可能有多筆

3. swap XVS for BUSD via PancakeSwap ([bscscan])(https://bscscan.com/tx/0xec6a84c97baa4871368fbdb52f7db0b3b75fbada48f3e9e3be98702fe31c07d2)

```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

- 下注下個五分鐘的開始到結束的價格是上升還是下降( 非下注當下,對莊家最安全 ),價格由 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 規則進行的
- 賠率表示方式:

+: 下注 100 能贏的金額
-: 要贏 100 必須下注的金額
- 盤口: 輸贏盤、大小分盤、讓分盤
- 走地盤、滾球: 賠率隨著賽況變動的盤口
### pari mutuel
- 法國開始,源自於賽馬,單純根據彩池大小分派獎金,因此下注時不知道可能獲得的收益。對於莊家風險較小。
## 美國樂透
- 領獎期限: 180 天內
- 獎金稅率: 600 以上就要繳費,稅率最高 40 %
- 種類:
- Mega Millions美國大百萬樂透:
- 一般號 70 選 5 + 特別號 25 選 1
- 美西時間每周二、周五晚上8點,開獎當日7:45pm截止購票
- Megaplier: 加 1 元購買,獲得翻倍的權利
- 
- PowerBall:
- 一般號 52 選 5 + 特別號 10 選 1
- 一注2元,加1元購買 powerplay,powerplay中獎可以翻倍
- 
## 新加坡樂透
- TOTO
- 買五個號碼(49選1),開獎開6個號碼加一個特別號
- 
- 4D
- 0000~9999選一個號碼
- 
- 中獎拿固定獎金
- 
### 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