EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer

Subscribe to xyyme.eth

Subscribe to xyyme.eth
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
前面的文章我们介绍过,NFT 白名单校验主要有两种方式:
Merkle Tree
签名验证
这篇文章我们就来介绍一下签名验证的实现方法。我们这篇文章不会注重于签名的数学原理,仅仅对于链上与链下交互的实现做介绍。
在开始之前,我们先要了解一个基础知识:我们平时使用的区块链地址,都分为私钥和地址。私钥可以生成地址,而反过来,地址不能生成私钥。那么如何证明一个地址是属于某一个人呢?我们可以给他一段信息,让他用私钥对这段信息进行签名。我们拿到这段签名,再结合这个地址,就可以证明,这段信息是否是由这个地址对应的私钥签名,从而也就证明了地址的所有权。
明白了这个简单的原理之后,我们来看看实际项目中的白名单签名验证是什么步骤:
项目方后台数据库中保存所有的白名单用户
用户在网站连接钱包后,前端将用户地址发送给后端
后端检查该地址是否是白名单用户,如果是,则用后端的管理员私钥对地址进行签名
后端返回签名数据,同样,前端会将签名数据传给合约验证
合约验证通过,则用户可以白名单 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 中所说的管理员地址。也就是说,我们需要将管理员的地址设置在合约中进行比对,注意管理员的地址一定不能作为参数传入,否则用任意一个地址作签名之后再传入这个地址作验证,那结果一定是满足的。
对于这段代码调用的 tryRecover 中的逻辑我们就不再详述了,里面涉及到了数学原理,感兴趣的同学可以自行看看代码。
接下来我们利用 ECDSA 来实现验证逻辑:
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;
}
}
链上逻辑实现后,我们还要在链下对信息进行签名。我们使用 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();
这段代码就可以为我们生成签名数据,我们将签名数据传入合约的 verify 方法,就可以验证签名信息的准确性。
我们这里的签名信息仅仅是对合约发送者进行签名,这可能会造成签名信息被重复利用从而造成重放攻击。因此我们一般在实际项目开发中都会加上一些其他的信息,比如合约本身的地址(这样签名就确认只能应用于这个合约),或者盐(随机数确保随机)。
如果加上合约本身的地址作源信息,那么代码就需要改成:
// Solidity
bytes32 messageHash = keccak256(abi.encodePacked(address(this), msg.sender));
// Javascript
let messageHash = ethers.utils.solidityKeccak256(['address', 'address'], [message, message]);
关于 solidityKeccak256 的使用方法可以参考文档。
加盐的方法与上面类似,小幽灵(The Weirdo Ghost Gang)这个项目代码的白名单验证实现中就加入的盐的信息:


签名验证的数学逻辑比较复杂,但是已经有成熟的库将其封装好了,对于我们实际使用来说,难度不大,基本都是套路,多看看代码,多写几遍就很熟练了。
前面的文章我们介绍过,NFT 白名单校验主要有两种方式:
Merkle Tree
签名验证
这篇文章我们就来介绍一下签名验证的实现方法。我们这篇文章不会注重于签名的数学原理,仅仅对于链上与链下交互的实现做介绍。
在开始之前,我们先要了解一个基础知识:我们平时使用的区块链地址,都分为私钥和地址。私钥可以生成地址,而反过来,地址不能生成私钥。那么如何证明一个地址是属于某一个人呢?我们可以给他一段信息,让他用私钥对这段信息进行签名。我们拿到这段签名,再结合这个地址,就可以证明,这段信息是否是由这个地址对应的私钥签名,从而也就证明了地址的所有权。
明白了这个简单的原理之后,我们来看看实际项目中的白名单签名验证是什么步骤:
项目方后台数据库中保存所有的白名单用户
用户在网站连接钱包后,前端将用户地址发送给后端
后端检查该地址是否是白名单用户,如果是,则用后端的管理员私钥对地址进行签名
后端返回签名数据,同样,前端会将签名数据传给合约验证
合约验证通过,则用户可以白名单 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 中所说的管理员地址。也就是说,我们需要将管理员的地址设置在合约中进行比对,注意管理员的地址一定不能作为参数传入,否则用任意一个地址作签名之后再传入这个地址作验证,那结果一定是满足的。
对于这段代码调用的 tryRecover 中的逻辑我们就不再详述了,里面涉及到了数学原理,感兴趣的同学可以自行看看代码。
接下来我们利用 ECDSA 来实现验证逻辑:
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;
}
}
链上逻辑实现后,我们还要在链下对信息进行签名。我们使用 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();
这段代码就可以为我们生成签名数据,我们将签名数据传入合约的 verify 方法,就可以验证签名信息的准确性。
我们这里的签名信息仅仅是对合约发送者进行签名,这可能会造成签名信息被重复利用从而造成重放攻击。因此我们一般在实际项目开发中都会加上一些其他的信息,比如合约本身的地址(这样签名就确认只能应用于这个合约),或者盐(随机数确保随机)。
如果加上合约本身的地址作源信息,那么代码就需要改成:
// Solidity
bytes32 messageHash = keccak256(abi.encodePacked(address(this), msg.sender));
// Javascript
let messageHash = ethers.utils.solidityKeccak256(['address', 'address'], [message, message]);
关于 solidityKeccak256 的使用方法可以参考文档。
加盐的方法与上面类似,小幽灵(The Weirdo Ghost Gang)这个项目代码的白名单验证实现中就加入的盐的信息:


签名验证的数学逻辑比较复杂,但是已经有成熟的库将其封装好了,对于我们实际使用来说,难度不大,基本都是套路,多看看代码,多写几遍就很熟练了。
No activity yet