从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限制。
个人经验:
"optimizer": { "enabled": true, "runs": 200 },即如果在一个合约方法被调用 200 次的情况下优化字节码。好不容易在一个合约中完成了应有的功能,但在编译时,报错了:
InternalCompilerError:Stack Too Deep, try removing local variables.
表面原因是合约方法function有参数个数限制,出、入参数、变量不能超过16个。本质上,EVM是一个栈机(后进先出),在设计EVM时就规定了栈的深度为1024个元素(栈用于存放局部变量、中间计算结果、函数调用),不能超出。这样设计有两个目的:一是为了合约代码执行安全,防止恶意攻击,如合约无限递归、栈无限增长等;二是为了合约代码更高效的执行。
个人经验:
个人经验: 是的,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在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成本。
以上扩展阅读内容已经转发在我的知识星球,可扫码阅读。

欢迎关注我的公众号“web3-notes”,一起拥抱 web3 世界。
基于全文检索与主题相似度
关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在Solidity开发中,搞懂数据存储位置就像掌握
关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在solidity开发中,合约间的交互是常见需求,
关注我,带你转型Web3开发,分享我从2018年入行区块链以来的开发经验(DeFi/GameFi/NFT/DAO/OTC/钱包/Trading Bot/MEME Launch),免费领取我私下整理的“智能合约学习”资料一份,帮你少走弯路! 说在前面 在Solidit开发中,ETH 转账是一项基础却至