火爆的區塊鏈遊戲確是龐氏騙局,區塊鏈遊戲存在哪些潛在危機?

買賣虛擬貨幣

火爆的區塊鏈遊戲確是龐氏騙局,區塊鏈遊戲存在哪些潛在危機?如果你在2017年就開始關注以太坊區塊鏈,你應該知道早期的智慧合約有龐氏騙局。就好像傳統的龐氏騙局,這些遊戲的設計是為了能夠連續吸引玩家加入,來讓這個遊戲一直進行下去。雖然這些合約會戛然而止,有些人會發現是因為別的原因導致其結束。本文列舉了這類合約可能遭到的攻擊。

攻擊#1:異常障礙

當攻擊者利用合約的漏洞返回一個異常錯誤的時候,異常障礙攻擊就會發生。異常障礙會在合約不能成功呼叫類似address.send() 或者address.call.value()之類的函式時,自動觸發。這個錯誤本身不會被標出,痴肥合約指導去這樣做;異常錯誤不會自動產生。

攻擊示例

2016年2月6日,KotET遊戲的智慧合約部署完成。KotET遊戲中,玩家需要傳送給合約一些以太幣,從而獲得“王位”。只要拿到了王位,玩家就會被加到皇庭,並且永遠地被記錄在區塊鏈上。更重要地是,後來的國王有權去獲得新國王的以太幣。隨著國外數量增多,成為國王的代價也會越來越貴。如果14天過去了,還沒有新的繼承者,那麼王位就會重置,並且遊戲也全部重新開始。這個遊戲的理想是新的國外會支付一定的費用,來獲得王位,同時有新人來不停地進行遊戲,這就導致了“龐氏陷阱”。

程式碼示例

下面是初始KotET合約的簡化版程式碼。需要注意地是返回函式,這會在玩家將msg.value傳送到合約的時候觸發。返回函式會首先檢查國王是否發出了足夠的以太幣來獲得王位。如果沒有,這個需求就會被丟棄,然後程式碼也會返回。如果有足夠的以太幣,那麼現在的國王就獲得足夠的彌補(認購價格減去服務費),並且發出資金的人就會成為新的國王。然後,新的國王價格會計算出來。

contract KotET {

address public king;

uint public claimPrice = 100;

address owner;

//constructor, assigning ownership

constructor() {

owner = msg.sender;

king = msg.sender;

}

//for contract creator to withdraw commission fees

function sweepCommission(uint amount) {

owner.send(amount);

}

//fallback function

function() {

if (msg.value < claimPrice) revert;

uint compensation = calculateCompensation();

king.send(compensation);

king = msg.sender;

claimPrice = calculateNewPrice();

}

}

KotET合約的漏洞在於使用了address.send(),並且在不成功呼叫的時候,就不能檢查異常錯誤。就像之前討論的,address.send() and address.transfer()都是受限於2300的燃料費。雖然這對於防止重入攻擊很有用,但是gas燃料限制會導致傳送資金給國王地址失敗,如果國王的合約有退回函式需要花費超過2300的gas燃料費。這就是KotET的情況,支付給國王的錢會傳送到以太坊mist“合約錢包”,而不是“合約賬戶”,這就需要更多的gas燃料來完成轉賬。最終的結果就是不成功的轉賬,以太幣唄退回到國王的賬戶中,新的國王無法進行加冕,所以這個合約就會一直卡住。

解決方案

KotET能夠用以下2個辦法解決問題:

1. 將異常丟棄,那麼呼叫就會恢復- 我們可以透過在函式中新增revert來完成。這會防止合約停止,但是也會需要多餘的步驟來啟動支付轉賬。有兩種方案,一是讓使用者自己發出多個支付轉賬(太中心化),二是實施批次支付確保付款,直到在“頭獎”中沒有剩餘資金。

2. 使用提現,而不是直接的send呼叫,合約就可以有結構的,然後玩家就可以讓自己的提現失敗,而不是合約中剩下的資金。提現演算法的唯一不好處,就是這並不是自動化的,需要很多的使用者互動。讓我們來看看,我們可以如何更新合約,來實施這些變化。

contract KotET {

address public king;

uint public claimPrice = 100;

uint public resolutionFunds

address owner;

mapping (address => uint) creditedFunds;

//constructor, assigning ownership

constructor() {

owner = msg.sender;

king = msg.sender;

}

//for contract creator to withdraw commission fees

function sweepCommission(uint amount) {

owner.send(amount);

}

//for assigning new king and crediting balance

function becomeKing() public payable returns (bool) {

if (msg.value > claimPrice) {

creditedFunds[richest] += msg.value;

king = msg.sender;

return true;

} else {

return false;

}

}

function withdraw() public {

uint amount = creditedFunds[msg.sender];

//zeroing the balance BEFORE sending creditedFunds

//to prevent re-entrancy attacks

pendingWithdrawals[msg.sender] = 0;

msg.sender.transfer(amount);

}

}

現在合約再也不用依賴於退回函式來執行對新的國外進行加冕了,並且可以直接傳送資金給下個國王。這個合約現在對於任何的能夠攻擊合約的回退/重入攻擊來說,都是安全的。

攻擊#2:呼叫棧攻擊

在EIP150使用之前,以太坊虛擬機器的呼叫棧深度為1024.這也就是說,有人可以在自動使用第1024個呼叫之前,呼叫某個合約1023次。攻擊者最終會達到第1023次合約,導致接下來的呼叫失敗,並且讓他們自身來盜竊合約的資金,並且掌控合約。

攻擊示例

和KotET這類旁氏遊戲類似,使用者會發出以太幣給合約,來加入遊戲。每輪遊戲的贏家可以獲得獎池的金額。遊戲的規則如下:

• 你必須要傳送至少1ETH到合約,然後你會被支付10%的利息。

• 如果“政府”(合約)在12小時內沒有收到新的資金,最後的人獲得所有的獎池,所有人都會失去資金。

• 傳送到合約的以太幣分配如下:5%給獎池,5%給合約擁有者,90%根據支付順序,用來支付給傳送資金的人

• 當獎池滿了(1萬以太幣),95%的資金會傳送給支付者。

• 紅利:支付者可以使用推薦連結來邀請別人。如果有朋友對這個合約進行支付,那麼邀請人可以獲得5%,5%會給到合約擁有者,5%會進入獎池,剩下的85%會用來支付利息。

合約的寫入,需要保證使用者和他們的資金被記錄在2個陣列,ddress[] public credAddr 和int[] public credAmt。這兩個陣列會在遊戲最後重置。GovernMental已經非常成功了,因為陣列變得非常大,需要清除他們的燃料費已經超過每個轉賬能夠做到的極限。最終的結局是獎池的永久性凍結,總共有大約1100個以太坊。最後,在2個月後,資金最後還是解凍了,並且發給了呼叫者。

GovernMental雖然不是被惡意的使用者攻擊,但是它也是很好的例子,這類災難會由呼叫棧攻擊產生。這也表面,在進行大型資料庫工作的時候,需要格外的小心。

程式碼

下面是GovernMental智慧合約的完整程式碼,其中還包含簡短的變數。我已經在它的整體中包含了真正的合約,因為透過一行行地檢查合約可以學到很多,包含這個合約是如何構建的。有人可以看到function lendGovernmentMoney(),代表了發出資金者的地址,並且需要以太幣的數量被重置或者新增到現有資料。需要注意,在同個函式中,資金是如何在合約擁有者以及12個小時結束時的最後發出資金者之間分配的, credAddr[credAddr.length 1].send(profitFromCrash); 以及corruptElite.send(this.balance)。

contract Government {

// Global Variables

uint32 public lastPaid;

uint public lastTimeOfNewCredit;

uint public profitFromCrash;

address[] public credAddr;

uint[] public credit;

address public corruptElite;

mapping (address => uint) buddies;

uint constant TWELVE_HOURS = 43200;

uint8 public round;

// constructor

constructor() {

profitFromCrash = msg.value;

corruptElite = msg.sender;

lastTimeOfNewCredit = block.timestamp;

}

function lendGovernmentMoney(address buddy) returns (bool) {

uint amount = msg.value;

// check if the system already broke down.

// If 12h no new creditor gives new credit to

// the system it will brake down.

// 12h are on average = 60*60*12/12.5 = 3456

if (lastTimeOfNewCredit + TWELVE_HOURS < block.timestamp)

// Return money to sender

msg.sender.send(amount);

// Sends all contract money to the last creditor

credAddr[credAddr.length - 1].send(profitFromCrash);

corruptElite.send(this.balance);

// Reset contract state

lastPaid = 0;

lastTimeOfNewCredit = block.timestamp;

profitFromCrash = 0;

// this is where the arrays are cleared

credAddr = new address[](0);

credAmt = new uint[](0);

round += 1;

return false;

}

else {

// the system needs to collect at

// least 1% of the profit from a crash to stay alive

if (amount >= 10 ** 18) {

// the System has received fresh money,

// it will survive at leat 12h more

lastTimeOfNewCredit = block.timestamp;

// register the new creditor and his

// amount with 10% interest rate

credAddr.push(msg.sender);

credAmt.push(amount * 110 / 100);

// now the money is distributed

// first the corrupt elite grabs 5% — thieves!

corruptElite.send(amount * 5/100);

// 5% are going into the economy (they will increase

// the value for the person seeing the crash coming)

if (profitFromCrash < 10000 * 10**18)

profitFromCrash += amount * 5/100;

}

// if you have a buddy in the government (and he is

// in the creditor list) he can get 5% of your

// credits. Make a deal with him.

if(buddies[buddy] >= amount) {

buddy.send(amount * 5/100);

}

buddies[msg.sender] += amount * 110 / 100;

// 90% of money used to pay out old creditors

if (credAmt[lastPaid] <= address(this).balance — profitFromCrash){

credAddr[lastPaid].send(credAmt[lastPaid]);

buddies[credAddr[lastPaid]] -= credAmt[lastPaid];

lastPaid += 1;

}

return true;

}

else {

msg.sender.send(amount);

return false;

}

}

}

// fallback function

function() {

lendGovernmentMoney(0);

}

function totalDebt() returns (uint debt) {

for(uint i=lastPaid; i

debt += credAmt[i];

}

}

function totalPayedOut() returns (uint payout) {

for(uint i=0; i

payout += credAmt[i];

}

}

// donate funds to "the government"

function investInTheSystem() {

profitFromCrash += msg.value;

}

// From time to time the corrupt elite

// inherits it’s power to the next generation

function inheritToNextGeneration(address nextGeneration) {

if (msg.sender == corruptElite) {

corruptElite = nextGeneration;

}

}

function getCreditorAddresses() returns (address[]) {

return credAddr;

}

function getCreditorAmounts() returns (uint[]) {

return credAmt;

}

}

我們假設攻擊者寫了如下的合約,進行惡意攻擊contract Government {}。

contract attackGov {

function attackGov (address target, uint count) {

if (0<= count && count<1023) {

this.attackGov.gas(gasleft() - 2000)(target, count+1);

}

else {

attackGov(target).lendGovernmentMoney;

}

}

攻擊者呼叫了contract attackGov{} 函式,來進行呼叫直到棧的大小為1023.當棧達到1022.lendGovernmentMoney()函式就會在第1023個棧上執行。因為第1024個呼叫已經失敗了,並且 send() 函式不會檢查返回的程式碼,合約的credAddr[credAddr.length — 1].send(profitFromCrash)程式碼也會失效。合約之後就會重置,而且下一輪已經可以開始。因為支付失敗了,合約現在就會從最後一輪中獲得獎池,在下輪結束後,合約擁有者就會獲得全部的資金,corruptElite.send(this.balance)。

解決方案

那麼我們怎麼才能避免全棧攻擊呢?很幸運地是,EIP150標準進行了更新,使得棧呼叫的深度達到1024是幾乎不可能的事情。規則中寫到,子呼叫不能花費主呼叫的63/64燃料費用。為了達到接近棧呼叫的極限,攻擊者需要花費難以想象地費用,所以很少有人會這麼做。

另個方面,對於大量資料的處理方法包含:

• 寫合約的時候,要在多個轉賬中分散資料清理工作,而不是集中在某個,或者

• 透過讓使用者能夠獨立處理資料集的方式來寫入合約。

攻擊#3- 不可更改的管理器缺陷

什麼使得智慧合約這麼特別?他們是不可更改的。什麼造就了智慧合約的噩夢?他們是不可更改的。現在,很遺憾的結論是,當在寫智慧合約時,很多時候會出現錯誤。在啟用合約之前,對整體的函式,引數和合約結構進行稽覈,是非常必要的。

如果在以太坊歷史上,有智慧合約是因為整體架構出問題,而最終失敗的,毫無疑問就是Rubixi。Rubixi是另一個旁氏遊戲,其中玩家需要傳送以太幣到合約中,並且可以獲得更多的以太幣。但是,在Rubixi開發的過程中,擁有者隨意更改了合約名稱,但是並沒有檢車任何的不一致性。毋庸置疑,Rubixi遠不能稱為“成功”。

攻擊示例

由於Solidity v0.4.24演算法,合約的管理器功能是construct()。但是,在Rubixi合約建立的時候,管理器功能被以太坊虛擬機器和合約共享了同個名字。Rubixi的問題在於當合約中部署了管理器的名稱為function DynamicPyramid() ,而不是function Rubixi(),,這就意味著Rubixi最初的名字叫“DynamicPyramid”。由於這個不一致性,合約在建立的時候,並沒有指定擁有者,所以城堡的鑰匙被搶走了。任何人都能夠定義他們自己為合約的擁有者,然後獲得參與者加入的合約費用。

程式碼示例

如果我們把合約程式碼的前幾行拿出來,你就會發現合約名稱和指定管理器函式的區別。

contract Rubixi {

//Declare variables for storage critical to contract

uint private balance = 0;

uint private collectedFees = 0;

uint private feePercent = 10;

uint private pyramidMultiplier = 300;

uint private payoutOrder = 0;

address private creator;

//Sets creator

function DynamicPyramid() {

creator = msg.sender;

}

現在你應該明白了,攻擊者需要做的,就是建立合約的名字為function DynamicPyramid(), 然後獲得擁有權。然後,攻擊者可以呼叫function collectAllFees(),然後提現。雖然這個攻擊已經非常直接了,Rubixi是個很好的例子,告訴我們一定要徹底地檢查合約。

contract extractRubixi {

address owner;

Rubixi r = Rubixi(0xe82...);

constructor() public {

owner=msg.sender;

}

function setAndGrab() public {

r.DynamicPyramid();

r.collectAllFees();

}

}

解決方案

很幸運地是,Solidity語言已經更新了,以至於管理器功能被定義為constructor() ,而不是contractName()。我們可以從中學到的是,多次檢查我們的合約程式碼,並且保證你在整個開發過程中,保持一致性。沒有什麼比部署一個無法改變的合約,但是發現其中有問題,更糟糕了。

以上就是火爆的區塊鏈遊戲確是龐氏騙局,區塊鏈遊戲存在哪些潛在危機?的詳細介紹,龐氏區塊鏈遊戲或許已經是過去的事情,但是George Santayana曾經說過,“那些不能從歷史中學到教訓的人,還會重複錯誤。”透過從KotET, GovernMental和Rubixi這類錯誤中學習,我們可以防止自己在錯誤的道路上越走越遠。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;