https://eips.ethereum.org/EIPS/eip-712

EIP-712 는 트랜젝션 데이터가 아닌 모든 데이터를 어떤 구조로 서명할 지에 대해 정의한 제안이며 사용자에게 서명할 메세지를 사람이 읽을 수 있는 구조로 보여주고, 그 구조에 따라 서명하여 신뢰성과 보안성을 높이는데 초점을 둡니다.
TYPE_HASH 는 기능이름(parameters) 로 된 문자열을 keccak256 으로 해싱 한 값입니다.
EIP712Domain 에 대한 TYPE_HASH 는 다음과 같습니다.
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
어떤 네트워크의 어떤 컨트랙트에서 사용되는(검증되는) 서명인지 구분하기 위한 구분자.
DomainSeperator 의 구성은 다음과 같습니다.
string name : 컨트랙트(DApp 혹은 Protocol)의 이름string version : 컨트랙트 버전uint256 chainId : 컨트랙트가 배포된 네트워크 아이디address verifyingContract : 서명을 검증하는 컨트랙트의 주소(배포된 컨트랙트 주소)Openzeppelin 에서 DomainSeperator를 생성하는 코드는 다음과 같습니다. (EIP712.sol#L90)
function _buildDomainSeparator() private view returns (bytes32) {
return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));
}
Parameter 로 전달되는 인자의 type을 정의한 정의서. encodeType을 통해 해당 함수에 대한 TYPE_HASH 가 생성됩니다.
예시
const type = {
transfer: [
{name:"to", type:"address"},
{name:"amount", type:"uint256"},
]
};
Parameter 로 전달되는 인자의 값
예시
const data = {
to : wallet.address,
amount : 1000000000000000000n
};
signer는 private key 를 사용하여 메세지를 서명합니다.
서명된 메세지는 다음 데이터들로부터 생성됩니다.
서명된 메세지(signature)는 v, r, s 로 분리됩니다.
함수 호출자는 서명된 데이터(v,r,s) 와 arguments 를 함께 전달합니다.
함수에서는 전달받는 arguments 와 v,r,s 값으로 ecrecover 함수를 통해 해당 서명을 검증합니다.
a. 서명에 사용된 값과 arguments 값이 일치하면 ecrecover 는 서명자의 주소를 반환합니다.
b. 서명에 사용된 값과 arguments 값이 일치하지 않을경우 ecrecover 는 엉뚱한 주소를 반환합니다.
예시
1. owner 가 to:0x1234, amount:100 값에 대해 서명하여 Signature 생성
2. 함수호출자가 to:0x1234, amount:9000000 라는 값을 Owner 의 Signature 와 함께 호출
-> 검증 과정에서 ecrecover 가 엉뚱한 주소를 반환하여 검증 실패
1. owner 가 to:0x1234, amount:100 값에 대해 서명하여 Signature 생성
2. 함수호출자가 to:0x1234, amount:100 라는 값을 Owner 의 Signature 와 함께 호출
-> 검증 과정에서 ecrecover 가 owner의 주소를 반환하여 검증 성공
컨트랙트 코드를 함께 보며 이해도를 높이면 좋을것 같습니다.
다음은 설명을 위한 컨트랙트의 함수 코드입니다.
// mintWithApproval() 은 owner의 private key 로 sign 된 메세지(signature)가 정상일 때 민팅을 할 수 있는 함수입니다.
function mintWithApproval(
address to,
uint256 amount,
uint8 v,
bytes32 r,
bytes32 s
) external {
// 함수의 TYPE_HASH 와 전달받은 arguments 로 structHash 를 생성합니다.
bytes32 structHash = keccak256(
abi.encode(
keccak256("mintWithApproval(address to,uint256 amount)"), // TYPE_HASH
to, // arg1
amount // arg2
)
);
bytes32 hash = _hashTypedDataV4(structHash);
// 서명된 데이터(v,r,s) 에 사용된 데이터와 arguments(to, amount) 값이 같다면 ecrecover 는 서명한 지갑의 주소를 반환합니다.
address signer = ecrecover(hash, v, r, s);
if (signer != owner) {
revert("Invalid signature");
}
_mint(to, amount);
}
EIP-712 를 따르는 서명을 만드는 방법은 다양하겠지만 이 글에서는 ehters.js 라이브러리로 서명을 생성하는 방법만 다뤄보겠습니다.
domainSeperator 생성을 위한 도메인을 지정해줍니다.
const domain = {
name: "Test",
version: "1",
chainId: 1,
verifyingContract: "0x1234contractAddress"
};
parameter 로 전달할 인자들의 타입을 지정해줍니다.
const types = {
mintWithApproval:[
{name: "to", type: "address"},
{name: "amount", type: "uint256"},
]
};
parameter 로 전달할 인자들의 값을 지정해줍니다.
const data = {
to: "0x9876walletAddress",
amount: 1000000000000000000n
};
ethers 의 Wallet instance 로 서명을 생성합니다.
const owner = ethers.Wallet.createRandom();
const signature = owner.signTypedData(domain, types, data);
const {v, r, s} = ethers.Signature.from(signature);