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ù)看看