EOA 可以用私鑰對訊息進行簽章,合約可以在鏈上對訊息雜湊後,配上簽章,導出 EOA 的地址,合約驗證是否為 EOA 本人的意圖,EOA 就不需要親自執行交易,透過提供簽章,他人代送交易的方式,得以進行鏈上活動。
Signing:
Verify:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import "forge-std/Test.sol";
library SignatureUtils {
function splitSignature(bytes memory signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
require(signature.length == 65, "Wrong length");
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
}
}
contract A {
address owner;
constructor(address _owner) {
owner = _owner;
}
function isValidSignatureFromEOA(bytes32 digest, bytes memory signature) public view returns (bool) {
(uint8 v, bytes32 r, bytes32 s) = SignatureUtils.splitSignature(signature);
address signer = ecrecover(digest, v, r, s);
if (signer == owner) return true;
return false;
}
}
contract ATest is Test {
address alice = vm.addr(0xbeef);
A a = new A(alice);
function testSign() public {
bytes memory message = "hello world";
(uint8 v, bytes32 r, bytes32 s) = vm.sign(0xbeef, keccak256(message));
bytes memory signature = abi.encodePacked(r, s, v);
console.logBytes(signature);
}
function testIsValidSignatureFromEOA() public {
bytes memory message = "hello world";
(uint8 v, bytes32 r, bytes32 s) = vm.sign(0xbeef, keccak256(message));
bytes memory signature = abi.encodePacked(r, s, v);
console.logBytes(signature);
console.logBytes32(r);
console.logBytes32(s);
console.log(v);
bool valid = a.isValidSignatureFromEOA(keccak256(message), signature);
assertEq(valid, true);
}
}
Signature 的長度為 65 個字元,組合的順序是 r,s,v。
0x9951afc45afbf8c6bbd2469649cedcfa6d8b039d50a5bb8bfe216c4338e0cbb2008d3e5243755a08bd89f269e31a71e7b7ebfe9a0abb367422b658bd8c9c4e871c
r: 32 bytes
0x9951afc45afbf8c6bbd2469649cedcfa6d8b039d50a5bb8bfe216c4338e0cbb2
s: 32 bytes
0x008d3e5243755a08bd89f269e31a71e7b7ebfe9a0abb367422b658bd8c9c4e87
v: 1 bytes
0x1c
鏈上 recover 時要填入雜湊後的訊息和拆開的簽章,參數的順序是 v,r,s。
ecrecover (elliptic curve recover):
ecrecover(digest, v, r, s);
openzeppelin:
ECDSA.recover(hash, v, r, s);
function splitSignature(bytes memory signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
require(signature.length == 65, "Wrong length");
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
}
因為 signature 的型別是 bytes memory,因此前 32 bytes 是陣列的長度,所以 signature 的起始位置要加 32 bytes,才是 r 的 offset,接著 32 bytes 就是 s。
最後 v 因為只有 1 byte,使用 BYTE 從 32 bytes 中提取 1 byte。
message
\x19Ethereum Signed Message:\n + message.length + message
message
"\x19\x01" + domain_separator + keccak256(abi.encode(TYPEHASH, ...args))
TYPEHASH example:
bytes32 private constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
ERC20Permit.sol
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, v, r, s);
if (signer != owner) {
revert ERC2612InvalidSigner(signer, owner);
}
_approve(owner, spender, value);
}
Smart Contract Security - Signature Related
ecrecover
會回傳 address(0) 且不會 revert。安全作法: