Fomo3D 源碼解析, 部署指南

Fomo3D 合約源碼分析

準備工作

環(huán)境準備 (用于調試合約)

  • git, nodejs, Chrome
  • ganache-cli, remix-ide

代碼 及 IDE

安裝好 Git 后, 下載源碼 git clone https://github.com/reedhong/fomo3d_clone.git
安裝好 nodejs 后, 使用 npm 安裝2個東西(建議使用國內鏡像源:cnpm)
npm install ganache-cli -g & npm install remix-ide -g

至于IDE 上的選擇, 只要 IDE 支持 sol 語法, 如 idea 就有 solidity 插件, 亦或者 vscode 也很棒, 而且中文支持比較好, 還對于大文件 js 及 json 打開速度比較快, 編輯也比較流暢( idea 可能是插件太多, 各種語法解析比較卡)

源碼結構

+-- interface    
|    +-- DiviesInterface.sol
|    +-- F3DexternalSettingsInterface.sol
|    +-- HourglassInterface.sol
|    +-- JIincForwarderInterface.sol
|    +-- JIincInterfaceForForwarder.sol
|    +-- PlayerBookInterface.sol
|    +-- PlayerBookReceiverInterface.sol
|    +-- TeamJustInterface.sol
|    +-- otherFoMo3D.sol
+-- library 
|    +-- F3DKeysCalcLong.sol
|    +-- F3Ddatasets.sol
|    +-- MSFun.sol  
|    +-- NameFilter.sol
|    +-- SafeMath.sol   
|    +-- UintCompressor.sol
+-- Divies.sol
+-- F3Devents.sol   
+-- F3DexternalSettings.sol 
+-- FoMo3Dlong.sol  
+-- Hourglass.sol   
+-- JIincForwarder.sol
+-- PlayerBook.sol  
+-- TeamJust.sol    
+-- modularLong.sol

以上就是 reed 大佬整理的源碼結構, 看到這么多文件, 心里感覺好慌, 別怕, 其實大多數(shù)文件都是擺設, 沒有太多邏輯代碼, 我們主要需要看的, 也就是那么幾個合約, 既然如此, 我們先排除一些用處不大, 非游戲關鍵核心的合約

各大收款合約

  • JIincForwarder.sol (JIincForwarderInterface 類型變量的實際引用), 用于向項目基金會轉賬
  • otherFoMo3D.sol (游戲 activate 前必須設置的 otherFomo 變量的實際引用), 向不知道哪個地址轉賬
  • Divies.sol (DeviesInterface 類型變量的實際引用), 用于 p3d 分紅

JIincForwarder.sol

這個合約就是向 基金會地址 轉發(fā) ether, 單獨寫一個中轉的好處就是靈活, 這個合約可以做到基金會地址安全轉移, 也就是說中途可以改變基金會的轉賬地址, 而這個過程需要新舊2個合約共同完成(舊.startMigration(新地址)--> 新.finishMigration(), 中途 舊方可以 舊.cancelMigration(), 而完成地址轉移后, 新地址完全替代舊地址 )
其中比較轉賬邏輯就是調用下面的這個接口對應的實際合約 的 deposit 方法

interface JIincInterfaceForForwarder {
    function deposit(address _addr) external payable returns (bool);
    function migrationReceiver_setup() external returns (bool);
}

至于現(xiàn)在這個基金會的地址到底是啥, 可以通過 status() 方法查看哦

otherFoMo3D.sol

這個合約很有意思, 或者說它的背后很有意思, 大家都想知道 其他的 fomo 到底是啥, 據(jù)說不是 soon 版
至于邏輯上, 這個 potSwap 的調用時機是在玩家買 key 的時候, 而它的作用, 我認為是游戲間的獎池交換
比如說, fomolong 共有100個 ether 買入, 那么就會有1%流向 otherFomo 的獎池, 同理, otherFomo 里應該也會有這個邏輯的存在, 這么做有啥用就交給大家自己思考了


interface otherFoMo3D {
    function potSwap() external payable;
}

fomo3Dlong 代碼: (fomo3Dlong本身也可以是一個 otherFomo, 甚至在 真正的otherFomo 里它的那個 otherFomo 就是 fomo3Dlong 也不一定)

function potSwap()
    external
    payable
{
    // setup local rID
    uint256 _rID = rID_ + 1;
    
    round_[_rID].pot = round_[_rID].pot.add(msg.value); // 獎池金額增加
    emit F3Devents.onPotSwapDeposit(_rID, msg.value);
}

Divies.sol

這部分是給 P3D 分紅的, 代碼很簡單, 就一個轉賬的調用, 調用時機上, 首先是買 key 的錢被瓜分時, 有它的一份, 其次當一輪 (Round) 結束后, 又會根據(jù)贏的隊伍來分配獎池, 抽出一部分給到 P3D

interface DiviesInterface {
    function deposit() external payable;
}

當然這其中如何給 P3D 分紅我還沒搞太懂, 大致流程貌似是: 買 key分紅 --> 調用 Divies 的 deposit 方法, Divies 合約中此方法無具體實現(xiàn)(空方法, 啥也不干, 就收錢) --> 預計什么時候會有 P3D 的玩家來調用這個合約的 distribute 方法, 而 這個方法的作用似乎是將 分紅轉來的錢拿去投入 P3D, 然后賣出, 根據(jù)傳入的百分比決定是否繼續(xù)投入或重復投入和售出多少次, 最后把錢提現(xiàn)回來(可能就沒多少了), 而錢通過10% 的分紅機制全給了 P3D 的用戶??? 這一塊一直不太懂, 而且這個方法的 調用時機不明, 調用時還增加了 時間限制和擁擠隊列的限制. 總的將這里面就是存在給 P3D 分紅的錢, 但這錢啥時候 給 P3D, 我還是沒猜出來.

3大合約

光是轉賬合約就感覺有些看不懂了, 真是頭疼啊, 只好把不懂的放下, 留待日后琢磨. 還是先分析游戲核心代碼吧

  • TeamJust.sol
  • PlayerBook.sol
  • FoMo3Dlong.sol

TeamJust.sol

首先看 TeamJust.sol , 這個是用來做權限控制的, 里面 除了與 muitiSig( 這個以后說 )相關的幾個方法, 也就是管理 admin 和 dev 了, 如 addAdmin removeAdmin, 而 isDev isAdmin 則是拿來給其他合約調用(比如 playerBook 的 onlyDevs)

function setup(address _addr)
   onlyDevs()
   public
{
   require( address(Jekyll_Island_Inc) == address(0) );
   Jekyll_Island_Inc = JIincForwarderInterface(_addr);
}  

經(jīng)過我的觀察發(fā)現(xiàn), 這個 teamJust 合約應該是比較后加的, 比如 fomo3Dlong 合約的激活就沒有使用, 而2個合約不同的對于 Jekyll_Island_Inc 的賦值也讓我推測這可能是較新的寫法. 我也覺得這種通過調用合約賦值的方式比較好, 所以在我整的 項目 fomo3d_truffle 中, 我把 activate 函數(shù)的用戶限制 也改成了 用 teamJust 來做, 而 其中的 playerBook 和 teamJust 實際合約地址也是通過 類似上面 setup 的方式 賦值, 這么做還有個好處就是可以通過 truffle 一鍵把這些合約部署且賦值, 而不是弄一個改源碼重新編譯這種測試起來比較麻煩的方式

PlayerBook.sol

這個合約主要是管理 玩家信息, 而玩家信息則分為 name, id, addr, id 是根據(jù)地址是否存在自增生成的, 而 name 則是通過 花錢注冊可用于推廣獲取提成的! 合約內大多方法都像個數(shù)據(jù)庫一樣均為 crud 操作, 夾帶的邏輯無非就是一些驗證, 其他的都比較少, 里面比較有意思的點就是 addGame

function addGame(address _gameAddress, string _gameNameStr)
    onlyDevs()
    public
{
    require(gameIDs_[_gameAddress] == 0, "derp, that games already been registered");
    
    if (multiSigDev("addGame") == true)
    {deleteProposal("addGame");
        gID_++;
        bytes32 _name = _gameNameStr.nameFilter();
        gameIDs_[_gameAddress] = gID_;
        gameNames_[_gameAddress] = _name;
        games_[gID_] = PlayerBookReceiverInterface(_gameAddress);
    
        games_[gID_].receivePlayerInfo(1, plyr_[1].addr, plyr_[1].name, 0);
        games_[gID_].receivePlayerInfo(2, plyr_[2].addr, plyr_[2].name, 0);
        games_[gID_].receivePlayerInfo(3, plyr_[3].addr, plyr_[3].name, 0);
        games_[gID_].receivePlayerInfo(4, plyr_[4].addr, plyr_[4].name, 0);
    }
}

這里是把 fomo3Dlong 的地址和名稱傳入, 然后就會通過接口向 fomo3Dlong 傳入幾個預設的玩家信息(來自 playerbook的構造方法), 而調用過這個方法后, registerNameXxxxFromDapp 這樣的方法才能不被 isRegisteredGame 攔截. 所以部署時, 這一步是必做的.

其他的幾個點: 可設置的注冊費用, 且費用被轉到基金會; 購買 key 邀請分紅總是和訪問的鏈接的推廣碼有關, 只有在無推廣碼時, 才從歷史中獲取 laff, 而 laff 每訪問一個推廣碼(并買了 key)都在改變

FoMo3Dlong.sol

主要合約啊, 先看下 所有的 state 變量

string constant public name = "FoMo3D Long Official";
string constant public symbol = "F3D";
uint256 private rndExtra_ = extSettings.getLongExtra();     // length of the very first ICO 
uint256 private rndGap_ = extSettings.getLongGap();         // length of ICO phase, set to 1 year for EOS.
uint256 constant private rndInit_ = 1 hours;                // round timer starts at this
uint256 constant private rndInc_ = 30 seconds;              // every full key purchased adds this much to the timer
uint256 constant private rndMax_ = 24 hours;                // max length a round timer can be  
uint256 public airDropPot_;             // person who gets the airdrop wins part of this pot
uint256 public airDropTracker_ = 0;     // incremented each time a "qualified" tx occurs.  used to determine winning air drop
uint256 public rID_;  

mapping (address => uint256) public pIDxAddr_;          // (addr => pID) returns player id by address
mapping (bytes32 => uint256) public pIDxName_;          // (name => pID) returns player id by name
mapping (uint256 => F3Ddatasets.Player) public plyr_;   // (pID => data) player data
mapping (uint256 => mapping (uint256 => F3Ddatasets.PlayerRounds)) public plyrRnds_;    // (pID => rID => data) player round data by player id & round id
mapping (uint256 => mapping (bytes32 => bool)) public plyrNames_; // (pID => name => bool) list of names a player owns.  (used so you can change your display name amongst any name you own)

mapping (uint256 => F3Ddatasets.Round) public round_;   // (rID => data) round data
mapping (uint256 => mapping(uint256 => uint256)) public rndTmEth_;      // (rID => tID => data) eth in per team, by round id and team id

mapping (uint256 => F3Ddatasets.TeamFee) public fees_;          // (team => fees) fee distribution by team
mapping (uint256 => F3Ddatasets.PotSplit) public potSplit_;     // (team => fees) pot split distribution by team

大部分都可以通過 變量名 猜出個大概, 實在不行可以搜索大致看一下哪里用了, 結合的先看一下, 其他都是各種數(shù)據(jù), 沒啥復雜的, 這里就主要看下 fees_ 和 potSplit_

// Team allocation percentages
// (F3D, P3D) + (Pot , Referrals, Community)
    // Referrals / Community rewards are mathematically designed to come from the winner's share of the pot.
fees_[0] = F3Ddatasets.TeamFee(30,6);   //50% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot
fees_[1] = F3Ddatasets.TeamFee(43,0);   //43% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot
fees_[2] = F3Ddatasets.TeamFee(56,10);  //20% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot
fees_[3] = F3Ddatasets.TeamFee(43,8);   //35% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot
    
// how to split up the final pot based on which team was picked
// (F3D, P3D)
potSplit_[0] = F3Ddatasets.PotSplit(15,10);  //48% to winner, 25% to next round, 2% to com
potSplit_[1] = F3Ddatasets.PotSplit(25,0);   //48% to winner, 25% to next round, 2% to com
potSplit_[2] = F3Ddatasets.PotSplit(20,20);  //48% to winner, 10% to next round, 2% to com
potSplit_[3] = F3Ddatasets.PotSplit(30,10);  //48% to winner, 10% to next round, 2% to com

fees_ 就是用來決定 玩家 買 key 后, 買 key 的 ether 怎么分配, 其中 2% 基金會(com) + 1% (otherFomo) + 1% 空投池 + fees_[].p3d % P3D + fees_[].gen % 收益, 10% 給 推薦人(無則給P3D)
總結就是 14% 固定 + 86% 可設定, 86% 分3塊( gen+p3d+pot ),所以2隊是56% gen + 10% p3d + 20% pot, 其他隊伍類似
potSplit_ 類似, 固定的 48%(win)+2%(com) + 50% 可設定, 分3塊(gen+p3d+nextround), 如2隊的 20 gen + 20 p3d + 10 next

然后講講所有的方法, 簡單的歸類下

修飾器
isActivated()           //攔截游戲未激活
isHuman()                   //聽說攔截非人類?
isWithinLimits(eth)     //攔截太窮的人和 v 神 ???

ether 買     //從不同地址進的, 第一個參數(shù)是推薦人標識, 第二個是選的 team
buyXid(id, team)    
buyXaddr(addr, team)
buyXname(name, team)

valuts 買        //從不同地址進的, 第一個參數(shù)是推薦人標識, 第二個是選的 team, 第三個是根據(jù) key 數(shù)量計算出來的 eth
reLoadXid(id, team, eth)    
reLoadXaddr(addr, team, eth)
reLoadXname(name, team, eth)


buyCore             // 這里就是判斷了一下本輪是否結束了, 然后直接調用的 core,當然結束會走 endRound
reLoadCore      // 同上, 結束的判斷, 還有就是減去 gen 的金額, 再調用 core
core                // 限制前100eth, 更新 end 時間, 超過0.1eth 判斷空投, 更新玩家及輪次等數(shù)據(jù), 調用2個分紅方法
distributeExternal  // 給固定的13% (10% aff,2% com,1% otherFomo) 及 P3D 打錢
distributeInternal  // 給空投1% 和 gen 和 pot 打錢

提現(xiàn)跑路
withdraw()  

結束一輪
endRound()  // pot 分成5分, win 拿48%, 2%給 com, 還有 gen, p3d, nextRound 則根據(jù)配置來分配, 其中 p3d 和下一輪邏輯比較簡單, 而 gen 我還沒太懂, 因為涉及到 mask 的我都沒看明白( 沒時間細看, 全是數(shù)學, 要慢慢推理分析 )     

注冊 name     //注冊一個 name 用于推廣獲取提成, 第一個參數(shù)是 name 標識, 第二個是推薦人的標識, 第三個是是否同步到其他游戲
registerNameXID(name, id, all) 
registerNameXaddr(name, addr, all)
registerNameXname(name, name, all)

玩家信息相關 , 前2個一般是給外部調用的
receivePlayerInfo           //將傳入玩家信息儲存
receivePlayerNameList       //儲存玩家的所有name
determinePID                //確定玩家信息, 若無則生成一個 pid

玩家分紅, keys相關
calcUnMaskedEarnings        // 實現(xiàn)看不懂, 不過方法作用是用來計算能提現(xiàn)的收益
calcKeysReceived(rid, eth)  // 根據(jù)輪次返回 用eth能買多少 keys
iWantXKeys                  // 根據(jù) key 數(shù)量返回需要多少 eth
managePlayer                    // 第 x 輪時將上一輪的收益移至此輪, 僅輪次開始后第一次購買執(zhí)行
updateGenVault              // 計算及更新收益
updateMasks                 // 更新被鎖定的收益
withdrawEarnings                // 計算可提現(xiàn)的收益

這么多方法, 我也只能列個大致作用和我看的懂的邏輯, 具體的細節(jié)等我參透再出文章

最后總結游戲大致邏輯 : 玩家買 key--> buyXxx(relaodXxx) 方法--> xxxCore --> core --> distributeExternal & distributeInternal --> 游戲結束 --> 玩家 buy 觸發(fā) endRound --> 分了錢 pot 的錢, 部分轉入下一輪 --> 激活新一輪 --> 接上最開始 進入循環(huán) !!! 當然中途可以提現(xiàn)自己沒鎖住的收益, 以及注冊 name 拉人啥的.

幾個有意思的類庫

MSFun.sol

首先說下, 這個庫是用來做多重簽名的, 啥意思呢? 就是一個方法, 必須好幾個(多)人同意執(zhí)行, 最后才會執(zhí)行. 用法如下:

 *                                ┌────────────────────┐
 *                                │ Setup Instructions │
 *                                └────────────────────┘
 * (Step 1) import the library into your contract
 * 
 *    import "./MSFun.sol";
 *
 * (Step 2) set up the signature data for msFun
 * 
 *     MSFun.Data private msData;
 *                                ┌────────────────────┐
 *                                │ Usage Instructions │
 *                                └────────────────────┘
 * at the beginning of a function
 * 
 *     function functionName() 
 *     {
 *         if (MSFun.multiSig(msData, required signatures, "functionName") == true)
 *         {
 *             MSFun.deleteProposal(msData, "functionName");
 * 
 *             // put function body here 
 *         }
 *     }

大致就是先導包, 然后定義一個 MSFun.data 作為區(qū)分合約的標識, 然后再方法中使用 if 包圍, if 第一句就是將之前的簽名清空

MSFun.multiSig( data標識, 需要簽名的數(shù)量, 方法名稱 )

最后說下此類庫在 fomo 中的樣子: 首先 data 照舊, 而需要簽名的數(shù)量來自 teamJust.sol, 它的定義是構造是初始為1, 以后每 add 一個 admin 或 dev 就把對應的 requiredSignatures 加一, remove 同理, 減一. 所以在部署時不改代碼的話, 只要滿足對應的身份限制, 加了這個MSFun.muitiSig 的方法默認是一個人調用就能執(zhí)行

SafeMath.sol

這個沒啥好說的, 操作金額必備, 聽說狼人殺就是少了這個被攻擊的(整形溢出), 也許可以不懂怎么攻擊, 但一定要懂怎么防范, so

/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) 
    internal 
    pure 
    returns (uint256 c) 
{
    if (a == 0) {
        return 0;
    }
    c = a * b;
    require(c / a == b, "SafeMath mul failed");
    return c;
}

如你所見, 簡單的判斷即可確保不會由于溢出導致數(shù)據(jù)錯亂

F3DKeysCalcLong.sol

我只能猜到作用, 至于完全理解... 沒上過大學的我瑟瑟發(fā)抖

keysRec(curEth, newEth)         // 第一個參數(shù)就是using 后的調用方, 第二個參數(shù)是 準備花的 eth, 如我花0.01 eth , 用 round_[rId].eth.keysRec(0.01 eth); 得出的就是當前輪次時0.01eth 能買多少個 key, 注意返回的 keys 很大, 1個 實際上是 1e18 吧, 
ethRec(curKeys, sellKeys)       // 同上, 輸入想買的 keys 數(shù)量, 返回當前輪次 keys 基數(shù)下購買 keys 需要花的 eth
keys(eth)                       // 根據(jù) eth 計算可得多少 keys
eth(keys)                       // 根據(jù) keys 計算需要多少 eth

bundle.js 中, iWantKeys 邏輯

count = BN(parseInt(count) * 1e18)
let priceQuotation = await JUST.Bridges.Browser.contracts.Quick.read('iWantXKeys', count)

keys 和 eth 應該是對應的, 而 eth 的變化規(guī)律如果畫圖的話應該是 指數(shù)級上升? 可以畫成函數(shù)看看

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容