cypto is the future!
cypto is the future!

Subscribe to cyptoJune

Subscribe to cyptoJune

Uniswap V2 交易所部署详解
在DeFi世界中,Dex(去中心化交易所)是最核心的一块,Uniswap是整个Dex的龙头,SushiSwap,PancakeSwap 等都是参考,甚至完全fork了Uniswap的整个产品逻辑和代码,也因此好多人也想部署快速部署一个属于自己的Uniswap,个人觉得也是很有必要的,通过自己从零搭建,一步一步完成,在这个过程可以清楚知道Uniswap整个项目工程的结构和产品逻辑,将来也能站在巨人的肩膀上,开发出自己的DeFi乐高! 前置说明:本次部署是参考Uniswap V2的线上版本,同时结合催眠大师的教程总结来的。本次部署的一些前置工作还需要自己提前准备,比如github账号,钱包,测试网代币水龙头,准备部署账户等部署流程步骤:从浏览器中下载合约源码准备部署账户使用remix编译部署合约部署前端代码安装依赖库修改路由地址将代码部署到Github Pages生成自定义的token自定义token导入Uniswap交易所正文 从浏览器中下载合约源码 这次用的是线上版本 工厂合约 路由合约2 路由合约1是可选的,部署的流程是先部署工厂合约,然后将工厂合约的地址复制给路由合约2的构造...
DeFi交易龙头之Uniswap:V2(核心合约-配对合约)
前面介绍了工厂合约,我们知道配对合约其实是需要工厂合约来进行部署的。从代码上来看,配对合约是继承ERC20合约,那么配对合约实际上就是一个遵守Erc20合约的token. 代码解析类型方法增强从上图看,首先是讲SafeMath和UQ112*112 的库方法给到了对应类型上。为什么要赋予uint224呢,这是因为在solidity中没有非整形的类型,但是实际上token的数量会出现小数位,使用库UQ112**112去模拟浮点数。 2. 常量16行中定义了一个最小流动性,在白皮书的3.4中初始化流动性代币供应这节会讲到,结论就是通过保证最小数量的流动性份额,会大大增加上述攻击的成本。具体的原理我会单独在开一篇章节进行讲解。 18行的selector的常量值是transfer(address, uint256)字符串哈希值的前4个字节,这个用于直接使用call方法调用token的转账方法。 22行工厂地址:因为pair合约是通过工厂合约进行部署的,所有会有一个变量专门去存放工厂合约地址。 23到27行主要是token地址和储备量地址相关。主要是存放两个token的地址,便于调用。储备量...

Uniswap V2 交易所部署详解
在DeFi世界中,Dex(去中心化交易所)是最核心的一块,Uniswap是整个Dex的龙头,SushiSwap,PancakeSwap 等都是参考,甚至完全fork了Uniswap的整个产品逻辑和代码,也因此好多人也想部署快速部署一个属于自己的Uniswap,个人觉得也是很有必要的,通过自己从零搭建,一步一步完成,在这个过程可以清楚知道Uniswap整个项目工程的结构和产品逻辑,将来也能站在巨人的肩膀上,开发出自己的DeFi乐高! 前置说明:本次部署是参考Uniswap V2的线上版本,同时结合催眠大师的教程总结来的。本次部署的一些前置工作还需要自己提前准备,比如github账号,钱包,测试网代币水龙头,准备部署账户等部署流程步骤:从浏览器中下载合约源码准备部署账户使用remix编译部署合约部署前端代码安装依赖库修改路由地址将代码部署到Github Pages生成自定义的token自定义token导入Uniswap交易所正文 从浏览器中下载合约源码 这次用的是线上版本 工厂合约 路由合约2 路由合约1是可选的,部署的流程是先部署工厂合约,然后将工厂合约的地址复制给路由合约2的构造...
DeFi交易龙头之Uniswap:V2(核心合约-配对合约)
前面介绍了工厂合约,我们知道配对合约其实是需要工厂合约来进行部署的。从代码上来看,配对合约是继承ERC20合约,那么配对合约实际上就是一个遵守Erc20合约的token. 代码解析类型方法增强从上图看,首先是讲SafeMath和UQ112*112 的库方法给到了对应类型上。为什么要赋予uint224呢,这是因为在solidity中没有非整形的类型,但是实际上token的数量会出现小数位,使用库UQ112**112去模拟浮点数。 2. 常量16行中定义了一个最小流动性,在白皮书的3.4中初始化流动性代币供应这节会讲到,结论就是通过保证最小数量的流动性份额,会大大增加上述攻击的成本。具体的原理我会单独在开一篇章节进行讲解。 18行的selector的常量值是transfer(address, uint256)字符串哈希值的前4个字节,这个用于直接使用call方法调用token的转账方法。 22行工厂地址:因为pair合约是通过工厂合约进行部署的,所有会有一个变量专门去存放工厂合约地址。 23到27行主要是token地址和储备量地址相关。主要是存放两个token的地址,便于调用。储备量...
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers


如果大家用过钱包,相信对于签名都不会陌生。

以太坊使用的数字签名算法叫双椭圆曲线数字签名算法(ECDSA),基于双椭圆曲线“私钥-公钥”对的数字签名算法。它主要有三种作用:
身份认证:证明签名方时私钥的持有人
不可否认:发送方不能否认发送过这个消息
完整性:消息在传输过程中无法被修改
在openzeppelin中提供了现成的ECDSA.sol合约,具体大家可以去官网看
创建签名
创建签名分为两步,第一步是打包消息,第二步是计算以太坊签名消息
打包消息: 在以太坊的ECDSA标准中,被签名的消息是一组数据的keccak256哈希,为bytes32类型。我们可以把任何想要签名的内容利用abi.encodePacked()函数打包,然后用keccak256()计算哈希,作为消息。
/*
* 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash
* _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
* _tokenId: 0
* 对应的消息msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
*/
function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
return keccak256(abi.encodePacked(_account, _tokenId));
}
计算以太坊签名消息: 消息可以是能被执行的交易,也可以是其他任何形式。为了避免用户误签了恶意交易,EIP191提倡在消息前加上"\x19Ethereum Signed Message:\n32"字符,并再做一次keccak256哈希,作为以太坊签名消息。经过toEthSignedMessageHash()函数处理后的消息,不能被用于执行交易。
[ /** * @dev 返回 以太坊签名消息 * `hash`:消息哈希 * 遵从以太坊签名标准:https://eth.wiki/json-rpc/API#eth_sign\[\`eth_sign\`\] * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191\` * 添加"\x19Ethereum Signed Message:\n32"字段,防止签名的是可执行交易。 */ function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { // 哈希的长度为32 return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); }]( /** * @dev 返回 以太坊签名消息 * `hash`:消息哈希 * 遵从以太坊签名标准:https://eth.wiki/json-rpc/API#eth_sign\[\`eth_sign\`\] * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191\` * 添加"\x19Ethereum Signed Message:\n32"字段,防止签名的是可执行交易。 */ function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { // 哈希的长度为32 return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); })
签名工具
一般我们用两种方法进行签名,一种是钱包签名,另外一种是脚本签名。
钱包签名
日常操作中,大部分用户都是通过这种方式进行签名。在获取到需要签名的消息之后,我们需要使用metamask钱包进行签名。metamask的personal_sign方法会自动把消息哈希转换为以太坊签名消息,然后发起签名。所以我们只需要输入消息哈希hash和签名者钱包account即可。需要注意的是输入的签名者钱包account需要和metamask当前连接的account一致才能使用。
脚本签名
利用web3.py签名: 批量调用中更倾向于使用代码进行签名,具体的代码可自行搜索,下面我给出一段.py的参考代码
[from web3 import Web3, HTTPProvider
from eth_account.messages import encode_defunct
private_key = "0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b" address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" rpc = 'https://rpc.ankr.com/eth' w3 = Web3(HTTPProvider(rpc))
#打包信息 msg = Web3.solidityKeccak(['address','uint256'], [address,0]) print(f"消息:{msg.hex()}") #构造可签名信息 message = encode_defunct(hexstr=msg.hex()) #签名 signed_message = w3.eth.account.sign_message(message, private_key=private_key) print(f"签名:{signed_message['signature'].hex()}")](from web3 import Web3, HTTPProvider from eth_account.messages import encode_defunct
private_key = "0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b" address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" rpc = 'https://rpc.ankr.com/eth' w3 = Web3(HTTPProvider(rpc))
#打包信息 msg = Web3.solidityKeccak(['address','uint256'], [address,0]) print(f"消息:{msg.hex()}") #构造可签名信息 message = encode_defunct(hexstr=msg.hex()) #签名 signed_message = w3.eth.account.sign_message(message, private_key=private_key) print(f"签名:{signed_message['signature'].hex()}"))
验证签名
上面讲解的是一些数字签名的基本知识,回到本文中心,我们探讨如何在实际项目中利用数字签名进行Nft的白名单的发放。我们知道,我们平时使用的区块链地址,**都分为私钥和地址。私钥可以生成地址,而反过来,地址不能生成私钥。那么如何证明一个地址是属于某一个人呢?我们可以给他一段信息,让他用私钥对这段信息进行签名。我们拿到这段签名,再结合这个地址,就可以证明,这段信息是否是由这个地址对应的私钥签名,从而也就证明了地址的所有权。**
上面这段原理理解之后,就可以运用到实际项目开发中。
验证逻辑
项目方后台数据库中保存所有的白名单用户
用户在网站连接钱包后,前端将用户地址发送给后端
后端检查该地址是否是白名单用户,如果是,则用后端的管理员私钥对地址进行签名
后端返回签名数据,同样,前端会将签名数据传给合约验证
合约验证通过,则用户可以白名单 mint
上面我们建议使用OpenZeppelin的ECDSA库进行引用,在项目实际开发中,主要调用的方法是
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
hash → 原始待签名信息的哈希值
signature → 签名数据 返回值是对这段信息签名的地址,也就是我们前面在步骤 3 中所说的管理员地址。也就是说,我们需要将管理员的地址设置在合约中进行比对。
通过上面的验证逻辑,可分为链上和链下进行。
链上部分
链上部分主要是对签名数据进行验证,看是否是管理员的私钥进行签名。
pragma solidity 0.8.13;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract TestSig {
// 使用 using 方法,就可以直接使用 bytes32 类型调用方法
using ECDSA for bytes32;
address public owner;
constructor() {
// 管理员地址(仅测试,不要对这个地址转账)
owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
}
function verify(bytes memory signature) public view returns (bool) {
// 验证签名者是否是管理员
return recoverSigner(signature) == owner;
}
function recoverSigner(bytes memory signature) public view returns (address) {
// 注意这里待哈希的内容需要与链下签名方法保持一致即可
// 可以加盐或者其他数据来保持唯一性,防止重放攻击
// 这里简单起见,仅对调用者的地址进行哈希签名
bytes32 messageHash = keccak256(abi.encodePacked(msg.sender));
// 调用 recover 验证签名地址
address signer = messageHash.toEthSignedMessageHash().recover(signature);
return signer;
}
}
这段代码就可以为我们生成签名数据,我们将签名数据传入合约的 verify 方法,就可以验证签名信息的准确性。
注:我们这里的签名信息仅仅是对合约发送者进行签名,这可能会造成签名信息被重复利用从而造成重放攻击。因此我们一般在实际项目开发中都会加上一些其他的信息,比如合约本身的地址(这样签名就确认只能应用于这个合约),或者盐(随机数确保随机)
如果加上合约本身的地址作源信息,那么代码就需要改成:
// Solidity
bytes32 messageHash = keccak256(abi.encodePacked(address(this), msg.sender));
// Javascript
let messageHash = ethers.utils.solidityKeccak256(['address', 'address'], [message, message]);
总结:
如果大家用过钱包,相信对于签名都不会陌生。

以太坊使用的数字签名算法叫双椭圆曲线数字签名算法(ECDSA),基于双椭圆曲线“私钥-公钥”对的数字签名算法。它主要有三种作用:
身份认证:证明签名方时私钥的持有人
不可否认:发送方不能否认发送过这个消息
完整性:消息在传输过程中无法被修改
在openzeppelin中提供了现成的ECDSA.sol合约,具体大家可以去官网看
创建签名
创建签名分为两步,第一步是打包消息,第二步是计算以太坊签名消息
打包消息: 在以太坊的ECDSA标准中,被签名的消息是一组数据的keccak256哈希,为bytes32类型。我们可以把任何想要签名的内容利用abi.encodePacked()函数打包,然后用keccak256()计算哈希,作为消息。
/*
* 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash
* _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
* _tokenId: 0
* 对应的消息msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
*/
function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
return keccak256(abi.encodePacked(_account, _tokenId));
}
计算以太坊签名消息: 消息可以是能被执行的交易,也可以是其他任何形式。为了避免用户误签了恶意交易,EIP191提倡在消息前加上"\x19Ethereum Signed Message:\n32"字符,并再做一次keccak256哈希,作为以太坊签名消息。经过toEthSignedMessageHash()函数处理后的消息,不能被用于执行交易。
[ /** * @dev 返回 以太坊签名消息 * `hash`:消息哈希 * 遵从以太坊签名标准:https://eth.wiki/json-rpc/API#eth_sign\[\`eth_sign\`\] * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191\` * 添加"\x19Ethereum Signed Message:\n32"字段,防止签名的是可执行交易。 */ function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { // 哈希的长度为32 return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); }]( /** * @dev 返回 以太坊签名消息 * `hash`:消息哈希 * 遵从以太坊签名标准:https://eth.wiki/json-rpc/API#eth_sign\[\`eth_sign\`\] * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191\` * 添加"\x19Ethereum Signed Message:\n32"字段,防止签名的是可执行交易。 */ function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { // 哈希的长度为32 return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); })
签名工具
一般我们用两种方法进行签名,一种是钱包签名,另外一种是脚本签名。
钱包签名
日常操作中,大部分用户都是通过这种方式进行签名。在获取到需要签名的消息之后,我们需要使用metamask钱包进行签名。metamask的personal_sign方法会自动把消息哈希转换为以太坊签名消息,然后发起签名。所以我们只需要输入消息哈希hash和签名者钱包account即可。需要注意的是输入的签名者钱包account需要和metamask当前连接的account一致才能使用。
脚本签名
利用web3.py签名: 批量调用中更倾向于使用代码进行签名,具体的代码可自行搜索,下面我给出一段.py的参考代码
[from web3 import Web3, HTTPProvider
from eth_account.messages import encode_defunct
private_key = "0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b" address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" rpc = 'https://rpc.ankr.com/eth' w3 = Web3(HTTPProvider(rpc))
#打包信息 msg = Web3.solidityKeccak(['address','uint256'], [address,0]) print(f"消息:{msg.hex()}") #构造可签名信息 message = encode_defunct(hexstr=msg.hex()) #签名 signed_message = w3.eth.account.sign_message(message, private_key=private_key) print(f"签名:{signed_message['signature'].hex()}")](from web3 import Web3, HTTPProvider from eth_account.messages import encode_defunct
private_key = "0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b" address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" rpc = 'https://rpc.ankr.com/eth' w3 = Web3(HTTPProvider(rpc))
#打包信息 msg = Web3.solidityKeccak(['address','uint256'], [address,0]) print(f"消息:{msg.hex()}") #构造可签名信息 message = encode_defunct(hexstr=msg.hex()) #签名 signed_message = w3.eth.account.sign_message(message, private_key=private_key) print(f"签名:{signed_message['signature'].hex()}"))
验证签名
上面讲解的是一些数字签名的基本知识,回到本文中心,我们探讨如何在实际项目中利用数字签名进行Nft的白名单的发放。我们知道,我们平时使用的区块链地址,**都分为私钥和地址。私钥可以生成地址,而反过来,地址不能生成私钥。那么如何证明一个地址是属于某一个人呢?我们可以给他一段信息,让他用私钥对这段信息进行签名。我们拿到这段签名,再结合这个地址,就可以证明,这段信息是否是由这个地址对应的私钥签名,从而也就证明了地址的所有权。**
上面这段原理理解之后,就可以运用到实际项目开发中。
验证逻辑
项目方后台数据库中保存所有的白名单用户
用户在网站连接钱包后,前端将用户地址发送给后端
后端检查该地址是否是白名单用户,如果是,则用后端的管理员私钥对地址进行签名
后端返回签名数据,同样,前端会将签名数据传给合约验证
合约验证通过,则用户可以白名单 mint
上面我们建议使用OpenZeppelin的ECDSA库进行引用,在项目实际开发中,主要调用的方法是
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
hash → 原始待签名信息的哈希值
signature → 签名数据 返回值是对这段信息签名的地址,也就是我们前面在步骤 3 中所说的管理员地址。也就是说,我们需要将管理员的地址设置在合约中进行比对。
通过上面的验证逻辑,可分为链上和链下进行。
链上部分
链上部分主要是对签名数据进行验证,看是否是管理员的私钥进行签名。
pragma solidity 0.8.13;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract TestSig {
// 使用 using 方法,就可以直接使用 bytes32 类型调用方法
using ECDSA for bytes32;
address public owner;
constructor() {
// 管理员地址(仅测试,不要对这个地址转账)
owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
}
function verify(bytes memory signature) public view returns (bool) {
// 验证签名者是否是管理员
return recoverSigner(signature) == owner;
}
function recoverSigner(bytes memory signature) public view returns (address) {
// 注意这里待哈希的内容需要与链下签名方法保持一致即可
// 可以加盐或者其他数据来保持唯一性,防止重放攻击
// 这里简单起见,仅对调用者的地址进行哈希签名
bytes32 messageHash = keccak256(abi.encodePacked(msg.sender));
// 调用 recover 验证签名地址
address signer = messageHash.toEthSignedMessageHash().recover(signature);
return signer;
}
}
这段代码就可以为我们生成签名数据,我们将签名数据传入合约的 verify 方法,就可以验证签名信息的准确性。
注:我们这里的签名信息仅仅是对合约发送者进行签名,这可能会造成签名信息被重复利用从而造成重放攻击。因此我们一般在实际项目开发中都会加上一些其他的信息,比如合约本身的地址(这样签名就确认只能应用于这个合约),或者盐(随机数确保随机)
如果加上合约本身的地址作源信息,那么代码就需要改成:
// Solidity
bytes32 messageHash = keccak256(abi.encodePacked(address(this), msg.sender));
// Javascript
let messageHash = ethers.utils.solidityKeccak256(['address', 'address'], [message, message]);
总结:
链下部分
链下部分使用 ethers 的 JavaScript 库进行签名操作,需要安装 ethers 的依赖。
npm install --save-dev ethers
const ethers = require('ethers');
const main = async () => {
// 管理员地址与私钥(仅测试,不要对这个地址转账)
const owner = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266';
const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
const signer = new ethers.Wallet(privateKey);
console.log(signer.address)
// 由于我们之前是使用了 msg.sender 进行签名
// 因此这里需要改为合约的调用地址
let message = '0xxxxx';
// 计算信息的哈希值
let messageHash = ethers.utils.solidityKeccak256(['address'], [message]);
console.log("Message Hash: ", messageHash);
// 由于 ethers 库的要求,需要先对哈希值数组化
let messageBytes = ethers.utils.arrayify(messageHash);
console.log("messageBytes: ", messageBytes);
// 签名
let signature = await signer.signMessage(messageBytes);
console.log("Signature: ", signature);
}
const runMain = async () => {
try {
await main();
process.exit(0);
}
catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
链下部分
链下部分使用 ethers 的 JavaScript 库进行签名操作,需要安装 ethers 的依赖。
npm install --save-dev ethers
const ethers = require('ethers');
const main = async () => {
// 管理员地址与私钥(仅测试,不要对这个地址转账)
const owner = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266';
const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
const signer = new ethers.Wallet(privateKey);
console.log(signer.address)
// 由于我们之前是使用了 msg.sender 进行签名
// 因此这里需要改为合约的调用地址
let message = '0xxxxx';
// 计算信息的哈希值
let messageHash = ethers.utils.solidityKeccak256(['address'], [message]);
console.log("Message Hash: ", messageHash);
// 由于 ethers 库的要求,需要先对哈希值数组化
let messageBytes = ethers.utils.arrayify(messageHash);
console.log("messageBytes: ", messageBytes);
// 签名
let signature = await signer.signMessage(messageBytes);
console.log("Signature: ", signature);
}
const runMain = async () => {
try {
await main();
process.exit(0);
}
catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
No activity yet