EIP712合约与前端的交互流程

  • 黄金叶
  • 发布于 2022-08-05 16:09
  • 阅读 4508

这是一篇关于介绍 EIP712合约的文章: EIP712用百度的话术来说, 是一种更高级, 更安全的交易签名方法. 在许多大型的项目中都会涉及到..完全去中心化的 Uniswap也运用了这个机制, 把账户的授权利用签名给到路由合约。

● 这是一篇关于介绍 EIP712合约的文章: EIP712用百度的话术来说, 是一种更高级, 更安全的交易签名方法. 在许多大型的项目中都会涉及到..完全去中心化的 Uniswap也运用了这个机制, 把账户的授权利用签名给到路由合约

场景介绍

假设现在有一个需求. 商城系统里面的积分系统需要升级成代币, 让用户消费. 那么之前数据库中的积分就需要等比兑换成Token给到用户的钱包地址. 假设积分和token的兑换比例为 1:1..也就是1积分兑换1个Token

如果没有EIP712 在用户点击 兑换按钮时 调用系统的API接口扣除账户的积分..同时调用合约领取对应的Token数量..这种方式会存在数据库和链上的数据不同步..同时其他人也可以直接调用合约暴露对外的方法直接领取Token.

  • 扣除积分之后用户关闭了页面或者刷新了页面..那么就会出现扣除了积分, 但是没有Token到账

当然可以通过服务器中的脚本..将扣除积分的用户利用系统脚本去触发合约将Token转到用户地址, 但是这种做法对与平台的GAS消耗是挺大的..每一笔的积分兑换Token..平台都需要去支付一笔gas费..但是为了数据安全..不得不这样处理

如今使用EIP712, 在系统中设置一个私钥..用户在兑换的时候..后台直接计算积分..并扣除..同时生成对应的签名返给前端..前端将参数调用合约接口与合约进行交互..
交互的流程图如下

image.png

后面的操作可以根据业务需求进行处理..比如生成签名之后, 用户没有确定钱包发起交易..系统可以利用定时器任务, 将没有交易哈希的签名对应的积分扣除数据进行回滚..毕竟这个操作不产生GAS费用.

实现

接下来就介绍一下怎么实现这个功能了
先直接看代码..然后再根据代码的实现..熟悉流程
● EIP712的代码浅析交互流程图

EIP712合约示例代码解析图.png
图比较大, 需要清晰图, 建议新窗口打开, 或者下载之后查看

准备私钥与钱包地址

● 利用钱包或者钱包加密算法获取一个私钥以及对应的钱包地址
● 私钥给到生成签名的脚本
● 公钥给合约..在合约拿到签名参数时..是可以知道由哪个钱包地址生成的签名..用来确保与合约交互的数据是通过指定服务器计算过的数据
准备合约
● 创建验签合约时需要确定作用域与需要参与签名的数据以及类型
● 将作用域保存在EIP712Domain结构体中 并用keccak256("结构体名+数据类型+名称") 计算出哈希值

    • 这里我们假定这个合约用来验签商城. 作用域名称"SHOP"
EIP712Domain{
  name: "SHOP",
  version: "1",
  chainId:97
}

//计算Domain作用域相关参数的哈希值比记录
DOMAIN_SEPARATOR = keccak256(
  abi.encode(
    keccak256("EIP712Domain(string name,string version,uint256 chainId)"),
    keccak256(bytes("SHOP")),
    keccak256(bytes("1")),
    97
  )
)

● 确定参与验签的数据 需要用户地址: 要将token转到目标地址, 数量:转移token的数量, 数据库中的订单id, 避免一个签名被多次使用
● 记录参与验签的数据结构哈希值

struct VerifyClaim {
  address userAddress;
  uint256 randNo;
  uint256 amount;
}
VERIFYCLAIM_TYPTHASH = keccak256("VerifyClaim(address userAddress,uint256 randNo,uint256 amount)"

脚本工作

● 引入 etherjs包
<script src="ether5.2umd.50ed9.js" charset="utf-8"></script>
● 确定验签的作用域, 根据合约格式

const domain = {
    name: "SHOP",
    version: "1",
    chainId: 97
}

● 整理与合约交互的参数格式 将合约中的结构体 整理成 结构体名[{参数名,参数类型...}]的格式

const types = {
  VerifyClaim: [
    {name: "userAddress", type: "address"},
    {name: "randNo", type: "uint256"},
    {name: "amount", type: "uint256"}
  ]
}

● 从系统中获取相关数据..计算出转移token数量, 订单id, 以及目标地址

let message = {
  userAddress: "0xE66Eb4D3845822568938664B06b676Dc2C33a6fB",
  randNo: "508091813966385152",
  amount: "10000"
}

● 使用ethers库连接网络并获取签名

//这里用币安测试链为例
var RPC_URL = "https://data-seed-prebsc-1-s1.binance.org:8545/"
var netWorkId = 97
var provider = new ethers.providers.StaticJsonRpcProvider(RPC_URL, netWorkId)
//var walletSigner = provider.getSigner()
this.signer = new ethers.Wallet("Private Key", provider)

//将作用域, 参数类型, 以及交互参数传入_signTypedData获取签名
const signResult = await this.signer._signTypedData(domain, types, message)

● 获取签名之后将签名切割..获得r, s, v参数

// signResult:0x7e7c845c626ea483f03890a2827236868a812777d7d9732954887aa95b8d015a66fb7c23527879704c5a622fee8f85adaf7c1b00057cceb2682064f04d2690e61c
//剪切0x
const signature = signResult.substring(2)
//因为r和s都是32字节的..所以前面64位是参数r
const r = "0x" + signature.substring(0, 64);
//接着后面64位是参数s
const s = "0x" + signature.substring(64, 128);
//最后2位是v. 再将16进制转10进制
const v = parseInt(signature.substring(128, 130), 16);
console.log("signature",message)
console.log("v",v)
console.log("s",s)
console.log("r",r)

合约验签

● 在合约中暴露一个对外接口

function test(
  address _userAddress,
  uint256 _randNo,
  uint 256 _amount,
  uint8 _v,
  bytes32 _r,
  bytes32 _s
)
  public
  view
  returns(bool)
{
  //根据合约中验签数据的结构体, 将参数整理成验签数据格式
  //计算参数哈希
  verifyclaimHash = keccak256(
    abi.encode(
      VERIFYCLAIM_TYPEHASH,
      _userAddress,
      _randNo,
      _amount
    )
  )
  
  bytes32 digest = keccak256(
    abi.encodePacked(
      "\x19\x01",
      DOMAIN_SEPARATOR,
      verifyclaimHash
    )
  )
  
  //利用内置函数ecrecover获取签名的钱包判断是否与指定钱包相等
  return ecrecover(digest, _v, _r, _s) == VERIFYADDRESS
}

相关文件

源码:

// file: EIP712DomainExample.sol
pragma solidity ^0.4.24;

contract Example {

    struct Person {
        string name;
        address wallet;
    }

    struct Mail {
        Person from;
        Person to;
        string contents;
    }

    bytes32 constant PERSON_TYPEHASH = keccak256(
        "Person(string name,address wallet)"
    );

    bytes32 constant MAIL_TYPEHASH = keccak256(
        "Mail(Person from,Person to,string contents)Person(string name,address wallet)"
    );
    
    struct EIP712Domain {
        string  name;
        string  version;
        uint256 chainId;
    }

    struct VerifyClaim{
        address userAddress;
        uint256 randNo;
        uint256 amount;
    }

    bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
        "EIP712Domain(string name,string version,uint256 chainId)"
    );

    bytes32 constant VERIFYCLAIM_TYPEHASH = keccak256(
        "VerifyClaim(address userAddress,uint256 randNo,uint256 amount)"
    );

    bytes32 DOMAIN_SEPARATOR;

    constructor () public {
        DOMAIN_SEPARATOR = hash(EIP712Domain({
            name: "VerifyClaim",
            version: '1',
            chainId: 97
        }));
    }

    function hash(Person person) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            PERSON_TYPEHASH,
            keccak256(bytes(person.name)),
            person.wallet
        ));
    }

    function hash(Mail mail) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            MAIL_TYPEHASH,
            hash(mail.from),
            hash(mail.to),
            keccak256(bytes(mail.contents))
        ));
    }

    function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            EIP712DOMAIN_TYPEHASH,
            keccak256(bytes(eip712Domain.name)),
            keccak256(bytes(eip712Domain.version)),
            eip712Domain.chainId
        ));
    }

    function hash(VerifyClaim verifyclaim) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            VERIFYCLAIM_TYPEHASH,
            verifyclaim.userAddress,
            verifyclaim.randNo,
            verifyclaim.amount
        ));
    }

    function verify(VerifyClaim verifyclaim, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {
        // Note: we need to use `encodePacked` here instead of `encode`.
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            hash(verifyclaim)
        ));
        return ecrecover(digest, v, r, s) == 0x53dE6A872435F5286BEFd0b6fB3bC06742aF8C8F;
    }
    
    function test(address _userAddress, uint256 _randNO, uint256 _amount, uint8 _v, bytes32 _r, bytes32 _s) public view returns (bool) {
        // Example signed message
        VerifyClaim memory verifyclaim = VerifyClaim({
            userAddress: _userAddress,
            randNo: _randNO,
            amount: _amount
        });
        assert(verify(verifyclaim, _v, _r, _s));
        return true;
    }
}

/*=======================================================================================================*/

//file: test.html
<body>
	<script src="ether5.2umd.50ed9.js" charset="utf-8"></script>
	<script>
		var RPC_URL = "https://data-seed-prebsc-1-s1.binance.org:8545/"
		var netWorkId = 97
		async function sign(){

		    const domain = {
		        name: 'DOOMSDAY',
		        version: '1',
		        chainId: 97,
		    }
		    const types = {
		        VerifyClaim: [
		            {name: 'userAddress', type: 'address'},
		            {name: 'randNo', type: 'uint256'},
		            {name: 'amount', type: 'uint256'}
		        ]
		    }
		    let _myAddress = "0xE66Eb4D3845822568938664B06b676Dc2C33a6fB"
		    let message = {userAddress:_myAddress,randNo:"508091813966385152",amount:"100"}
		    var provider = new ethers.providers.StaticJsonRpcProvider(RPC_URL, netWorkId);
		    var walletSigner = provider.getSigner();
		    this.signer = new ethers.Wallet(provider)
			
			console.log(this.signer);

		    const signResult = await this.signer._signTypedData(domain, types, message)
			console.log(signResult);
		    const signature = signResult.substring(2);
		    const r = "0x" + signature.substring(0, 64);
		    const s = "0x" + signature.substring(64, 128);
		    const v = parseInt(signature.substring(128, 130), 16);
		    console.log("signature",message)
		    console.log("v",v)
		    console.log("s",s)
		    console.log("r",r)
		}
		setTimeout(sign,1000)
	</script>
</body>



点赞 5
收藏 9
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
黄金叶
黄金叶
0xf8b2...537f
江湖只有他的大名,没有他的介绍。