uniswap v2 erc20合約中有一個預授權功能,也就是鏈下簽名鏈上驗證,授權方法如下:
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
里面在對簽名數據進行編碼時用到了abi.encode/abi.encodePacked兩種方法,查閱solitidy文檔得知兩種方法區別在於:abi.encode 編碼的數據需要32字節對齊,abi.encodePacked對小於32字節的數據不補0,以及其他一些區別:https://solidity.readthedocs.io/en/v0.7.3/abi-spec.html#abi-packed-mode
使用golang實現如下
// hash of packed byte array with arguments
hash := crypto.Keccak256Hash(
common.HexToAddress("0x0000000000000000000000000000000000000000").Bytes(),
[32]byte{'I','D','1'},
common.LeftPadBytes(big.NewInt(42).Bytes(), 32),// 不足32字節的補齊
[]byte("Some other string value"),//最后的參數不需要補齊
)
// normally we sign prefixed hash
// as in solidity with `ECDSA.toEthSignedMessageHash`
prefixedHash := crypto.Keccak256Hash(
[]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%v", len(hash))),//不足32字節 不補齊
hash.Bytes(),
)
最后,使用golang寫了一個UNISWAP合約中相關計算的例子。
const (
// keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
EIP712DomainHash = "0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f"
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
PERMIT_TYPEHASH = "0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9"
)
func calcDigest(verifyingContract, owner, spender ethcmn.Address, value *big.Int, nonce, deadline int64) ethcmn.Hash {
return crypto.Keccak256Hash(
[]byte("\x19\x01"),
calcDomainSeparatorHash(verifyingContract).Bytes(),
calcDigestPermitFuncHash(owner, spender, value, nonce, deadline).Bytes(),
)
}
func TestCalcDigest(t *testing.T) {
digest := calcDigest(ethcmn.HexToAddress("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"),
ethcmn.HexToAddress("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"),
ethcmn.HexToAddress("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"),
big.NewInt(123),
1,
1600000000)
fmt.Println(digest.Hex())
}
func calcDomainSeparatorHash(verifyingContract ethcmn.Address) ethcmn.Hash {
nameHash := crypto.Keccak256Hash([]byte("Uniswap V2")) //0xbfcc8ef98ffbf7b6c3fec7bf5185b566b9863e35a9d83acd49ad6824b5969738
versionHash := crypto.Keccak256Hash([]byte("1")) //0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6
chainId := big.NewInt(1).Bytes()
return crypto.Keccak256Hash(
ethcmn.HexToHash(EIP712DomainHash).Bytes(),
nameHash.Bytes(),
versionHash.Bytes(),
ethcmn.LeftPadBytes(chainId, 32),
ethcmn.LeftPadBytes(verifyingContract.Bytes(), 32),
)
}
func calcDigestPermitFuncHash(owner, spender ethcmn.Address, value *big.Int, nonce int64, deadline int64) ethcmn.Hash {
return crypto.Keccak256Hash(
ethcmn.HexToHash(PERMIT_TYPEHASH).Bytes(),
ethcmn.LeftPadBytes(owner.Bytes(), 32),
ethcmn.LeftPadBytes(spender.Bytes(), 32),
ethcmn.LeftPadBytes(value.Bytes(), 32),
ethcmn.LeftPadBytes(big.NewInt(nonce).Bytes(), 32),
ethcmn.LeftPadBytes(big.NewInt(deadline).Bytes(), 32),
)
}