current position:Home>Solidity vs. Vyper: pros and cons of different smart contract languages

Solidity vs. Vyper: pros and cons of different smart contract languages

2022-11-23 23:40:54Chainlink

This paper discusses the following questions:Which smart contract language is more advantageous,Solidity 还是 Vyper?最近,about which is“最好的”There is a lot of debate about smart contract languages,当然了,Every language has its supporters.

This article seeks to answer the most fundamental questions of the debate:

Which smart contract language should I use?

In order to understand the nature of the problem,We'll start by talking about the tools and usability of the language,Then consider one of the main concerns of smart contract developers:gas 优化.具体来说,我们将研究四种 EVM 语言(可以在 EthereumAvalanchePolygon The language running on the chain):SolidityVyperHuffYul.Rust 并不在其中,It should appear in an essay about the EVM chain articles.

但首先,According to plot the results.

Solidity、Vyper、Huff 和 Yul Both are great languages ​​that let you get your job done. Solidity 和 Vyper 是高级语言,most people use.But if you're interested in writing near assembly code,那 Yul 和 Huff 也可以胜任.

So if you insist on choosing one or the other use,那就抛硬币吧:because no matter which language you choose,It's all possible to complete the project.If you are new to smart contracts,Start your journey in absolutely any language.

此外,The languages ​​are also changing all the time,You can pick and choose specific smart contracts and data,thus making the different languages ​​that run them,better or worse performance.所以请注意,in order to avoid unobjective,We are comparing different languages ​​in gas Advantages and disadvantages of optimization,Both choose the simplest smart contract as an example,if you have better example,Please also share with us!

现在,If you are a veteran in this field,Let's dive into these languages,look at their details.

EVM 编程语言

The four languages ​​we will study are as follows:

  • Solidity:目前 DeFi TVL (DeFi Locked Token Value)language with the largest share.是一种高级语言,类似于 JavaScript.
  • Vyper:目前 DeFi TVL second language.is also a high-level language,类似于 Python.
  • Huff:A low-level assembly-like language.
  • Yul:A low-level assembly-like language,内置于 Solidity(although some people think itstill a high-level language).

why these four?

Use these four languages,because they are related to EVM 兼容,而且其中的 Solidity 和 Vyper are by far the two most popular languages.我添加了 Yul,because without considering Yul 的情况下,与 Solidity 进行 gas Optimization comparisons are not comprehensive.我们添加了 Huff Is because want to in a way that is not Yul,But with almost is using opcode The language in which contracts are written serves as a benchmark.

就 EVM 而言,在 Vyper 和 Solidity 之后,第三、4th and 5th are also growing in popularity.For languages ​​not compared in this paper;just because they're not used as much.然而,There are many promising smart contract languages ​​on the rise,I look forward to being able to try them in the future.

什么是 Solidity?

Solidity 是一种面向对象的编程语言,For writing smart contracts on Ethereum and other blockchains. Solidity 深受 C++、Python 和 JavaScript 的影响,并且专为 EVM 而设计.

什么是 Vyper?

Vyper is a contract-oriented approach similar to Python 的编程语言,也是为 EVM 设计的. Vyper 增强了可读性,and restricts certain usage,从而改进了 Solidity.理论上,Vyper Improved security and auditability of smart contracts.

当前的情况

svv-1.png来源于 DefiLlama 语言分析数据

根据 DefiLlama 的数据,截至目前,在 DeFi 领域,Solidity The smart contract gets 87% 的 TVL,而 Vyper The smart contract gets 8%.

因此,If you choose languages ​​purely based on popularity,除了 Solidity,you don't need to look at anything else.

compare identical contracts

Now let's see what a contract written in each language looks like,然后比较它们的 gas 性能.

Here are four copies written in each language几乎相同的合同.does roughly the same thing as,它们都:

  1. Storage slot 0 有一个私有变量 number (uint256).
  2. 有一个带有 readNumber() 函数签名的函数,它读取 storage slot 0 中的内容.
  3. 允许你使用 storeNumber(uint256) The function signature updates the variable.

This is what the contract does.

All the code we use to compare languages ​​is in this GitHub repo 中.

Solidity

svv-2.png

Vyper

svv-3.png

* Huff

svv-4.png

🧮 Yul

svv-5.png

开发体验

By looking at these four pictures,We can get an overview of what it's like to write each language.In terms of developer experience,编写 Solidity 和 Vyper 代码要快得多.These languages ​​are high-level languages,而 Yul 和 Huff is a lower level language.仅出于这个原因,It's easy to see why so many people use Vyper 和 Solidity(They also last longer).

看一下 Vyper 和 Solidity,you can clearly feel Vyper 是从 Python 中汲取了灵感,而 Solidity 是从 JavaScript 和 Java 中汲取灵感.因此,If you are more familiar with these languages,Then you can use the corresponding smart contract language well.

Vyper 旨在成为一种简约、Easy-to-audit programming language,而 Solidity Aims to be a general-purpose smart contract language.The experience of coding is also on a syntactic level,But everyone must have their own subjective feelings.

I won't talk too much about tools,Because most of these languages ​​have very similar tools.主流框架,包括 Hardhat、ape、titanoboa、BrownieFoundry,都支持 Vyper 和 Solidity. Solidity in most frameworks,are prioritized,而 Vyper 需要使用插件才能与 Hardhat 等工具一起使用.然而,titanoboa is designed for use with Vyper built to work together,除此以外,Most tools support both very well.

Which smart contract language is more economical gas?

now is the main event.In more intelligent contracts gas 性能时,需要牢记两点:

  1. 合约创建 gas 成本
  2. 运行时 gas 成本

How you implement your smart contract can have a big impact on these factors.例如,You may store a large number of arrays in the contract code,This makes it expensive to deploy but cheaper to run the function.或者,You can have your function generate the array dynamically,Thus making the deployment cost of the contract lower,but it is more expensive to run the function.

那么,Let's look at the four contracts,and create their contracts gas consumption vs runtime gas consumption to compare.你可以在我的 sc-language-comparison repo find all the codes in,Includes frameworks and tools used to compare them.

Gas 消耗比较 - 总结

Here is how we compile the smart contract for this section:

vyper src/vyper/VSimpleStorage.vyhuffc src/huff/HSimpleStorage.huff -bsolc --strict-assembly --optimize --optimize-runs 20000yul/YYSimpleStorage.yul --binsolc --optimize --optimize-runs 20000 src/yulsol/YSimpleStorage.sol --binsolc --optimize --optimize-runs 20000 src/solidity/SSimpleStorage.sol --bin

注意:I can also serve Solidity 编译使用 –via-ir 标志.另请注意,Vyper 和 Solidity Added at the end of its contract“metadata”.This accounts for the total gas A fraction of the cost increase,but not enough to change the ranking below.我将在 metadata 部分详细讨论这一点.

结果:

svv-6.pngEach language consumes when creating a contract gas 费

正如我们所见,像 Huff 和 Yul Such a low-level language is more Vyper 和 Solidity 的 gas 效率更高,但这是为什么呢? Vyper 似乎比 Solidity 更高效,we have this new“Sol and Yul”部分.That's because you can actually Solidity 中编写 Yul. Yul 是作为 Solidity Created by developers when writing closer to machine code.

因此,在上图中,我们比较了原始 Yul、原始 Solidity 和 Solidity-Yul 组合.of our code Solidity-Yul 版本如下所示:

svv-7.pngYul 和 Solidity combined contract

You'll see an example later,其中这个 inline-Yul 对 gas Consumption had a major impact.We'll see later why these exist gas 差异,But now let's look at the same Foundry A single test in the related gas 消耗.

svv-8.png我们的测试函数

This will test the number 77 存储在 storage 中,然后从 storage read this number in gas 成本.The following is to run this test 的结果.

svv-9.pngSimpleStorage 读和写的 gas 对比

我们没有 Yul 的数据,Because to get this data you have to make a Yul-Foundry 插件,我不想做 - and the result may be different from Huff 相似.请记住,This is what runs the entire test function gas 成本,rather than just a single function.

Gas 消耗对比

好,Let's analyze this data.The first question we need to answer is:为什么 Huff 和 Yul contract creation ratio Vyper 和 Solidity 的 gas 效率高得多?We can find out by looking directly at the bytecode of these contracts.

when you write a smart contract,it is usually divided into two or three distinct parts.

  1. contract creation code
  2. 运行时代码
  3. Metadata (非必需)

对于这部分,了解 opcode 的基础知识很重要. OpenZeppelin 关于Deconstruct the contract's blog helps you learn relevant knowledge from scratch.

contract creation code

The contract creation code is the first part of the bytecode,告诉 EVM Write the contract to the chain.You can usually find this by looking in the generated binaries CODECOPY opcode (39),And then found it on a chain,并使用 RETURN opcode (f3) return and end the call.

Huff:602f8060093d393df3Yul:603e80600c6000396000f3feVyper:61006b61000f60003961006b6000f3Solidity:6080604052348015600f57600080fd5b5060ac8061001e6000396000f3feSolidity-Yul:608060405234801561001057600080fd5b5060bc8061001f6000396000f3fe

you'll also notice a lot fe opcode,这是 INVALID 操作码. Solidity Add these as markers to show runtime、contract creation and metadata Differences between codes.f3 是 RETURN 操作码,Usually a function or context 的结尾.

你可能会认为,因为 Yul-Solidity The contract creation bytecode occupies the largest space and Huff The bytecode occupies the smallest space,所以 Huff cheapest and Yul-Solidity 最贵.But when you copy the entire code base and send it to the chain,The size of the codebase can make a big difference,这才是决定性因素.然而,This contract creation code really gives us an idea of ​​how the compiler works,i.e. how they will compile the contract.

怎么读取 Opcode 和 Stack

目前,EVM 是一个stack based machine,This means that most of what you do“事情”都是从堆栈中 push 和 pull 内容.You'll see on the left that we have opcode,On the right we have two slashes (//) indicates that they are comments,as well as executing on the same line opcode What the back stack looks like,Left is the top of the stack,右边是栈底.

Huff opcode 的解释

Huff The contract is created to do only the simplest thing it can do.it fetches the code you write,and return it to the chain.

PUSH 0x2f        // [2f]DUP1             // [2f, 2f]PUSH 0x09        // [09, 2f, 2f]RETURNDATASIZE   // [0, 09, 2f, 2f]CODECOPY         // [2f]RETURNDATASIZE   // [0, 2f]RETURN           // []

Yul opcode 的解释

Yul 做同样的事情,It uses some different opcode,但本质上,It just puts your contract code on-chain,using as few opcodes as possible and a INVALID opcode.

PUSH 0x3e  // [3e]DUP1       // [3e, 3e]PUSH 0x0c  // [0c, 3e, 3e]PUSH 0x0   // [0, 0c, 3e, 3e]CODECOPY   // [3e]PUSH 0x0   // [0, e3]RETURN     // []INVALID    // []

Vyper opcode 解释

Vyper also basically does the same thing.

PUSH2 0x06B  // [06B]PUSH2 0x0F   // [0F, 06B]PUSH1 0x0    // [0, 0F, 06B]CODECOPY     // []PUSH2 0x06B  // [06B]PUSH1 0x0    // [0, 06B]RETURN       // []

Solidity opcode 解释

现在让我们看看 Solidity 的 opcode.

// Free Memory PointerPUSH1 0x80   // [80]PUSH1 0x40   // [40]MSTORE       // []// Check msg.valueCALLVALUE    // [msg.value]DUP1         // [msg.value, msg.value]ISZERO       // [msg.value == 0, msg.value]PUSH1 0xF    // [F, msg.value == 0, msg.value]JUMPI        // [msg.value] Jump to JUMPDEST if value is not sent// We only reach this part if msg.value has valuePUSH1 0x0    // [0, msg.value]DUP1         // [0, 0, msg.value]REVERT       // [msg.value]// Finally, put our code on-chainJUMPDEST     // [msg.value]POP          // []PUSH1 0xAC   // [AC]DUP1         // [AC, AC]PUSH2 0x1E   // [1E, AC, AC]PUSH1 0x0    // [0, 1E, AC, AC]CODECOPY     // [AC]PUSH1 0x0    // [0, AC]RETURN       // []INVALID      // []

Solidity 做了更多的事情. Solidity The first thing to do is create a file called Free Memory Pointer 的东西.In order to create a dynamic array in memory,You need to keep track of which parts of memory are free and available.We will not use this in contract construction code Free Memory Pointer,But that's the first thing it needs to do behind the scenes.This is the first major difference between languages:内存管理.Each language handles memory differently.

接下来,Solidity The compiler looks at your code,and notice that your constructor is not payable.因此,To make sure you don't mistakenly send the ETH,它使用 CALLVALUE opcode Check to make sure you didn't send any tokens when creating the contract.This is the second major difference between languages:They each have different checks and protections for common problems.

最后,Solidity also does what other languages ​​do:It sends your contract to the chain.

我们将跳过 Solidity-Yul,它的工作方式与 Solidity similar to itself.

check and protect

从这个意义上说,Solidity 似乎“更安全”,because it has more protections than other languages.但是,如果你要向 Vyper The code adds a constructor and then recompiles,you'll notice some differences.

svv-10.pngVyper 语言的构造函数

编译它,Your contract creation code would look more like Solidity 的.

// First, we check the callvalue, and jump to a JUMPDEST much later in the opcodesCALLVALUEPUSH2 0x080JUMPI// This part is identical to the original compilationPUSH2 0x06BPUSH2 0x014PUSH1 0x0CODECOPYPUSH2 0x06BPUSH1 0x0RETURN

it still doesn't Solidity memory management,But you'll see it checks using the constructor callvalue.If you make the constructor payable 并重新编译,then the check will disappear.

因此,Just by looking at the configuration when these contracts were created,We can draw two conclusions:

  1. 在 Huff and Yul 中,You need to explicitly write the check operation yourself.
  2. 而 Solidity 和 Vyper will check for you,Solidity Will do more inspection and protection.

This will be one of the biggest trade-offs between languages:What checks do they perform behind the scenes?Huff 和 Yul These two languages ​​don't do anything behind the scenes.So your code will save gas,but you will have a harder time avoiding and tracking down errors.

运行时代码

Now we have some insight into what's going on behind the scenes,We can see how the different functions of the contract are executed,and why they perform the way they do.

让我们看看调用 storeNumber() 函数,在每种语言中,它的值都为 77.I do this by using something like forge test –debug “testStorageAndReadSol” A command like this uses Forge Debug Feature 来获取 opcode.我还使用了 Huff VSCode Extension.

Huff opcode 解释

// First, we get the function selector of the call and jump to the code for our storeNumber functionPUSH 0x0         // [0]                                                                                                                                              CALLDATALOAD     // [b6339418] The function selector for storing                                                                                                                                   PUSH 0xe         // [e, b6339418]                                                                                   SHR              // [b6339418]                                                                                                                                               DUP1             // [b6339418, b6339418]                                                                                                                                              PUSH 0xb6339418  // [b6339418, b6339418, b6339418]                                                                                      EQ               // [true, b6339418]                                                                                                                                              PUSH 0x1c        // [1c, true, b6339418]                                                                                  JUMPI            // [b6339418]// We skip a bunch of opcodes since we jumped// We place the 77 in storage, and end the callJUMPDEST         // [b6339418]                                                                                                                                           PUSH 0x4         // [4, b6339418]                                                                                CALLDATALOAD     // [4d, b6339418] We load 77 from the calldata                                                                                                                                             PUSH 0x0         // [0, 4d, b6339418]                                                                                                                                          SSTORE           // [b6339418] Place the 77 in storage STOP             // [b6339418] End call

有趣的是,如果我们没有 STOP 操作码,我们的 Huff the code will actually add a set of opcodeto return the value we just stored,使其比 Vyper code is more expensive.But this code still looks very intuitive,那我们就来看看 Vyper 是怎么做的吧.我们暂时跳过 Yul,because the results will be very similar.

Vyper opcode 解释

// First, we do a check on the calldata size to make sure we have at least 4 bytes for a function selectorPUSH 0x3        // [3]CALLDATASIZE    // [3, 24]GT              // [true]PUSH 0x000c     // [000c, true]JUMPI           // []// Then, we jump to our location, and get the function selectorJUMPDESTPUSH 0x0        // [0]CALLDATALOAD    // [b6339418]PUSH 0xe        // [e, b6339418]SHR             // [b6339418]// And we do a check for sending valueCALLVALUE       // [0, b6339418]PUSH 0x0059     // [59, 0, b6339418]JUMPI           // [b6339418]// Value looks good, so we compare selectors, and jump if the selector is something elsePUSH 0xb6339418 // [b6339418, b6339418]DUP2            // [b6339418, b6339418, b6339418]XOR             // [0, b6339418]PUSH 0x0032     // [32, 0, b6339418]JUMPI           // [b6339418]// We do a check to make sure the calldata size is big enough for a function selector and a uint256PUSH 0x24       // [24, b6339418]CALLDATASIZE    // [24, 24, b6339418]XOR             // [0, b6339418]PUSH 0x0059     // [59, 0, b6339418]JUMPI           // [b6339418]// Then, we store the variable and end the callPUSH 0x04       // [4, b6339418]CALLDATALOAD    // [4d, b6339418]PUSH 0x0        // [0, 4d, b6339418]SSTORE          // [b6339418]STOP

You can see that some checks are done while storing the value:

  1. 对于 function selector 来说,calldata Is there enough bytes?
  2. 他们的 value 是通过 call did you send it?
  3. calldata 的大小和 function selector + uint256 的大小一样吗?

All these checks increase our computation,But they also mean we're more likely not to make mistakes.

Solidity opcode 解释

// Free Memory PointerPUSH 0x80        // [80]PUSH 0x40        // [40,80]MSTORE           // []// msg.value check, jump to function, revert otherwiseCALLVALUE        // [0]DUP1             // [0,0]ISZERO           // [true, 0]PUSH 0x0f        // [0f, true, 0]JUMPI            // [0]// Skip reverting code// We do a check to make sure the calldata size is big enough for a function selector and a uint256JUMPDEST         // [0]POP              // []PUSH 0x04        // [4]CALLDATASIZE     // [24, 4]LT               // [false]PUSH 0x32        // [32, false]JUMPI            // []// Find the function selector and jump to it's codePUSH 0x00        // [0]CALLDATALOAD     // [b6339418]PUSH 0xe0        // [e0, b6339418]SHR              // [b6339418]DUP1             // [b6339418, b6339418]PUSH 0xb6339418  // [b6339418, b6339418, b6339418]EQ               // [true, b6339418]PUSH 0x37        // [37, true, b6339418]JUMPI            // [b6339418]// Setup the function by checking the calldata size, and setup the stack for the functionJUMPDESTPUSH 0x47        // [47, b6339418]PUSH 0x42        // [42, 47, b6339418]CALLDATASIZE     // [24, 42, 47, b6339418]PUSH 0x04        // [4, 24, 42, 47, b6339418]PUSH 0x5e        // [5e, 4, 24, 42, 47, b6339418]JUMP             // [4, 24, 42, 47, b6339418]JUMPDEST         // [4, 24, 42, 47, b6339418]PUSH 0x00        // [0, 4, 24, 42, 47, b6339418]PUSH 0x20        // [20, 0, 4, 24, 42, 47, b6339418]DUP3             // [4, 20, 0, 4, 24, 42, 47, b6339418]DUP5             // [24, 4, 20, 0, 4, 24, 42, 47, b6339418]SUB              // [20, 20, 0, 4, 24, 42, 47, b6339418]// See if the calldatasize minus the function selector size is smaller than 32 bytesSLT              // [false(0), 0, 4, 24, 42, 47, b6339418]ISZERO           // [true, 0, 4, 24, 42, 47, b6339418]PUSH 0x6f        // [6f, true, 0, 4, 24, 42, 47, b6339418]JUMPI            // [0, 4, 24, 42, 47, b6339418]// Get the 77 value, and jump to the function selector codeJUMPDESTPOP              // [24, 42, 47, b6339418]CALLDATALOAD     // [4d, 24, 42, 47, b6339418]SWAP2            // [42, 24, 4d, 47, b6339418]SWAP1            // [24, 42, 4d, 47, b6339418]POP              // [42, 4d, 47, b6339418]JUMP             // [4d, 47, b6339418]JUMPDEST         // [4d, 47, b6339418]// Store our 77 value to storage and end the function callPUSH 0x00        // [0, 4d, 47, b6339418]SSTORE           // [47, b6339418]JUMP             // [b6339418]JUMPDEST         // [b6339418]STOP

There's a lot to explain here.这与 Huff What are some of the main differences between the codes?

  1. 我们设置了一个 free memory pointer.
  2. We checked the sent value.
  3. 我们检查了 function selector 的 calldata 大小.
  4. 我们检查了 uint256 的大小.

Solidity 和 Vyper 之间的主要区别是什么?

  1. Free memory pointer 的设置.
  2. Stack At some point the depth is much greater.

The combination of the two seems to be Vyper 比 Solidity 便宜的原因.同样有趣的是,Solidity 使用 ISZERO opcode 进行检查,而 Vyper 使用 XOR opcode;Both seem to need about the same gas.It is these small differences in design all the difference.

So we can now see why Huff 和 Yul 在 gas cheaper on:They just do what you tell them to do,仅此而已,而 Vyper 和 Solidity trying to protect you from making mistakes.

Free Memory Pointer

那么这个 free memory pointer 有什么用呢? Solidity 与 Vyper 之间的 gas Consumption seems to vary widely.free memory pointer is a feature that controls memory management——anytime you add something to your memory array,你的 free memory pointer both just point to the end of it,就像这样:

svv-11.png

这很有用,Because we may need to load data structures such as dynamic arrays into memory.对于动态数组,we don't know how big it is,So we need to know where the memory ends.

在 Vyper 中,Because there is no dynamic data structure,You have to tell how big an object like an array is.知道这一点,Vyper Memory can be allocated at compile time,并且没有 free memory pointer.

svv-12.png

This means that in terms of memory management,Vyper 可以比 Solidity 进行更多的 gas 优化.缺点是使用 Vyper You need to be explicit about the size of your data structures and cannot have dynamic memory.然而,Vyper The team actually sees this as an advantage.

动态数组

Leaving memory issues aside,使用 Vyper indeed the bounds of the array must be declared.在 Solidity 中,You can declare an array with no size.在 Vyper 中,you can have a dynamic array,但它必须是“有界的”.

This is bad for developer experience,但是,在 Web3 中,This can also be viewed as a denial of service(DOS)攻击的保护措施,and prevent a large number of gas 成本.

If your array becomes too large,and you iterate over it,may consume a large amount of gas.但是,If you explicitly declare the bounds of the array,you will know the worst.

Solidity vs. Yul vs. SolYul

Check out my chart above,使用 Solidity 和 Yul Seems like the worst option,Because contract creation code is much more expensive.It may be suitable for smaller projects,因为 Solidity did something to make Yul 运行,but on a large scale?

以 Solidity 版本和 SolYul One of the most popular items for version writing is Seaport 项目.

svv-13.pngSeaport 项目 Logo.

One of the best aspects of using these languages ​​is that you can run commands to test each contract's gas 使用效率.我们添加了一个 PR to help test pure Solidity 合约的 gas command consumed,因为 Sol-Yul The contract has been tested.结果非常惊人,你可以在 gas-report.txtgas-report-reference.txt see all data in.

svv-14.pngSeaport Medium contract creation gas consumption difference

svv-15.pngSeaport 中函数调用 gas consumption difference

平均而言,函数调用在 SolYul Performance improved on version 25%,And the performance of contract creation is improved 40%.

这节省了大量的 gas.I want to know what they are doing in pure Yul how much you can save in?i want to know where they are Vyper vs. Sol-Yul How much will save?

Metadata

最后,Metadata. Vyper 和 Solidity Both append some extra at the end of the contract“Metadata”.虽然数量很少,but we'll basically ignore it in our comparison here.you can delete it manually(and according to your Solidity code length adjustment markers),但 Solidity The team is also building a PR,你可以在remove it when compiling.

总结

Here is my take on these languages:

  1. If you are writing a smart contract,请使用 Vyper 或 Solidity.它们都是高级语言,Checked and protected,Like checking the call data size and whether you accidentally sent it when you shouldn't ETH.They are both great languages,So choose one of them and learn slowly.
  2. If you need extremely performant code,Yul 和 Huff 是很棒的工具.While I wouldn't recommend most people program in these languages,But they are still worth learning and understanding,will give you a better understanding EVM.
  3. Solidity 和 Vyper 之间 gas One of the main differences in cost is Solidity 中的 free memory pointer -Once you get to an advanced level and want to understand one of the potential differences between tools,请记住这一点.

Looking Forward

These languages ​​will continue to evolve,We may also see more languages ​​emerge,比如 Reach programming languagefe.

Solidity 和 Vyper 团队致力于开发 intermediate representation compilation step. Solidity 团队有一个 –via-ir 的 flag,This will help optimize Solidity 代码,Vyper teams also have their venom 作为 intermediate representation.

whatever language you choose,You can write some cool smart contracts.happy coding!

The views expressed in this article are solely those of the author,并不反映 Chainlink.

欢迎关注 Chainlink 预言机并且私信加入开发者社区,有大量关于智能合约的学习资料以及关于区块链的话题!

copyright notice
author[Chainlink],Please bring the original link to reprint, thank you.
https://en.netfreeman.com/2022/327/202211232322121335.html

Random recommended