死磕solidity之如何有效的節(jié)省gas

為什么要強調(diào)優(yōu)化gas的重要性

DAPP中收取的費用取決于功能邏輯的復(fù)雜程度辙售,越復(fù)雜消耗的計算資源越多。并且需要用戶承擔(dān)一部分gas,所以solidity 的優(yōu)化顯得非常的重要士八。同時注重優(yōu)化gas的合約開發(fā)人員寫出來的合約代碼更安全,質(zhì)量更高陕见。

1. 封裝結(jié)構(gòu)

以uint 為例评甜,如果我們的程序中包含多個類似的變量粘舟,可以將其封裝在一起柑肴,因為不管uint8 ,uint32 ,uint16,solidity都會為其保留256位。即使你使用uint8也不會節(jié)省gas.

2. 最小化讀寫鏈上數(shù)據(jù)

首先明確一點在讀寫 memory 變量比讀寫 storage 變量便宜硕舆。

contract NotSaveGas {
uint public var1  = 70;
function f1() external view returns (uint) {
        uint sum = 0;
        for (uint i = 0; i < var1; i++) {
            sum += i;
        }
        return sum;
}

contract SaveGas {
function f2() external view returns (uint) {
        uint sum = 0;

        for (uint i = 0; i < var1; i++) {
            sum += i;
        }
        return sum;
    }
}

請一定要避免f1這種循環(huán)讀寫 storage 變量,這是比較消耗gas的方式。處理這種問題實際可以定義內(nèi)存變量作為緩存刊咳,將數(shù)據(jù)寫入,這樣可以節(jié)省大量的gas.

3.打開 solidity 優(yōu)化器

hardhat 配置:

module.exports = {
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: true,
        runs: 1000,
      },
    },
  },
}

4.盡可能減少鏈上數(shù)據(jù)

區(qū)塊鏈上保存數(shù)據(jù)是非常昂貴的跷坝,所以需要盡可能將鏈上存儲的信息減少柴钻,以此來節(jié)省大量的交易gas.

使用事件

事件是外部事物(例如用戶界面)從區(qū)塊鏈中獲得通知的內(nèi)置方式。當(dāng)發(fā)出事件時毫蚓,將通知該事件的監(jiān)視者畔乙。更新合約變量時不會發(fā)生通知。事件以不同的方式存儲牍鞠,比使用合約存儲便宜得多。合約不能直接訪問日志硫眯。

IPFS

如果你需要存儲文件之類净宵,可以使用IPFS保存文件,并將存儲的ID保存在鏈上敏储。

無狀態(tài)合約

Merkle Proofs

如果需要使用區(qū)塊鏈來驗證一些信息是否有效,可以使用 merkle 證明。Merkle 證明使用單一的數(shù)據(jù)塊來證明更大的數(shù)據(jù)量的有效性。例如贝搁,如果有人想證明 "Tx4 "的有效性,他將需要提供 Tx4膀哲、Hash3锐朴、Hash12 和 Hash5678衣迷,然后你的合約將能夠重新計算 Merkle 根(Hash12345678)膳沽,并檢查它是否與存儲在區(qū)塊鏈上的根相一致陨界。你將不需要存儲所有交易的哈希值录平。


5.注重變量順序

Solidity 存儲槽的長度為 32 字節(jié)动猬,但并不是所有的數(shù)據(jù)類型都需要這么大的空間:bool, int8 ... int128, bytes1 ... bytes31 和地址需要的空間小于 32 字節(jié)彼水。solidity 會嘗試將這些變量打包到同一個存儲槽中。

如果你接連定義了 2 個uint128慈俯,它們都會被打包到同一個存儲槽中刑峡,因為它們各占 16 字節(jié)随闪。然而,如果你定義了一個uint128铐伴,接著是一個unit256,然后是另一個int128俏讹,你將使用 3 個存儲槽当宴,因為在兩個 int128 之間的 unit256 需要一個完整的存儲槽。

contract T{
    // 不好的方式
    uint128 v1;
    uint256 v2;
    uint128 v3;
    
    // 推薦方式
    uint128 v1;
    uint128 v3;
    uint256 v2;
}

6.首選數(shù)據(jù)類型

如果智能合約只需要一個狀態(tài)變量泽疆,一個永遠(yuǎn)不會大于 255 的無符號整數(shù)户矢。我們常規(guī)思想可能是想用uint8,會覺得節(jié)省gas,實際并不會。以太坊操作碼被設(shè)計為使用 256 位的變量(EVM 堆棧的大醒程邸)梯浪,而 uint8 只需要 8 位,EVM 會在剩余的位上填上 "0"瓢娜,以便能夠操作它挂洛。這個由 EVM 執(zhí)行的填 "0" 操作將花費 Gas,因此為了節(jié)省交易 Gas眠砾,最好使用 uint256 而不是 uint8虏劲。

7.獨立部署庫

如果在智能合約中重復(fù)使用代碼,最好是將所有的代碼打包到一個庫中,并通過import的方式指向它柒巫。

庫包含:

  • 嵌入式庫:包含內(nèi)部函數(shù)的庫励堡,這些庫都是嵌入在合約中,和合約一同部署堡掏,所以會比較消耗gas
  • 獨立庫:包含public和外部函數(shù)的庫应结,這些庫只會被部署一次,同時被所有導(dǎo)入它的所有合約使用泉唁,從而節(jié)省了大量的gas.

8.構(gòu)造函數(shù)

常量和不可變的狀態(tài)變量在合約被部署后不能被改變鹅龄。區(qū)別在于,常量必須在編譯時定義游两,而不可變量可以在構(gòu)造函數(shù)中定義砾层。總是盡量使用常量贱案,以便使構(gòu)造函數(shù)更便宜肛炮。

9.使合約盡可能的小

單個合約的限制是24KB,所以要想節(jié)省gas,就必須使實現(xiàn)的合約盡可能的小宝踪。

  • revert和assert的提示信息要盡可能的短
  • 修改器: 修改器(modifier)代碼是內(nèi)聯(lián)的侨糟,這意味著它會被添加在所修改的函數(shù)的開頭或結(jié)尾。在使用修改器時減少合約大小的一個技巧是編寫一個實現(xiàn)修改器邏輯的函數(shù)瘩燥,然后讓修改器調(diào)用該函數(shù)秕重。這樣實現(xiàn)修改器的代碼就不會被復(fù)制,只有函數(shù)調(diào)用會被復(fù)制厉膀。這種技術(shù)只在同一修改器被多次使用時有效溶耘。
modifier TestModifier(uint256 value){
        JudgeLength(value);
        _;
}
function JudgeLength(uint256 value)internal{
    //logic
}

10.最小代理(ERC1167)

如果需要部署多個功能完全相同的合約,應(yīng)該考慮使用 "最小代理"(在ERC 1167中定義)

最小的代理只是一個合約服鹅,它將把所有的調(diào)用委托給一個預(yù)先定義的實現(xiàn)合約凳兵。它有一個定義好的字節(jié)碼,代表最小代理合約的編譯代碼企软,你只需要把你的實現(xiàn)合約地址插入其中庐扫,你就可以根據(jù)需要部署最小代理的多個副本。 參考ERC 1167 相關(guān)文章仗哨,了解如何使用最小代理)形庭。

由于最小的代理字節(jié)碼非常小,部署它的成本也低到不能再低厌漂,因此節(jié)省一堆部署 Gas萨醒。

使用最小代理的注意事項,你應(yīng)該牢記:最小代理的實現(xiàn)合約地址不能改變桩卵,這意味著你將不能升級他們的代碼验靡。

11.內(nèi)存位置

以太坊存在4個內(nèi)存位置倍宾,從最便宜到最貴的:calldata、stack胜嗓、memory高职、storage。

  • calldata:只適用于輸入?yún)?shù)且參數(shù)是外部函數(shù)的引用數(shù)據(jù)類型(數(shù)組辞州,字符串 ...)怔锌。Calldata 參數(shù)是只讀的,如果你有一些需要傳遞給函數(shù)的引用類型变过,總是考慮使用 calldata埃元,因為它是最便宜的。
  • stack:只對方法中定義的值類型數(shù)據(jù)有效媚狰。
  • memory:內(nèi)存是易丟失的 RAM岛杀,在 EVM 終止運行的時候會被移除≌腹拢可以用它來存儲引用數(shù)據(jù)類型类嗤,它比storage變量更便宜。當(dāng)向其他函數(shù)傳遞參數(shù)辨宠,或在你的函數(shù)中聲明臨時變量時遗锣,除非你嚴(yán)格需要使用storage變量,否則應(yīng)該總是使用 memory變量嗤形。
  • storage:是最昂貴的存儲位置精偿。存儲數(shù)據(jù)在區(qū)塊鏈上持久存在,請盡量減少鏈上數(shù)據(jù)存儲赋兵。
  • 本地存儲變量:本地存儲變量是方法的本地變量笔咽,它指向一個實際的狀態(tài)變量(存儲在區(qū)塊鏈存儲中)。與其在內(nèi)存中復(fù)制/粘貼存儲數(shù)組以便操作它們霹期,然后將它們復(fù)制回存儲拓轻,不如簡單地使用本地存儲變量,直接在存儲上操作经伙。
  • 批處理:與其讓用戶用不同的值多次調(diào)用同一個函數(shù)(通過向區(qū)塊鏈發(fā)送多個交易),不如讓他們通過傳遞動態(tài)大小的數(shù)組勿锅,以便可以在一個單一的交易中批量執(zhí)行相同的功能帕膜。這將能夠節(jié)省一些交易基礎(chǔ)開銷成本。實際ERC1155有些思想就是如此

12.盡量減少鏈上操作

  • 字符串:如果可以使用bytes,則盡量使用溢十。如果仍然需要操作垮刹,則盡量放在智能合約外部操作。
  • 返回值:對返回值無需額外轉(zhuǎn)換张弛,如果這個是可以通過鏈外數(shù)據(jù)來處理荒典。
  • 循環(huán):避免在長數(shù)組中循環(huán)酪劫,這不僅會花費大量的 Gas,而且如果 Gas 成本增加到很高的程度(超過 BlockGas 限制)寺董,會使合約無法執(zhí)行覆糟。使用映射來代替長數(shù)組,映射是一個哈希表遮咖,可以讓你在一次操作中使用其鍵來訪問任何值滩字,而不是在數(shù)組中循環(huán),直到找到你要找的鍵御吞。

13.利用 view函數(shù)減少gas

當(dāng)用戶從外部調(diào)用一個view函數(shù)麦箍,是不需要支付一分 gas 的。

這是因為 view 函數(shù)不會真正改變區(qū)塊鏈上的任何數(shù)據(jù) - 它們只是讀取陶珠。因此用 view 標(biāo)記一個函數(shù)挟裂,意味著告訴 web3.js,運行這個函數(shù)只需要查詢你的本地以太坊節(jié)點揍诽,而不需要在區(qū)塊鏈上創(chuàng)建一個事務(wù)(事務(wù)需要運行在每個節(jié)點上诀蓉,因此花費 gas)。

在所能只讀的函數(shù)上標(biāo)記上表示“只讀”的“external view 聲明寝姿,就能為你的玩家減少在 DApp 中 gas 用量交排。

注意:如果一個 view 函數(shù)在另一個函數(shù)的內(nèi)部被調(diào)用,而調(diào)用函數(shù)與 view 函數(shù)的不屬于同一個合約饵筑,也會產(chǎn)生調(diào)用成本埃篓。這是因為如果主調(diào)函數(shù)在以太坊創(chuàng)建了一個事務(wù),它仍然需要逐個節(jié)點去驗證根资。所以標(biāo)記為 view 的函數(shù)只有在外部調(diào)用時才是免費的架专。


14.使用短路模式排序solidity操作

短路(short-circuiting)是一種使用或/與邏輯來排序不同成本操作的solidity合約 開發(fā)模式,它將低gas成本的操作放在前面玄帕,高gas成本的操作放在后面部脚,這樣如果前面的低成本操作可行,就可以跳過(短路)后面的高成本以太坊虛擬機操作了裤纹。

// f(x) 是低gas成本的操作
// g(y) 是高gas成本的操作

// 按如下排序不同gas成本的操作
f(x) || g(y)
f(x) && g(y)

15.刪除不必要的庫

在開發(fā)Solidity智能合約時委刘,我們引入的庫通常只需要用到其中的部分功能,這意味著其中可能會包含大量對于你的智能合約而言其實是冗余的solidity代碼鹰椒。如果可以在你自己的合約里安全有效地實現(xiàn)所依賴的庫功能锡移,那么就能夠達(dá)到優(yōu)化solidity合約的gas利用的目的。

例如漆际,在下面的solidity代碼中淆珊,我們的以太坊合約只是用到了SafeMath庫的add方法:

import './SafeMath.sol' as SafeMath;

contract SafeAddition {
 function safeAdd(uint a, uint b) public pure returns(uint) {
 return SafeMath.add(a, b);
 }
}

通過參考SafeMath的這部分代碼的實現(xiàn),可以把對這個solidity庫的依賴剔除掉:

contract SafeAddition {
 function safeAdd(uint a, uint b) public pure returns(uint) {
 uint c = a + b;
 require(c >= a, "Addition overflow");
 return c;
 }
}

16.精確的聲明函數(shù)的可見性

在Solidity合約開發(fā)中奸汇,顯式聲明函數(shù)的可見性不僅可以提高智能合約的安全性施符, 同時也有利于優(yōu)化合約執(zhí)行的gas成本往声。例如,通過顯式地標(biāo)記函數(shù)為外部函數(shù)(External)戳吝,可以強制將函數(shù)參數(shù)的存儲位置設(shè)置為calldata浩销,這會節(jié)約每次函數(shù)執(zhí)行時所需的以太坊gas成本。

External 可見性比 public 消耗gas 少骨坑。

17.避免代碼中死代碼

死代碼(Dead code)是指那些永遠(yuǎn)也不會執(zhí)行的Solidity代碼撼嗓,例如那些執(zhí)行條件永遠(yuǎn)也不可能滿足的代碼,就像下面的兩個自相矛盾的條件判斷里的Solidity代碼塊欢唾,消耗了以太坊gas資源但沒有任何作用:

function deadCode(uint x) public pure {
 if(x < 1 {
    if(x > 2) {
        return x;
    }
 }
}

18.避免使用常量結(jié)果的循環(huán)

如果一個循環(huán)計算的結(jié)果是無需編譯執(zhí)行Solidity代碼就可以預(yù)測的且警,那么 就不要使用循環(huán),這可以可觀地節(jié)省gas礁遣。例如下面的以太坊合約代碼就可以 直接設(shè)置num變量的值:

function constantOutcome() public pure returns(uint) {
  uint num = 0;
  for(uint i = 0; i < 100; i++) {
    num += 1;
  }
  return num;
}

19.合并循環(huán)

有時候在Solidity智能合約中斑芜,你會發(fā)現(xiàn)兩個循環(huán)的判斷條件一致,那么在這種情況下就沒有理由不合并它們祟霍。例如下面的以太坊合約代碼:

function loopFusion(uint x, uint y) public pure returns(uint) {
  for(uint i = 0; i < 100; i++) {
    x += 1;
  }
  for(uint i = 0; i < 100; i++) {
    y += 1;
  }
  return x + y;
}

20.去除循環(huán)中的比較運算

如果在循環(huán)的每個迭代中執(zhí)行比較運算杏头,但每次的比較結(jié)果都相同,則應(yīng)將其從循環(huán)中刪除沸呐。

function unilateralOutcome(uint x) public returns(uint) {
  uint sum = 0;
  for(uint i = 0; i <= 100; i++) {
    if(x > 1) {
      sum += 1;
    }
  }
  return sum;
} 

參考

https://medium.com/coinmonks/smart-contracts-gas-optimization-techniques-2bd07add0e86

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末醇王,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子崭添,更是在濱河造成了極大的恐慌寓娩,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呼渣,死亡現(xiàn)場離奇詭異棘伴,居然都是意外死亡,警方通過查閱死者的電腦和手機屁置,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門焊夸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蓝角,你說我怎么就攤上這事阱穗。” “怎么了使鹅?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵颇象,是天一觀的道長。 經(jīng)常有香客問我并徘,道長,這世上最難降的妖魔是什么扰魂? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任麦乞,我火速辦了婚禮蕴茴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘姐直。我一直安慰自己倦淀,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布声畏。 她就那樣靜靜地躺著撞叽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪插龄。 梳的紋絲不亂的頭發(fā)上愿棋,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音均牢,去河邊找鬼糠雨。 笑死,一個胖子當(dāng)著我的面吹牛徘跪,可吹牛的內(nèi)容都是我干的甘邀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼垮庐,長吁一口氣:“原來是場噩夢啊……” “哼松邪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起哨查,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤逗抑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后解恰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锋八,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年护盈,在試婚紗的時候發(fā)現(xiàn)自己被綠了挟纱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡腐宋,死狀恐怖紊服,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胸竞,我是刑警寧澤欺嗤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站卫枝,受9級特大地震影響煎饼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜校赤,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一吆玖、第九天 我趴在偏房一處隱蔽的房頂上張望筒溃。 院中可真熱鬧,春花似錦沾乘、人聲如沸怜奖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽歪玲。三九已至,卻和暖如春掷匠,著一層夾襖步出監(jiān)牢的瞬間滥崩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工槐雾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留夭委,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓募强,卻偏偏與公主長得像株灸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子擎值,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容