seccamp2022L4: Uniswap ②
https://gyazo.com/a5b6ee29732439e85e200324195a4437
https://uniswap.org/
前回: seccamp2022L4: Uniswap ①
今回やること
UNIトークンのコードリーディング
(Uniswapのコアコントラクトは次回に)
Etherscanにアップロードされているコードを見ていく
コード: https://etherscan.io/address/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984#code
本当にコードをコンパイルしたものかどうかはEtherscanによって検証されている。
pragma
pragma solidity ^0.5.16;
コンパイラに対するSolidityのバージョン指定。
0.5.16以上かつ0.5.x。低い。
pragma experimental ABIEncoderV2;
Solidityの変数や構造体をバイナリにエンコードする方法はABIエンコーディングと呼ばれている。
そのバージョン2を指定。バージョン2になって多くの型をサポートするようになるなど。
experimentalとあるように0.5.16では実験的な機能だが、0.8.0で標準化された。
SafeMathライブラリ
四則演算の際に算術オーバーフロー/アンダーフローをチェックしてくれる。
Solidity 0.8.0でチェックがデフォルトになったから要らない子に。
例: add()
code:sol(js)
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
require文でやっている。
他にsub,mul,div,mod。
ERC-20の要件とオプション
code:ERC-20の要件(js)
function totalSupply() constant returns (uint totalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns (bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
OpenZeppelinのERC-20インターフェースは上記要件だけ。
code:ERC-20のオプション(js)
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
Uniコントラクト: ERC-20オプション部
string public constant name = "Uniswap";
トークン名。
string public constant symbol = "UNI";
シンボル。通貨単位。100 UNIのように使われる部分。
uint8 public constant decimals = 18;
トークンの量は正の整数(厳密にはuint)でなくてはならない。
ちなみにEVMのオペコードレベルで浮動小数点数はない。誤差が出たら大変。EVM上で動く有志のライブラリはある。
小数点以下を表現したいトークンの場合このdecimalsの値を増やす。
18であれば小数点以下18桁まで表現できるようになる。
name,symbol,decimalsともにユーザーインターフェースや他のコントラクトが利用する部分で、ERC-20トークンの実装中にこの変数が使われることはほぼない。ただしnameは後述するEIP-712で用いる。
Uniコントラクト: ERC-20の要件totalSupply関数
uint public totalSupply = 1_000_000_000e18; // 1 billion Uni
decimalsが18だからイメージとしては1_000_000_000.000_000_000_000_000_000ということ。
要件では関数だったが、Solidityコンパイラはpublic変数からゲッター関数を自動的に生成するため問題ない。
OpenZeppelinのERC-20実装サンプルはprivateの_totalSupplyを用意してゲッター関数totalSupplyを自作している。
code:sol(js)
uint256 private _totalSupply;
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
view/pure
view: ステートを変更しない(書き込みしない)ことを表す。
pure: ステートを読み込みもできない。
view/pureとpublic/external/internal/privateを適切に設定するとガス代の節約になる。
virtual,overrideはSolidity 0.6.0から。
virtualは関数をオーバーライドしても良いことを表す。
overrideは関数をオーバーライドしていることを表す。このコードの場合、IERC-20からオーバーライドしているということ。Solidity 0.8.8からはインターフェースからオーバーライドするときoverrideを書く必要はなくなった。
totalSupply()が返せるならなんでもいい。リベーストークンはこのtotalSupply()が変動する。
Uniコントラクト: ERC-20の要件balanceOf関数
code:sol(js)
mapping (address => uint96) internal balances;
function balanceOf(address account) external view returns (uint) {
return balancesaccount;
}
internal: このコントラクトと派生コントラクトが利用できる。外部コントラクトは利用できない。
Uniコントラクト: ERC-20の要件transfer関数
code:transfer()(js)
function transfer(address dst, uint rawAmount) external returns (bool) {
uint96 amount = safe96(rawAmount, "Uni::transfer: amount exceeds 96 bits");
_transferTokens(msg.sender, dst, amount);
return true;
}
rawAmountが96ビットを超える値($ 2^{96})でないかをsafe96()で確認して_transferTokens()で送金している。
code:safe96()(js)
function safe96(uint n, string memory errorMessage) internal pure returns (uint96) {
require(n < 2**96, errorMessage);
return uint96(n);
}
safe96()はrequireでチェックしてuint96にキャストしてるだけ。ステートを読まないからpure。
code:_transferTokens()(js)
function _transferTokens(address src, address dst, uint96 amount) internal {
require(src != address(0), "Uni::_transferTokens: cannot transfer from the zero address");
require(dst != address(0), "Uni::_transferTokens: cannot transfer to the zero address");
balancessrc = sub96(balancessrc, amount, "Uni::_transferTokens: transfer amount exceeds balance");
balancesdst = add96(balancesdst, amount, "Uni::_transferTokens: transfer amount overflows");
emit Transfer(src, dst, amount);
_moveDelegates(delegatessrc, delegatesdst, amount);
}
_transferTokens()はsrcからdstにamountを送る関数。後述するtransferFrom()でも呼ばれる。
srcとdstがゼロアドレスだったらエラー。
add96()とsub96()を使って安全にsrcとdstの残高を変更。
code:add96(),sub96()(js)
function add96(uint96 a, uint96 b, string memory errorMessage) internal pure returns (uint96) {
uint96 c = a + b;
require(c >= a, errorMessage);
return c;
}
function sub96(uint96 a, uint96 b, string memory errorMessage) internal pure returns (uint96) {
require(b <= a, errorMessage);
return a - b;
}
Transferイベントを発行(後述)して、_moveDelegates()を実行(後述)。
Uniコントラクト: ERC-20の要件Transferイベント
event Transfer(address indexed from, address indexed to, uint256 amount);
fromからtoにamount送ったというイベント。
emit Transfer(src, dst, amount);のようにイベントが呼ばれると「ログ」がノードに保存される。
トランザクションレシート(トランザクション実行結果のための構造体)に保存される。
https://gyazo.com/1f996e6eccdfc72f53a892cef0252a73
引用: https://ethereum.stackexchange.com/questions/268/ethereum-block-architecture/273
ログはコントラクトからはアクセスできないためコントラクトの関数で利用するといったことはできない。
しかしストレージと異なり安価で、オフチェーンの我々が検索や調査を行うために用いるには適している。
例えば、誰が誰にいくら送金したかという情報データベースがあって、Aさんが関わる送受金をフィルタしたいようなケースでは、コントラクトからそのデータを利用することはないから、Transferイベントを発行してログを検索すれば良い。ブロックチェーン上の高価なストレージに保存する必要はない。
現在は全てのログが永続的に保存されているが将来的にはどうなるかはわからない。
イベントの引数は通常ABIエンコードされて一つのデータにまとめられるが、indexedをつけるとトピックと呼ばれる特別なデータ構造に保存され、高速な検索ができる。
(イベント名と引数の型,引数ごちゃまぜデータ)より(イベント名と引数の型,引数1,引数2,引数3)のほうが扱いやすいよね、ということ。
indexedは最大3つまでつけれる。
Uniコントラクト: ERC-20の要件approve関数とallowance関数
code:sol(js)
mapping (address => mapping (address => uint96)) internal allowances;
function allowance(address account, address spender) external view returns (uint) {
return allowancesaccountspender;
}
allowanceというaddressからuint96へのマッピングとそのゲッター関数。allowanceはapprove関数で使う。
code:sol(js)
function approve(address spender, uint rawAmount) external returns (bool) {
uint96 amount;
if (rawAmount == uint(-1)) {
amount = uint96(-1);
} else {
amount = safe96(rawAmount, "Uni::approve: amount exceeds 96 bits");
}
allowancesmsg.senderspender = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
approve関数はallowanceのセッター関数。
何に使うのか?
Aliceが一般的なDEXで1000 UNIをWETHにスワップしたいとする。
まずAliceは何をしたらよいだろうか?UNIトークンコントラクトのtransfer関数を使ってUNIをDEXに送ればよいだろうか?transfer関数を実行したあとDEXのスワップ関数を実行するのだろうか?
答えはNOで、もしtranfer関数を実行してDEXにUNIを送ったら、そのUNIは二度と戻ってこない。
transfer関数がDEXのスワップ関数をトリガーできればいいがERC-20にはその機能はない。
またDEXがUNIの残高が増えたことを検知できる機能があればいいが、Transferイベントでも話したように送金履歴はログにしか残らないため、そういった機能を作るのは難しい。例えば、DEX自体に前の残高を記録して差分を証拠にするということもできるがガスコストが高く非効率。
正しいやり方は、まずapprove関数を使ってDEXに1000 UNIの操作権利を渡し、次にDEXのスワップ関数を呼ぶ。スワップ関数の内部でtransferFrom関数(後述)が使われ、1000 UNIがAliceからプールに送られ、代わりにプールからAliceにWETHが送られる。
rawAmountがuint(-1)である場合はInfinite Approvalと言い、トークンの全残高を操作できるようになる。
最後にApprovalイベントが発火される。
Uniコントラクト: ERC-20の要件transferFrom関数
code:transferFrom()(js)
function transferFrom(address src, address dst, uint rawAmount) external returns (bool) {
address spender = msg.sender;
uint96 spenderAllowance = allowancessrcspender;
uint96 amount = safe96(rawAmount, "Uni::approve: amount exceeds 96 bits");
if (spender != src && spenderAllowance != uint96(-1)) {
uint96 newAllowance = sub96(spenderAllowance, amount, "Uni::transferFrom: transfer amount exceeds spender allowance");
allowancessrcspender = newAllowance;
emit Approval(src, spender, newAllowance);
}
_transferTokens(src, dst, amount);
return true;
}
srcからdstにrawAmount送る。
ただし、spender = msg.senderとして、spenderがsrcのトークンをrawAmount分操作できるかチェックする。
操作した分をallowances[src][spender]から引く。
Approvalイベントを発火させてallowancesに更新があったことをログして、_transferTokens関数で実際にトークンを移動させる。
Uniコントラクト: ERC-20の要件Approvalイベント
event Approval(address indexed owner, address indexed spender, uint256 amount);
ownerのトークンがamountだけspenderが操作できるようになったよ、というイベント
Uniコントラクト: コンストラクタ
コントラクト作成時に実行されるもの。
code:constructor()(js)
constructor(address account, address minter_, uint mintingAllowedAfter_) public {
require(mintingAllowedAfter_ >= block.timestamp, "Uni::constructor: minting can only begin after deployment");
balancesaccount = uint96(totalSupply);
emit Transfer(address(0), account, totalSupply);
minter = minter_;
emit MinterChanged(address(0), minter);
mintingAllowedAfter = mintingAllowedAfter_;
}
最初accountがtotalSupply分のUNIを所持する。
そのあとミント関連の操作が行われており、アドレス変数minterとuintの変数mintingAllowedAfter_が設定されている。
Uniコントラクト: ミント
Uniコントラクトにはミント機能があり新しくUNIを発行することが可能。
code:mint()(js)
function mint(address dst, uint rawAmount) external {
require(msg.sender == minter, "Uni::mint: only the minter can mint");
require(block.timestamp >= mintingAllowedAfter, "Uni::mint: minting not allowed yet");
require(dst != address(0), "Uni::mint: cannot transfer to the zero address");
// record the mint
mintingAllowedAfter = SafeMath.add(block.timestamp, minimumTimeBetweenMints);
// mint the amount
uint96 amount = safe96(rawAmount, "Uni::mint: amount exceeds 96 bits");
require(amount <= SafeMath.div(SafeMath.mul(totalSupply, mintCap), 100), "Uni::mint: exceeded mint cap");
totalSupply = safe96(SafeMath.add(totalSupply, amount), "Uni::mint: totalSupply exceeds 96 bits");
// transfer the amount to the recipient
balancesdst = add96(balancesdst, amount, "Uni::mint: transfer amount overflows");
emit Transfer(address(0), dst, amount);
// move delegates
_moveDelegates(address(0), delegatesdst, amount);
}
最初にmsg.senderがミントする権利を持つminterかどうかを確認。
また、mintingAllowedAfterまではミントするのは駄目だとわかる。
そして一度ミントが実行されるとこのmintingAllowedAfterが更新される。uint32 public constant minimumTimeBetweenMints = 1 days * 365;であるから次のミントが可能になるのは1年後であり、この変数はconstantで変更できない。
また一度にミントできる総量が決まっている。uint8 public constant mintCap = 2;であるから0.02倍以上にできない。これもconstant。
そして新しく発行したUNIはdstに送られる。
Uniコントラクト: ガバナンス
UNIはUniswapのガバナンストークンであり、UNIトークン自体にガバナンス機能の一部が載っている。
先程から出現している_moveDelegates関数がその一つ。
投票権の委譲をUniコントラクトで管理している。
mapping (address => address) public delegates;が誰が誰に投票権を委譲しているかを表すマッピング。
Uniコントラクト: delegate関数と_delegate関数
code:delegate()(js)
function delegate(address delegatee) public {
return _delegate(msg.sender, delegatee);
}
msg.senderからdelegateeに投票権を委譲する。
code:_delegate()(js)
function _delegate(address delegator, address delegatee) internal {
address currentDelegate = delegatesdelegator;
uint96 delegatorBalance = balancesdelegator;
delegatesdelegator = delegatee;
emit DelegateChanged(delegator, currentDelegate, delegatee);
_moveDelegates(currentDelegate, delegatee, delegatorBalance);
}
delegatorからdelegateeに投票権を委譲する。
delegatesを更新して、DelegateChangedイベントを発生させて、最後に_moveDelegates関数を実行している。
_moveDelegatesは今までの委譲先アドレスから新しい委譲先アドレスにdelegatorBalance分のトークンの投票権利が移ったことを処理する。
Uniコントラクト: チェックポイント
code:sol(js)
struct Checkpoint {
uint32 fromBlock;
uint96 votes;
}
誰がという情報を持たないが、(誰が)どこのブロック(fromBlock)からどれだけの投票権利(votes)を得たかを記録するための構造体。
code:sol
mapping (address => mapping (uint32 => Checkpoint)) public checkpoints;
mapping (address => uint32) public numCheckpoints;
checkpointsマッピングは誰の投票権がどう変化してきたかを記録する。
numCheckpointsは誰の投票券が何回変化したかを記録する。同じブロックでの変化は1回とカウントされる。
Uniコントラクト: _moveDelegates関数と_writeCheckpoint関数
チェックポイントを更新する関数。
長いがやっていることはやっていることは簡単で、委譲先の変化に伴うチェックポイントの更新だけ。
code:_moveDelegates()(js)
function _moveDelegates(address srcRep, address dstRep, uint96 amount) internal {
if (srcRep != dstRep && amount > 0) {
if (srcRep != address(0)) {
uint32 srcRepNum = numCheckpointssrcRep;
uint96 srcRepOld = srcRepNum > 0 ? checkpointssrcRepsrcRepNum - 1.votes : 0;
uint96 srcRepNew = sub96(srcRepOld, amount, "Uni::_moveVotes: vote amount underflows");
_writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
}
if (dstRep != address(0)) {
uint32 dstRepNum = numCheckpointsdstRep;
uint96 dstRepOld = dstRepNum > 0 ? checkpointsdstRepdstRepNum - 1.votes : 0;
uint96 dstRepNew = add96(dstRepOld, amount, "Uni::_moveVotes: vote amount overflows");
_writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
}
}
}
code:_writeCheckpoint()(js)
function _writeCheckpoint(address delegatee, uint32 nCheckpoints, uint96 oldVotes, uint96 newVotes) internal {
uint32 blockNumber = safe32(block.number, "Uni::_writeCheckpoint: block number exceeds 32 bits");
if (nCheckpoints > 0 && checkpointsdelegateenCheckpoints - 1.fromBlock == blockNumber) {
checkpointsdelegateenCheckpoints - 1.votes = newVotes;
} else {
checkpointsdelegateenCheckpoints = Checkpoint(blockNumber, newVotes);
numCheckpointsdelegatee = nCheckpoints + 1;
}
emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
}
Uniコントラクト: getCurrentVotes関数
code:getCurrentVotes()(js)
function getCurrentVotes(address account) external view returns (uint96) {
uint32 nCheckpoints = numCheckpointsaccount;
return nCheckpoints > 0 ? checkpointsaccountnCheckpoints - 1.votes : 0;
}
アカウントが現在持っている投票権を取得。
Uniコントラクト: getPriorVotes関数
あるアカウントがblockNumberのときどれだけの投票権を持っていたか調べる。
code:getPriorVotes()(js)
function getPriorVotes(address account, uint blockNumber) public view returns (uint96) {
require(blockNumber < block.number, "Uni::getPriorVotes: not yet determined");
uint32 nCheckpoints = numCheckpointsaccount;
if (nCheckpoints == 0) {
return 0;
}
// First check most recent balance
if (checkpointsaccountnCheckpoints - 1.fromBlock <= blockNumber) {
return checkpointsaccountnCheckpoints - 1.votes;
}
// Next check implicit zero balance
if (checkpointsaccount0.fromBlock > blockNumber) {
return 0;
}
uint32 lower = 0;
uint32 upper = nCheckpoints - 1;
while (upper > lower) {
uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow
Checkpoint memory cp = checkpointsaccountcenter;
if (cp.fromBlock == blockNumber) {
return cp.votes;
} else if (cp.fromBlock < blockNumber) {
lower = center;
} else {
upper = center - 1;
}
}
return checkpointsaccountlower.votes;
}
後半は二分探索を使ってそのアカウントのチェックポイント数を$ nとして$ O(\log n)で計算している。
checkpoints[account]はblockNumberからのマッピングではないから直接調べられない。
なぜ直接マッピングしないのか?というと毎ブロック記録しているわけではないから。
Uniコントラクト: delegateBySig関数
署名(v,r,s)をしたアカウントから委譲先delegateeへ投票権を委譲する。
code:delegateBySig()(js)
function delegateBySig(address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s) public {
bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "Uni::delegateBySig: invalid signature");
require(nonce == noncessignatory++, "Uni::delegateBySig: invalid nonce");
require(now <= expiry, "Uni::delegateBySig: signature expired");
return _delegate(signatory, delegatee);
}
Uniコントラクト: permit関数
approve面倒問題の解決策としてEIP-712で提案された。
署名を使ってメタトランザクションをすることで次の2つのメリットが生まれる。
ERC-20トークンの操作にERC-20トークンの支払いだけでよくなる。
従来はトランザクションのガス代としてETHを払っていた。
トランザクション発行者を委譲するため委譲元であるユーザーはERC-20トークンを払ってもよい。
approve()とtransferFrom()の2つの連続したトランザクションだったが、1つのトランザクションで可能に。
code:permit()(js)
function permit(address owner, address spender, uint rawAmount, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
uint96 amount;
if (rawAmount == uint(-1)) {
amount = uint96(-1);
} else {
amount = safe96(rawAmount, "Uni::permit: amount exceeds 96 bits");
}
bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, rawAmount, noncesowner++, deadline));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "Uni::permit: invalid signature");
require(signatory == owner, "Uni::permit: unauthorized");
require(now <= deadline, "Uni::permit: signature expired");
allowancesownerspender = amount;
emit Approval(owner, spender, amount);
}