当支持 EIP712 的 Dapp 请求签名时,钱包会展示签名消息的原始数据,用户可以在验证数据符合预期之后签名。如果我们关心的只是字节串,那么对数据进行签名就是一个已解决的问题。不幸的是,在现实世界中,我们关心的是复杂而有意义的消息。此 EIP 旨在提高链下消息签名在链上使用的可用性。
- EIP712 签名必须包含一个 EIP712Domain 部分,它包含了合约的 name,version(一般约定为 “1”),chainId,和 verifyingContract(验证签名的合约地址)。
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
]
// 定义domain的数据:
const domain = {
name: "EIP712Storage",
version: "1",
chainId: "1",
verifyingContract: "0xC3916677350056A30D29D4dE42262704F0f77f8e",
};
- 你需要根据使用场景自定义一个签名的数据类型,他要与合约匹配。在 EIP712Storage 例子中,我们定义了一个 Storage 类型,它有两个成员: address 类型的 spender,指定了可以修改变量的调用者;uint256 类型的 number,指定了变量修改后的值。
const types = {
Storage: [
{ name: "spender", type: "address" },
{ name: "number", type: "uint256" },
],
};
// 要被签名的结构化数据
const message = {
spender: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
number: "100",
};
- 调用钱包的signTypeData方法,传入前面步骤中的 domain,types,和 message 变量进行签名(这里使用 ethersjs v6)。
// 获得provider
const provider = new ethers.BrowserProvider(window.ethereum)
// 获得signer后调用signTypedData方法进行eip712签名
const signature = await signer.signTypedData(domain, types, message);
console.log("Signature:", signature);
在ERC20的可以直接转账或者通过授权转账,但是都避免不了需要支付gas。在ERC20Permit中的一个拓展是:使用签名进行授权。达到改善用户体验的目的。
- 授权这步仅需用户在链下签名,减少一笔交易。
- 签名后,用户可以委托第三方进行后续交易,不需要持有 ETH:用户 A 可以将签名发送给 拥有gas的第三方 B,委托 B 来执行后续交易。
它定义了 3 个函数:
-
permit(): 根据 owner 的签名, 将 owenr 的ERC20代币余额授权给 spender,数量为 value。要求:
- spender 不能是零地址。
- deadline 必须是未来的时间戳。
- v,r 和 s 必须是 owner 对 EIP712 格式的函数参数的有效 keccak256 签名。
- 签名必须使用 owner 当前的 nonce。
-
nonces(): 返回 owner 的当前 nonce。每次为 permit() 函数生成签名时,都必须包括此值。每次成功调用 permit() 函数都会将 owner 的 nonce 增加 1,防止多次使用同一个签名。
-
DOMAIN_SEPARATOR(): 返回用于编码 permit() 函数的签名的域分隔符(domain separator),如 EIP712 所定义。
跨链桥是一种区块链协议,它允许在两个或多个区块链之间移动数字资产和信息。例如,一个在以太坊主网上运行的ERC20代币,可以通过跨链桥转移到其他兼容以太坊的侧链或独立链。
- Burn/Mint:在源链上销毁(burn)代币,然后在目标链上创建(mint)同等数量的代币。此方法好处是代币的总供应量保持不变,但是需要跨链桥拥有代币的铸造权限,适合项目方搭建自己的跨链桥。
- Stake/Mint:在源链上锁定(stake)代币,然后在目标链上创建(mint)同等数量的代币(凭证)。源链上的代币被锁定,当代币从目标链移回源链时再解锁。这是一般跨链桥使用的方案,不需要任何权限,但是风险也较大,当源链的资产被黑客攻击时,目标链上的凭证将变为空气。
- Stake/Unstake:在源链上锁定(stake)代币,然后在目标链上释放(unstake)同等数量的代币,在目标链上的代币可以随时兑换回源链的代币。这个方法需要跨链桥在两条链都有锁定的代币,门槛较高,一般需要激励用户在跨链桥锁仓。
MultiCall 多重调用合约,它的设计目的在于一次交易中执行多个函数调用,这样可以显著降低交易费用并提高效率。
-
方便性:MultiCall能让你在一次交易中对不同合约的不同函数进行调用,同时这些调用还可以使用不同的参数。比如你可以一次性查询多个地址的ERC20代币余额。
-
节省gas:MultiCall能将多个交易合并成一次交易中的多个调用,从而节省gas。
-
原子性:MultiCall能让用户在一笔交易中执行所有操作,保证所有操作要么全部成功,要么全部失败,这样就保持了原子性。比如,你可以按照特定的顺序进行一系列的代币交易。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Multicall {
// Call结构体,包含目标合约target,是否允许调用失败allowFailure,和call data
struct Call {
address target;
bool allowFailure;
bytes callData;
}
// Result结构体,包含调用是否成功和return data
struct Result {
bool success;
bytes returnData;
}
/// @notice 将多个调用(支持不同合约/不同方法/不同参数)合并到一次调用
/// @param calls Call结构体组成的数组
/// @return returnData Result结构体组成的数组
function multicall(Call[] calldata calls) public returns (Result[] memory returnData) {
uint256 length = calls.length;
returnData = new Result[](length);
Call calldata calli;
// 在循环中依次调用
for (uint256 i = 0; i < length; i++) {
Result memory result = returnData[i];
calli = calls[i];
(result.success, result.returnData) = calli.target.call(calli.callData);
// 如果 calli.allowFailure 和 result.success 均为 false,则 revert
if (!(calli.allowFailure || result.success)){
revert("Multicall: call failed");
}
}
}
}