📜 [專欄新文章] Gas Efficient Card Drawing in Solidity
✍️ Ping Chen
📥 歡迎投稿: https://medium.com/taipei-ethereum-meetup #徵技術分享文 #使用心得 #教學文 #medium
Assign random numbers as the index of newly minted NFTs
Scenario
The fun of generative art NFT projects depends on randomness. The industry standard is “blind box”, where both the images’ serial number and the NFTs’ index are predetermined but will be shifted randomly when the selling period ends. (They call it “reveal”) This approach effectively solves the randomness issue. However, it also requires buyers to wait until the campaign terminates. What if buyers want to know the exact card right away? We’ll need a reliable onchain card drawing solution.
The creator of Astrogator🐊 isn’t a fan of blind boxes; instead, it thinks unpacking cards right after purchase is more interesting.
Spec
When initializing this NFT contract, the creator will determine the total supply of it. And there will be an iterable function that is randomly picking a number from the remaining pool. The number must be in range and must not collide with any existing ones.
Our top priority is accessibility/gas efficiency. Given that gas cost on Ethereum is damn high nowadays, we need an elegant algorithm to control gas expanse at an acceptable range.
Achieving robust randomness isn’t the primary goal here. We assume there’s no strong financial incentive to cheat, so the RNG isn’t specified. Implementers can bring their own source of randomness that they think is good enough.
Implementation
Overview
The implementation is pretty short and straightforward. Imagine there’s an array that contains all remaining(unsold) cards. When drawIndex() is called, it generates a (uniform) random seed to draw a card from the array, shortens the array, and returns the selected card.
Algorithm
Drawing X cards from a deck with the same X amount of cards is equal to shuffling the deck and dealing them sequentially. It’s not a surprise that our algorithm is similar to random shuffling, and the only difference is turning that classic algo into an interactive version.
A typical random shuffle looks like this: for an array with N elements, you randomly pick a number i in (0,N), swap array[0] and array[i], then choose another number i in (1,N), swap array[1] and array[i], and so on. Eventually, you’ll get a mathematically random array in O(N) time.
So, the concept of our random card dealing is the same. When a user mints a new card, the smart contract picks a number in the array as NFT index, then grabs a number from the tail to fill the vacancy, in order to keep the array continuous.
Tweak
Furthermore, as long as the space of the NFT index is known, we don’t need to declare/initialize an array(which is super gas-intensive). Instead, assume there’s such an array that the n-th element is n, we don’t actually initialize it (so it is an array only contains “0”) until the rule is broken.
For the convenience of explanation, let’s call that mapping cache. If cache[i] is empty, it should be interpreted as i instead of 0. On the other hand, when a number is chosen and used, we’ll need to fill it up with another unused number. An intuitive method is to pick a number from the end of the array, since the length of the array is going to decrease by 1.
By doing so, the gas cost in the worst-case scenario is bound to be constant.
Performance and limitation
Comparing with the normal ascending index NFT minting, our random NFT implementation requires two extra SSTORE and one extra SLOAD, which cost 12600 ~ 27600 (5000+20000+2600) excess gas per token minted.
Theoretically, any instantly generated onchain random number is vulnerable. We can restrict contract interaction to mitigate risk. The mitigation is far from perfect, but it is the tradeoff that we have to accept.
ping.eth
Gas Efficient Card Drawing in Solidity was originally published in Taipei Ethereum Meetup on Medium, where people are continuing the conversation by highlighting and responding to this story.
👏 歡迎轉載分享鼓掌
mapping function 在 Taipei Ethereum Meetup Facebook 的最讚貼文
📜 [專欄新文章] 可升級合約介紹 - 鑽石合約(EIP-2535 Diamond standard)
✍️ Kimi Wu
📥 歡迎投稿: https://medium.com/taipei-ethereum-meetup #徵技術分享文 #使用心得 #教學文 #medium
Photo by Evie S. on Unsplash
前言
可升級合約簡單來說是透過 proxy contract(代理合約)來達成,藉由代理合約去呼叫欲執行的合約,若要升級,則把代理合約中的指向的地址換為新的合約地址即可。而執行的方式則是透過 delegateCall,但 delegateCall 不會更動目標合約的狀態。所以要怎麼處理變數,就是一門學問了。
舉例來說,contract B 有個變數 uint256 x,初始值為 0, 而 function setX(uint256),可以改變 x 的值。proxy contract A 使用 delegatecall 呼叫 contract B 的 setX(10),交易結束後,contract B中的 x 依然還是 0。
OpenZeppelin 提出了三種實作方式,可以做到可升級合約,細節可參考 Proxy Patterns,而最終的實作選用了 Unstructured Storage的這個方式,這種方式對於開發較友善,開發時不需特別處理 state variables(不過升級時就需要特別注意了)。而這篇主要是介紹 Diamond standard,OpenZeppelin 的可升級合約就不多做介紹。
USDC V2 : Upgrading a multi-billion dollar ERC-20 token 詳細地介紹代理合約跟變數儲存之間的關係,不了解升級合約的原理,建議先看看。
鑽石合約
名詞介紹
diamond:合約本體,是一個代理合約,無商業邏輯
facet:延伸的合約(實際商業邏輯實作的合約)
loupe:也是一個 facet,負責查詢的功能。可查詢此 diamond所提供的 facet與facet所提供的函式
diamondCut:一組函式,用來管理(增加/取代/減少)此 diamond合約所支援的功能
Loupe
直接來看 loupe的介面,從宣告就能很清楚暸解 diamond合約的實作方式,loupe宣告了一個結構 Facet,Facet結構包含一個地址及 function selector 陣列,所以我們只需要記錄一個 Facet陣列就可以得知這個 diamond 合約有多少個延伸合約及所支援的功能(loupe只定義結構,而實際變數是存在diamon合約中的)。也就是 diamond合約中只記錄延伸合約的地址及其支援的 function selectors,及少數 diamond合約的管理邏輯,並無商業邏輯,因此可以外掛非常非常多的合約上去(就像一個Hub),也就可以突破一個合約只有24K的限制。
// A loupe is a small magnifying glass used to look at diamonds.interface IDiamondLoupe { struct Facet { address facetAddress; bytes4[] functionSelectors; } function facets() external view returns (Facet[] memory facets_); function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_); function facetAddresses() external view returns (address[] memory facetAddresses_); function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);}
DiamondCut
至於 facet在 diamond合約上的註冊或是修改,就由 diamondCut負責,從以下程式碼可以清楚瞭解其功能(EIP中有規範,每次改變都需要發送DiamondCut事件)
interface IDiamondCut { enum FacetCutAction {Add, Replace, Remove} // Add=0, Replace=1, Remove=2 struct FacetCut { address facetAddress; FacetCutAction action; bytes4[] functionSelectors; } function diamondCut( FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata ) external; event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata);}
Diamond合約
接下來就是最核心的部分 — diamond本體合約。以下是官方的範例,方法上跟 OpenZeppelin 一樣使用 fallback 函式跟 delegateCall 。
呼叫合約所不支援的函式,就會去執行 fallback 函式,fallback 函式中再透過 delegateCall 呼叫 facet 合約相對應的函式
fallback() external payable { address facet = selectorTofacet[msg.sig]; require(facet != address(0)); // Execute external function from facet using delegatecall and return any value. assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 {revert(0, returndatasize())} default {return (0, returndatasize())} }}
主要的差異在於變數的處理,OpenZepplin 是針對單一合約設計的代理合約(也就是每個合約都有自己的代理合約),所以無法處理單一代理合約儲存多個合約的變數(state variables)的狀況(後有圖例)。先由官方的範例程式來了解是怎麼處理變數的
在官方的範例中,都是以更改合約 owner 為例子
首先看到 DimaondStorage這個結構,結構中的前面三個變數都是在維持 diamond合約的運作(同上面loupe的範例),最後一個變數 contractOwner就是我們商業邏輯中所需的變數。
接著看到 function diamondStorage(),取變數的方式就跟OpenZeppelin 儲存特定變數方式一樣(EIP-1967),是把變數存到一個遠方不會跟其他變數碰撞到的位置,在這裡就是從 DIMOND_STORAGE_POSITION 這個 storage slot 讀取。
在實作上就可以有 LibDiamond1 ,宣告DIMOND_STORAGE_POSITION1=keccak256("diamond.standard.diamond.storage1") ,負責處理另一組的變數。藉由這種方式讓每個 facet合約有屬於自己合約的變數, facet合約間就不會互相影響。而最下方的 setContractOwner 是實際使用的範例。
library LibDiamond {
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
struct FacetAddressAndSelectorPosition { address facetAddress; uint16 selectorPosition; }
struct DiamondStorage { mapping(bytes4 => FacetAddressAndSelectorPosition) facetAddressAndSelectorPosition; bytes4[] selectors; mapping(bytes4 => bool) supportedInterfaces; // owner of the contract address contractOwner; }
function diamondStorage() internal pure returns (DiamondStorage storage ds) { bytes32 position = DIAMOND_STORAGE_POSITION; assembly { ds.slot := position } }
function setContractOwner(address _newOwner) internal { DiamondStorage storage ds = diamondStorage(); address previousOwner = ds.contractOwner; ds.contractOwner = _newOwner; emit OwnershipTransferred(previousOwner, _newOwner); }
每個 library 處理了一組或多組變數的存取, facet 合約透過 library 對變數做操作。也就是把變數存在diamond主體合約,延伸的 facet合約只處理邏輯,是透過 library 去操作變數。
下面圖中清楚地解釋了 facet合約,function selectors 與變數之間的關係,從最左上這邊有個 facets 的 map,紀錄了哪個 selector 在哪個合約中,例如func1, func2是 FacetA的函式。左下角宣告了變數,每組變數的存取如同上述 library 的方式處理。
https://eips.ethereum.org/EIPS/eip-2535#diagrams
在 diamond的設計中,每個 facet合約都是獨立的,因此可以重複使用(跟library 的概念一樣)
https://eips.ethereum.org/EIPS/eip-2535#diagrams
小結
diamond合約使用不同的設計來達成合約的可升級性,藉由這種Hub方式可隨時擴充/移除功能,讓合約不再受限於24KB的限制,此外充分的模組化,讓每次升級的範圍可以很小。最後,因為跟library一樣只處理邏輯,並無狀態儲存,所以可以重複被不同的diamond合約所使用。
雖然又不少好處,也是有些缺點。首先,術語名詞太多,facet, diamondCut, loupe等等(其實還有好幾個,不過沒有介紹到那些部分,所以沒有寫出來)。開發上不直覺,把變數跟邏輯拆開,若要再加上合約之間的繼承關係,容易搞混,不易維護。最後,gas的花費,在函式的讀取、呼叫,變數的存取、傳遞都會有不少的額外支出。Trail of Bits 專欄中有點出更多的缺陷 Good idea, bad design: How the Diamond standard falls short,不過作者也有反擊 Addressing Josselin Feist’s Concern’s of EIP-2535 Diamond Standard,有興趣的讀者可以自行看看、比較。
為了模組化及彈性,diamond合約在設計上有點太複雜(over engineering),會造成可讀性越差(這點也是Vyper誕生的原因之一),而可讀性越差就越容易產生bug、也越不容易抓到bug,而在defi專案中,一個小小的bug通常代表著大筆金額的損失 😱😱😱。
雖然如此,筆者還是覺得很酷,有些設計的思維仍然可以使用在自己的專案
ref:
EIP 2535
Diamond 實作
Addressing Josselin Feist’s Concern’s of EIP-2535 Diamond Standard
OpenZeppelin upgradeable contract
可升級合約介紹 - 鑽石合約(EIP-2535 Diamond standard) was originally published in Taipei Ethereum Meetup on Medium, where people are continuing the conversation by highlighting and responding to this story.
👏 歡迎轉載分享鼓掌
mapping function 在 Taipei Ethereum Meetup Facebook 的最佳貼文
📜 [專欄新文章] Reason Why You Should Use EIP1167 Proxy Contract. (With Tutorial)
✍️ Ping Chen
📥 歡迎投稿: https://medium.com/taipei-ethereum-meetup #徵技術分享文 #使用心得 #教學文 #medium
EIP1167 minimal proxy contract is a standardized, gas-efficient way to deploy a bunch of contract clones from a factory.
1. Who may consider using EIP1167
For some DApp that are creating clones of a contract for its users, a “factory pattern” is usually introduced. Users simply interact with the factory to get a copy. For example, Gnosis Multisig Wallet has a factory. So, instead of copy-and-paste the source code to Remix, compile, key in some parameters, and deploy it by yourself, you can just ask the factory to create a wallet for you since the contract code has already been on-chain.
The problem is: we need standalone contract instances for each user, but then we’ll have many copies of the same bytecode on the blockchain, which seems redundant. Take multisig wallet as an example, different multisig wallet instances have separate addresses to receive assets and store the wallet’s owners’ addresses, but they can share the same program logic by referring to the same library. We call them ‘proxy contracts’.
One of the most famous proxy contract users is Uniswap. It also has a factory pattern to create exchanges for each ERC20 tokens. Different from Gnosis Multisig, Uniswap only has one exchange instance that contains full bytecode as the program logic, and the remainders are all proxies. So, when you go to Etherscan to check out the code, you’ll see a short bytecode, which is unlikely an implementation of an exchange.
0x3660006000376110006000366000732157a7894439191e520825fe9399ab8655e0f7085af41558576110006000f3
What it does is blindly relay every incoming transaction to the reference contract 0x2157a7894439191e520825fe9399ab8655e0f708by delegatecall.
Every proxy is a 100% replica of that contract but serving for different tokens.
The length of the creation code of Uniswap exchange implementation is 12468 bytes. A proxy contract, however, has only 46 bytes, which is much more gas efficient. So, if your DApp is in a scenario of creating copies of a contract, no matter for each user, each token, or what else, you may consider using proxy contracts to save gas.
2. Why use EIP1167
According to the proposal, EIP is a “minimal proxy contract”. It is currently the known shortest(in bytecode) and lowest gas consumption overhead implementation of proxy contract. Though most ERCs are protocols or interfaces, EIP1167 is the “best practice” of a proxy contract. It uses some EVM black magic to optimize performance.
EIP1167 not only minimizes length, but it is also literally a “minimal” proxy that does nothing but proxying. It minimizes trust. Unlike other upgradable proxy contracts that rely on the honesty of their administrator (who can change the implementation), address in EIP1167 is hardcoded in bytecode and remain unchangeable.
That brings convenience to the community.
Etherscan automatically displays code for EIP1167 proxies.
When you see an EIP1167 proxy, you can definitely regard it as the contract that it points to. For instance, if Etherscan finds a contract meets the format of EIP1167, and the reference implementation’s code has been published, it will automatically use that code for the proxy contract. Unfortunately, non-standard EIP1167 proxies like Uniswap will not benefit from this kind of network effect.
3. How to upgrade a contract to EIP1167 compatible
*Please read all the steps before use, otherwise there might have problems.
A. Build a clone factory
For Vyper, there’s a function create_with_code_of(address)that creates a proxy and returns its address. For Solidity, you may find a reference implementation here.
function createClone(address target) internal returns (address result){ bytes20 targetBytes = bytes20(target); assembly { let clone := mload(0x40) mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) mstore(add(clone, 0x14), targetBytes) mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) result := create(0, clone, 0x37) }}
You can either deploy the implementation contract first or deploy it with the factory’s constructor. I’ll suggest the former, so you can optimize it with higher runs.
contract WalletFactory is CloneFactory { address Template = "0xc0ffee"; function createWallet() external returns (address newWallet) { newWallet = createClone(Template); }}
B. Replace constructor with initializer
When it comes to a contract, there are two kinds of code: creation code and runtime code. Runtime code is the actual business logic stored in the contract’s code slot. Creation code, on the other hand, is runtime code plus an initialization process. When you compile a solidity source code, the output bytecode you get is creation code. And the permanent bytecode you can find on the blockchain is runtime code.
For EIP1167 proxies, we say it ‘clones’ a contract. It actually clones a contract’s runtime code. But if the contract that it is cloning has a constructor, the clone is not 100% precise. So, we need to slightly modify our implementation contract. Replace the constructor with an ‘initializer’, which is part of the permanent code but can only be called once.
// constructorconstructor(address _owner) external { owner = _owner;}// initializerfunction set(address _owner) external { require(owner == address(0)); owner = _owner;}
Mind that initializer is not a constructor, so theoretically it can be called multiple times. You need to maintain the edge case by yourself. Take the code above as an example, when the contract is initialized, the owner must never be set to 0, or anyone can modify it.
C. Don’t assign value outside a function
As mentioned, a creation code contains runtime code and initialization process. A so-called “initialization process” is not only a constructor but also all the variable assignments outside a function. If an EIP1167 proxy points to a contract that assigns value outside a function, it will again have different behavior. We need to remove them.
There are two approaches to solve this problem. The first one is to turn all the variables that need to be assigned to constant. By doing so, they are no longer a variable written in the contract’s storage, but a constant value that hardcoded everywhere it is used.
bytes32 public constant symbol = "4441490000000000000000000000000000000000000000000000000000000000";uint256 public constant decimals = 18;
Second, if you really want to assign a non-constant variable while initializing, then just add it to the initializer.
mapping(address => bool) public isOwner;uint public dailyWithdrawLimit;uint public signaturesRequired;
function set(address[] _owner, uint limit, uint required) external { require(dailyWithdrawLimit == 0 && signaturesRequired == 0); dailyWithdrawLimit = limit; signaturesRequired = required; //DO SOMETHING ELSE}
Our ultimate goal is to eliminate the difference between runtime code and creation code, so EIP1167 proxy can 100% imitate its implementation.
D. Put them all together
A proxy contract pattern splits the deployment process into two. But the factory can combine two steps into one, so users won’t feel different.
contract multisigWallet { //wallet interfaces function set(address[] owners, uint required, uint limit) external;}contract walletFactory is cloneFactory { address constant template = "0xdeadbeef"; function create(address[] owners, uint required, uint limit) external returns (address) { address wallet = createClone(template); multisigWallet(wallet).set(owners, required, limit); return wallet; }}
Since both the factory and the clone/proxy has exactly the same interface, no modification is required for all the existing DApp, webpage, and tools, just enjoy the benefit of proxy contracts!
4. Drawbacks
Though proxy contract can lower the storage fee of deploying multiple clones, it will slightly increase the gas cost of each operation in the future due to the usage of delegatecall. So, if the contract is not so long(in bytes), and you expect it’ll be called millions of times, it’ll eventually be more efficient to not use EIP1167 proxies.
In addition, proxy pattern also introduces a different attack vector to the system. For EIP1167 proxies, trust is minimized since the address they point to is hardcoded in bytecode. But, if the reference contract is not permanent, some problems may happen.
You might ever hear of parity multisig wallet hack. There are multiple proxies(not EIP1167) that refer to the same implementation. However, the wallet has a self-destruct function, which empties both the storage and the code of a contract. Unfortunately, there was a bug in Parity wallet’s access control and someone accidentally gained the ownership of the original implementation. That did not directly steal assets from other parity wallets, but then the hacker deleted the original implementation, making all the remaining wallets a shell without functionality, and lock assets in it forever.
https://cointelegraph.com/news/parity-multisig-wallet-hacked-or-how-come
Conclusion
In brief, the proxy factory pattern helps you to deploy a bunch of contract clones with a considerably lower gas cost. EIP1167 defines a bytecode format standard for minimal proxy and it is supported by Etherscan.
To upgrade a contract to EIP1167 compatible, you have to remove both constructor and variable assignment outside a function. So that runtime code will contain all business logic that proxies may need.
Here’s a use case of EIP1167 proxy contract: create adapters for ERC1155 tokens to support ERC20 interface.
pelith/erc-1155-adapter
References
https://eips.ethereum.org/EIPS/eip-1167
https://blog.openzeppelin.com/on-the-parity-wallet-multisig-hack-405a8c12e8f7/
Donation:
pingchen.eth
0xc1F9BB72216E5ecDc97e248F65E14df1fE46600a
Reason Why You Should Use EIP1167 Proxy Contract. (With Tutorial) was originally published in Taipei Ethereum Meetup on Medium, where people are continuing the conversation by highlighting and responding to this story.
👏 歡迎轉載分享鼓掌
mapping function 在 映射- 維基百科,自由的百科全書 的相關結果
映射(英語:Map),或稱射影、寫像(Mapping),在數學及相關的領域經常等同於函數。基於此,部分映射就相當於部分函數,而完全映射相當於完全函數。 ... <看更多>
mapping function 在 Mapping Diagrams - Varsity Tutors 的相關結果
A function is a special type of relation in which each element of the domain is paired with exactly one element in the range . A mapping shows how the elements ... ... <看更多>
mapping function 在 mapping function - 作(遺傳連)鎖圖函數 - 國家教育研究院 ... 的相關結果
出處/學術領域, 英文詞彙, 中文詞彙. 學術名詞 生物學名詞-植物, mapping function, 作(遺傳連)鎖圖函數. 學術名詞 數學名詞, mapping function, 映射函數. 學術名詞 ... <看更多>