こちらのfoundry チュートリアル(EIP-2612のテスト)をやってみた結果と気づきのメモです。
https://book.getfoundry.sh/tutorials/testing-eip712
// はじめの理解は不足していたので追記しました。
ERC-20ではapprove(spender, aomunt)
で defiなどのcontractに対してtransferfrom(sender,recipient,amount)
を直接ユーザが許可するのだけど、ERC2612では以下をユーザに渡して署名してもらいます。
"message": {
"owner": owner,
"spender": spender,
"value": value,
"nonce": nonce,
"deadline": deadline
}
これの何がうれしいのかというと、この署名(v,r.s)があると、permitをcontract側ができるので、ユーザはapproveにかかるガス代がかからないそうです。
ERC-20のコイン作成 (Orecoin)
ERC-20も作った事なかったのですが、チュートリアルに無いのでERC20.sol
をみながら作成。
ERC20を定義しているライブラリはいくつかあるらしく、チュートリアルのとおりsolmateのERC20を使ってます。(ERC-2612用の定義があります)
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
import "lib/solmate/src/tokens/ERC20.sol";
contract Orecoin is ERC20 {
constructor() ERC20("Orecoin","ORC", 100) {
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
-
ERC20
のconstructor
の第三引数は基準単位と最小単位の桁数。ether と weiの関係。wethの場合は18 -
ERC20
はabstract
になっていて継承して使うようになってる。 - ミントは
_mint()
でinternal
になっているので、これだけpublic から呼べるように実装。他のERC20関数はERC20.solで実装済み。
ERC-20テストの作成
チュートリアルだとないけど、ERC-20の正常性テストのミントと転送だけやってみた。
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import "../src/orecoin.sol";
import "../src/sigutil.sol";
contract OreCoinTest is Test {
Orecoin public token;
function setUp() public {
token = new Orecoin();
}
// basic ERC20 test
function test_mint_Increment() public {
token.mint(address(1), 100);
assertEq(token.balanceOf(address(1)), 100);
}
function test_transfer() public {
token.mint(address(1), 100);
vm.prank(address(1));
token.transfer(address(2), 10);
assertEq(token.balanceOf(address(2)), 10);
assertEq(token.balanceOf(address(1)), 90);
}
ERC-2612のテスト
まずオフチェーンサインをするデータ部作成とハッシュ作成のcontractを src/sigutil.sol
にコピペ。これはfoundryのテストでユーザが署名する部分の関数です。foundryはすべてsolidiryで記述するのでこのようになります。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract SigUtils {
bytes32 internal DOMAIN_SEPARATOR;
constructor(bytes32 _DOMAIN_SEPARATOR) {
DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR;
}
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH =
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}
// computes the hash of a permit
function getStructHash(Permit memory _permit)
internal
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
PERMIT_TYPEHASH,
_permit.owner,
_permit.spender,
_permit.value,
_permit.nonce,
_permit.deadline
)
);
}
// computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer
function getTypedDataHash(Permit memory _permit)
public
view
returns (bytes32)
{
return
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
getStructHash(_permit)
)
);
}
}
次に./test/orecoin.t.sol
を書き換え。 setUp()
やglobal変数をチュートリアルの通りに変えてみた。
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import "../src/orecoin.sol";
import "../src/sigutil.sol";
contract OreCoinTest is Test {
Orecoin public token;
SigUtils public sigUtils;
uint256 internal ownerPrivateKey;
uint256 internal spenderPrivateKey;
address internal owner;
address internal spender;
function setUp() public {
token = new Orecoin();
sigUtils = new SigUtils(token.DOMAIN_SEPARATOR());
ownerPrivateKey = 0xA11CE;
spenderPrivateKey = 0xB0B;
owner = vm.addr(ownerPrivateKey);
spender = vm.addr(spenderPrivateKey);
token.mint(owner, 1e18);
}
次に正常性のテスト。permit
とTransferFrom
をまとめてみた。
// ERC2612 test
function test_Permit_and_transfer() public {
// permitのデータ構造をつくる。
SigUtils.Permit memory permitData = SigUtils.Permit({
owner: owner,
spender: spender,
value: 1e18,
nonce: 0,
deadline: 1 days
});
// ここでサービスプロバイダはユーザに上記のpermit情報を渡す
bytes32 digest = sigUtils.getTypedDataHash(permitData);
// ユーザは自分の秘密鍵でpermit情報にサイン。これはオフチェーンでできる。
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
// サービスプロバイダはpermitをownerの代わりに実行
vm.prank(spender);
token.permit(
permitData.owner,
permitData.spender,
permitData.value,
permitData.deadline,
v,
r,
s
);
assertEq(token.allowance(owner, spender), 1e18);
assertEq(token.nonces(owner), 1);
// サービスプロバイダがtransferFromを実行
vm.prank(spender);
token.transferFrom(owner, spender, 1e18);
assertEq(token.balanceOf(owner), 0);
assertEq(token.balanceOf(spender), 1e18);
assertEq(token.allowance(owner, spender), 0);
// トランザクションはtransferFromしても増えない
assertEq(token.nonces(owner), 1);
}
}
-
自分の理解したところを日本語コメントいれてみた。勘違いがあるかもしれない。
-
チュートリアルでは
token.permit
とSigUtil
のpermit
が紛らわしかったのでSigUtil
で作る方をpermitData
としました。 -
nonce
ってなにかわからなかったけど、そのユーザのトランザクションの累計なんですね。テストでははじめは0から始まるので、permit
には +1された1 がはいる。トランザクションにこの値を入れることによってリプレイ攻撃を防ぐ。
あとはネガティブテストをチュートリアルのとおりにやってみた。