Xinmin's Blog
AdminRSS
全部AIDeFiSolidity以太坊公链实战区块链行业观察
Admin
RSS

【solidity】分享我的一点合约开发经验

Solidity·2023年12月4日·1 次阅读·约 14 分钟
© 2026·

从17年开始学习区块链以来,笔者有幸参与开发诸多主流web3项目,如DeFi借贷、NFT交易平台、链游、IDO、O2C、DAO、版权、Fabric等DAPPs,一直以来想做些输出分享,今天从诸多项目中抓几个重要的点,笼统地分享一下,后续会分系列、分专题展开,持续更新,欢迎关注我的公众号。

需求变更了,合约无法升级?

做过DAPP开发的同学都知道,智能合约是存储在区块链上的一个协议,或者说是一种数据存储格式,也可以理解为分布式数据库,由于区块链的不可篡改性,在合约开发前,我们需要优先考虑它的可扩展性。

从需求出发,先考虑整体架构,梳理出有哪些合约对象(一个合约即一个对象),哪些合约是需要升级的,哪些是不要升级的。

个人经验:

非资产类的合约(ERC20、ERC721、ERC1155等)都要支持可升级,反之,纯资产类的合约都不要设计成可升级的(因为交易所不会支持)为了让合约支持升级,通常使用代理模式来部署合约。

比较常用的代理模式有3种:uups、transparent、beacon,个人推荐使用uups,相对来说uups更节省gas。

在使用代理模式后,要注意的是,在部署完合约后记得调用initialize()去初始化合约。Hardhat框架已经集成了以上提到的代理模式,且自动初始化合约,所以学会使用Hardhat框架,可以帮助我们事半功倍。

什么是代理模式? 从概念上来说,这跟以往程序语言中提到的代理模式是一样的,一对一代理。但实现上有些区别,合约中的代理模式是将合约分成了两个部分:代理合约(数据层)+业务逻辑合约(结构层)。代理合约负责存储数据和执行业务逻辑代码,业务逻辑合约则负责存储数据结构和业务逻辑代码。代理合约是通过solidity的匿名函数fallback()+delegatecall(calldata)方法来委托调用业务逻辑合约完成代理功能的。

在升级合约后,由于代理合约不变,原有的数据就不会丢失,相当于无缝迁移了。升级合约也有一些技巧,处理不好会踩坑,后续还会写一篇专题来介绍,这里不再赘述。

合约被攻击了?

solidity从0.4走到今天的0.8版本,期间发生了好些黑客事件,如The DAO的重入攻击、BEC的溢出攻击、构造函数失控等。我把它分成两类:一类是语言层面的,另一类是使用层面的。很多安全问题并不是solidity语言不好,而是我们开发人员太大意,与其吐槽solidity,不如学好语法的同时多学习已经出现的安全案例,以合约安全审计员的身份来要求自己,可能就会避开很多攻击。

个人经验:

开发同学如果要少踩坑,尽量选择成熟的经过审计的基础合约来做二次开发,比如openzeppelin/openzeppelin-contracts仓库。为了避免溢出漏洞,尽量选择0.8或以上版本,因为默认支持溢出和下溢检查。0.8以下的版本,可引入SafeMath库,如果合约中要用到类型转换,可以使用SafeCast库来确保不会发生溢出。为了避免重入攻击,转账ETH时能用address.tranfer(),就不用address.send()和address.call.value(amount)(),另外可以考虑禁止收款账号是合约地址(The DAO事件中收款方就是一个合约地址)。

如果项目会投入商用,建议找专门的安全审计公司出具审计报告,一方面项目方可以自证清白,另一方面尽可能提前排除合约的安全隐患。

openzeppelin是国外一家最早做合约开发、合约审计的公司,我从17年学习区块链开始,就时常用到了他的一些基础合约(ERC20/ERC721等),发展到今天,他的基础合约俨然成为了一种行业标准,以太坊和BSC链上的很多知名项目,如Uniswap、AAVE、MarkDAO都用它的代码来做二次开发。

合约代码超载了?

在开发一个DeFi的项目(x-maker)时,由于需求复杂,导致在一个合约中写了过多代码,部署时遇到了合约代码超载的问题:

Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet.

从以太坊EIP170提案中可以得知,一个合约的字节数不能超过24KB(24576 Byte),并在以太坊Spurious Dragon分叉中引入了这个限制。目的有两个:一是为了防止拒绝服务 (DOS) 攻击;二是在一笔交易中存放合约字节码所需要的gas可能会超出一个区块的最大gas限制。

个人经验:

  • 1、拆分合约代码,如将一些与业务关联不是那么紧密的代码,分离到library的库文件中,单独部署,再通过using for引入到主合约中;
  • 2、缩短错误信息,如require中的错误提示;
  • 3、简化合约方法和变量,删除非必要的代码;
  • 4、部署合约时,配置优化器,如"optimizer": { "enabled": true, "runs": 200 },即如果在一个合约方法被调用 200 次的情况下优化字节码。

合约编译报错:栈太深?

好不容易在一个合约中完成了应有的功能,但在编译时,报错了:

InternalCompilerError:Stack Too Deep, try removing local variables.

表面原因是合约方法function有参数个数限制,出、入参数、变量不能超过16个。本质上,EVM是一个栈机(后进先出),在设计EVM时就规定了栈的深度为1024个元素(栈用于存放局部变量、中间计算结果、函数调用),不能超出。这样设计有两个目的:一是为了合约代码执行安全,防止恶意攻击,如合约无限递归、栈无限增长等;二是为了合约代码更高效的执行。

个人经验:

  • 1、减少参数个数;
  • 2、实在不行,就将参数打包成一个struct,这也是Aave、Compound项目中的常用写法;
  • 3、在方法中使用代码块,将一些临时计算放到代码块中,相当于调用了一个子函数,由于代码块是独立的作用域,所以代码块中的变量数不被计入主方法的16个中,这种写法在Uniswap v2版本中可以见到,如Uniswap/v2-core中的swap()方法。

什么?合约中不能有小数

个人经验: 是的,solidity语言中没有浮点数类型,EVM也被设计成不支持小数(大部分公链都参考照搬了),主要是基于两方面的考虑:一是计算的确定性,另一个是区块链的共识机制。

在不同操作系统(windows/linux)、编程语言(solidity/vyper,move/go/rust)上,浮点数的精度和舍入规则会有所不同,这可能导致在不同的区块链节点上执行相同的合约,因为精度问题出现不同的结果(如DeFi中的利息计算、汇率兑换),破坏了计算的确定性。

正因为如此,区块链节点之间可能在计算结果上产生分歧,而导致无法达成共识。

合约的路由和权限管理乱糟糟?

当一个项目的合约文件超过了10个,合约之间需要互相调用,就应该对路由和权限进行统一管理。在近期(2023/09~12)中途接手改造一个链游项目时,就因为路由和权限管理特别混乱而导致BUG奇多。

个人经验:

可以考虑在一个全局的配置合约中去注册所有合约地址。开发之前将路由和权限设计好可以使得合约架构条理更清晰更安全。openzeppelin-contracts的合约仓库中提供一个基于RBAC的合约AccessControl,可以参考借鉴。

验签老是通不过?

在以太坊中,为了增加对签名的保护,引入了签名前缀,它与具体消息的哈希值一起用于构造消息的最终哈希,以供签名。

在 EIP-712 的上下文中,使用的签名前缀有两种形式:

\x19Ethereum Signed Message:\n
\x19Ethereum Signed Message:\n32

第一种前缀,用于包含域信息的消息。在 EIP-712 中,消息通常包括域和消息类型信息,以便更加结构化和透明。没有后缀的数字,表示消息的长度是不确定的,因为这个长度是消息的内容的长度,而不是哈希值的长度。

第二种前缀,用于没有域信息的消息。这是传统的签名前缀,用于简单的消息签名。32表示消息的长度(以字节为单位),这是因为 EIP-712 中的消息通常是一个 32 字节的哈希。

个人经验:

在编写带有签名功能的合约方法时,在对接WalletConnect时,都有踩到这个坑,验证签名时老是通不过,是因为漏掉或弄错了这个前缀,需要留意。推荐第一种带域的更安全。

map不能遍历,束手束脚

合约的数据结构,本质上就是键值对存储,map在solidity中是常用的一种数据类型,但使用时不能遍历,感觉束手束脚。

个人经验:map的遍历需要借助第三方库,如ethereum/IterableMapping,具体实现如下:

struct itmap
  {
    mapping(uint => IndexValue) data;  // 存放有哪些value
    KeyFlag[] keys; // 存放有哪些key和key的状态,比如是否已删除
    uint size; // 保存长度
  }
struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

把键和值分开存放,键放在数组中,值放在mapping中,然后用一变量保存map的长度,这样既可以遍历键,又可以遍历值。另外map的存储长度是没有限制的。

字符串的拼接和比较不太便利

个人经验:

字符串的拼接,需要先将字符串转换成bytes类型,然后通过遍历拼成一个新的bytes数组,最后将其转换成字符串。字符串比较时可以将两个字符串通过keccak256哈希后进行对比。如果在合约中字符串的操作很频繁,建议借助第三方库,如Arachnid/solidity-stringutils、openzeppelin-contracts中的Strings库。

注意:对字符串的处理一般都比较耗gas,需要考虑gas成本。

扩展阅读

  • 为什么合约字节码大小不能超过24K?
  • openzeppelin中的代理示例
  • Uniswap v2中的代码块示例
  • openzeppelin中的RBAC权限管理示例
  • IterableMapping遍历map示例
  • solidity字符串处理库
  • EIP 712提案详情

以上扩展阅读内容已经转发在我的知识星球,可扫码阅读。

Untitled


欢迎关注我的公众号“web3-notes”,一起拥抱 web3 世界。

继续阅读

基于全文检索与主题相似度

Solidity2025年4月20日

Solidity面试必问:storage、memory、calldata和transient sto

关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在Solidity开发中,搞懂数据存储位置就像掌握

Solidity2025年4月20日

Solidity面试必问:call、delegatecall、staticcall的区别

关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在solidity开发中,合约间的交互是常见需求,

Solidity2025年4月20日

Solidity面试必备:transfer、send、call的区别

关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在Solidit开发中,ETH 转账是一项基础却至