# BBB:Putty contest ## [H-01]プット実行時ではなく、プット失効時に手数料が差し引かれる ### Category ### Conditions ` (!order.isCall && !isExercised))`の際も手数料が差し引かれる ### Hacking details ```solidity if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); ``` ```solidity ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike); ``` PuttyV2.solL#495-L503 の関数 withdraw() では、Put が実行されず、失効した場合でも手数料が差し引かれています。 ### How to Fix PuttyV2.sol#L498のif条件を`(fee > 0 && order.isCall && isExercised)`で更新。 PuttyV2.sol#L451でプット行使、ストライク移行後の`feeAmount`の計算と控除を以下のように追加。 ```solidity uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); ``` ## [H-02] acceptCounterOffer() May Result In Both Orders Being Filled ### Category Front run ### Conditions ### Hacking details acceptCounterOffer() は元のoderからcancel() を呼び出しますが、counter offerがすでにされている場合は、revertされない。 ```solidity function acceptCounterOffer( Order memory order, bytes calldata signature, Order memory originalOrder ) public payable returns (uint256 positionId) { // cancel the original order cancel(originalOrder); // accept the counter offer uint256[] memory floorAssetTokenIds = new uint256[](0); positionId = fillOrder(order, signature, floorAssetTokenIds); } ``` cancel() は、orderがすでに確定している場合はrevertされず、次のfillOrder() トランザクションが成功するのを防ぐだけ。 ```solidity function cancel(Order memory order) public { require(msg.sender == order.maker, "Not your order"); bytes32 orderHash = hashOrder(order); // mark the order as cancelled cancelledOrders[orderHash] = true; emit CancelledOrder(orderHash, order); } ``` なので、元のfillOrderを満たすトランザクション内容であれば、前倒ししてトランザクションを実行することが可能 ### How to Fix 上記の orderの内容が前のと一致している場合、cancel()を戻すようにすることを検討してください。これは、以下の行を追加することで可能です require(_ownerOf[uint256(orderHash)] == 0). ## [H-03]空ではないフロアのショートコール注文を作成すると、オプションの行使や引き出しが不可能になる ### Category ### Conditions case 1: 空のfloorAssetTokenIds配列でエクササイズを呼び出した場合。 case 2: order.floorTokensと同じ長さの空でないfloorAssetTokenIds配列でエクササイズを呼び出した場合。 ### Hacking details ケース1では、入力されたfloorAssetTokenIdsがput注文のために空であることがチェックされ、呼び出しはこの要件を通過しています。しかし、最終的に_transferFloorsInが呼ばれ、Index out of boundsエラーがでる。なぜなら、floorTokensが空ではなく、空のfloorAssetTokenIdsと一致しないため。 ```solidity // case 1 // PuttyV2.sol: _transferFloorsIn called by exercise // The floorTokens and floorTokenIds do not match the lenghts // floorTokens.length is not zero, while floorTokenIds.length is zero ERC721(floorTokens[i]).safeTransferFrom(from, address(this), floorTokenIds[i]); ``` case2では、入力されたfloorAssetTokenIdsがput注文で空であることを確認しましたが、空ではありません。そのため、リバーとされる ```solidity // maker trying to withdraw // PuttyV2.sol: withdraw _transferFloorsOut(order.floorTokens, positionFloorAssetTokenIds[floorPositionId]); ``` ### How to Fix これは、fillOrderが、注文のショートコール時にorder.floorTokensが空になることを保証していないために発生します。 ## [H-4]Zero strike call options can be systemically used to steal premium from the taker ### Category Revert on Zero Value Transfers ### Conditions ERC20を使ったゼロの値が送られる可能性あるとき ### Hacking details 0の金額を送れる可能性があり、ERC20の中には0の金額を許さないものもある。そうなった場合には、ゼロストライクコールを行使できないため実質無料でプレミアムを引き出すことが可能になる このように、権利行使価格がゼロのトークンオプションの場合、メーカーはショートコール注文を作成し、プレミアムを受け取ることができます。 ```solidity if (weth == order.baseAsset && msg.value > 0) { // check enough ETH was sent to cover the premium require(msg.value == order.premium, "Incorrect ETH amount sent"); // convert ETH to WETH and send premium to maker // converting to WETH instead of forwarding native ETH to the maker has two benefits; // 1) active market makers will mostly be using WETH not native ETH // 2) attack surface for re-entrancy is reduced IWETH(weth).deposit{value: msg.value}(); IWETH(weth).transfer(order.maker, msg.value); } else { ERC20(order.baseAsset).safeTransferFrom(msg.sender, order.maker, order.premium); } ``` ### How to Fix 例えば、すべてのケースで転送前にストライクが正であることを確認することを検討する。 ```solidity } else { + if (order.strike > 0) { ERC20(order.baseAsset).safeTransferFrom(msg.sender, address(this), order.strike); + } } ``` ## [M-01] 悪質なトークンコントラクトによるロックオーダーの可能性 ### Category token whitelist ### Conditions 以下のいずれかのフィールドに悪意のあるトークンコントラクトを含ませることで、注文のexercise()やwritten()を実行させないようにすることが可能です。 - `baseAsset` - `floorTokens[]` - `erc20Assets[]` - `erc721Assets[]` ### Hacking details 攻撃者は、注文を作成し、そのアドレスの1つを攻撃者のコントロール下にある悪意のあるコントラクトに設定することができる。攻撃者は、ユーザーが注文を満たすことを許可し、その後、常にそれを戻すように悪意のあるコントラクト上の変数をトグルします。 攻撃者は、注文が好ましくないポジションにある場合(例えば、ショートして価格が上昇した場合)、注文が`exercise()`されるのを防ぐことによって利益を得る。攻撃者は、時間切れか価格の下落を待って、悪意のあるトークンで転送が発生するようにします。 `withdraw()`も信頼できない外部アドレスを呼び出すため、同様の攻撃を行うことができます。この場合、攻撃者はオプションを行使し、他のユーザーがNFTまたはERC20トークンを要求できないようにすることができます。 ### How to Fix ユーザーが悪意のあるトークンコントラクトを設定するのを防ぐために、承認されたERC20トークンまたはERC721アドレスコントラクトをホワイトリスト化することを検討 ## [M-02] Unbounded loops may cause exercise()s and withdraw()s to fail ### Category infinity loop ### Conditions for inifinity loop ### Hacking details トークンを送る数に制限がないので、永遠に送信することができる。ガス量も変わることもあるため、貸し出し期間の27年間の間にトークンを受け取れない可能性もある。 ### How to Fix 資産数に上限を設ける、または必要に応じて1つずつ移管できるようにする ## [M-03] プットオプションの売り手は、ゼロ金額、または存在しないトークンを指定することで行使を防ぐことができる ### Category require miss ### Conditions プット・オプションの買い手は、売り手に資産を「プット」して、現在の市場価格ではなく、権利行使価格を得ることができるという特権のために、売り手にオプション・プレミアムを支払う。「プット」を実行できない場合、彼らは何もせずにプレミアムを支払ったことになり、本質的に資金を盗まれたことになる。 ### Hacking details プットオプションの売り手がorder.erc20Assetsに、いずれかの資産にゼロの金額を含めるか、そのアドレスにコードがない資産を指定すると、プットの買い手はオプションを行使できず、無駄にプレミアムを支払ったことになる。 ```solidity File: contracts/src/PuttyV2.sol #1 453 // transfer assets from exerciser to putty 454 _transferERC20sIn(order.erc20Assets, msg.sender); ``` ループの中でsafeTransferを使用しているので、rquiereが通ってしまった分は、transferされてしまう。requiereではじかれた時点でrevertされるので、プットオプションは実行されないのに、プレミアムはとられる ```solidity File: contracts/src/PuttyV2.sol #2 593 function _transferERC20sIn(ERC20Asset[] memory assets, address from) internal { 594 for (uint256 i = 0; i < assets.length; i++) { 595 address token = assets[i].token; 596 uint256 tokenAmount = assets[i].tokenAmount; 597 598 require(token.code.length > 0, "ERC20: Token is not contract"); 599 require(tokenAmount > 0, "ERC20: Amount too small"); 600 601 ERC20(token).safeTransferFrom(from, address(this), tokenAmount); 602 } 603 } ``` ### How to Fix `fillOrder()`の際に資産の金額と住所を確認し、その時点でトークンが存在しない場合は行使を許可する。 ## [M-04] Put options are free of any fees ### Category ### Conditions ### Hacking details オプションが行使されるたびに手数料が支払われることが可能性がある。 プロトコル料金が正しく課金される。 ```solidity // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); // @audit DoS due to reverting erc20 token transfer (weird erc20 tokens, blacklisted or paused owner; erc777 hook on owner receiver side can prevent transfer hence reverting and preventing withdrawal) - use pull pattern @high // @audit zero value token transfers can revert. Small strike prices and low fee can lead to rounding down to 0 - check feeAmount > 0 @high // @audit should not take fees if renounced owner (zero address) as fees can not be withdrawn @medium } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); // @audit fee should not be paid if strike is simply returned to short owner for expired put @high return; } ``` 逆に、プットオプションは手数料が無料です。 PuttyV2.sol#L450-L451 ```solidity // transfer strike from putty to exerciser ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike); ``` ### How to Fix プットオプション時にも手数料をチャージすべきです。 ## [M-05] fillOrder()とexercise()は、コントラクトに送られたEtherを永遠にロックする可能性があります。 ### Category require miss ### Conditions fillOrder() と exercise() には Ether を送る必要があるコードパスがあり (例: WETH を基本資産として使用、または行使価格の提供)、したがってこれら 2 つの関数には payable 修飾子が設定されています。しかし、これらの関数内にはEtherを必要としないコードパスが存在します。イーサを必要としないコードパスを使用した場合、関数に渡されたイーサは永遠に契約に固定され、送信者はその見返りとして何も追加で得ることはありません。 ### Hacking details function にpayableがありmsg.valueでETHが送金できてしまう ```solidity function fillOrder( Order memory order, bytes calldata signature, uint256[] memory floorAssetTokenIds ) public payable returns (uint256 positionId) { ~~~~~ File: contracts/src/PuttyV2.sol #1 323 if (order.isLong) { 324 ERC20(order.baseAsset).safeTransferFrom(order.maker, msg.sender, order.premium); 325 } else { ~~~~~ File: contracts/src/PuttyV2.sol #2 337 } else { 338 ERC20(order.baseAsset).safeTransferFrom(msg.sender, order.maker, order.premium); 339 } ~~~~~ File: contracts/src/PuttyV2.sol #3 435 } else { 436 ERC20(order.baseAsset).safeTransferFrom(msg.sender, address(this), order.strike); 437 } ~~~~~ } ``` ### How to Fix 上記3箇所にrequireを付与 ```solidity Add a require(0 == msg.value) for the above three conditions ``` ## [M-06] Contract Owner Could Block Users From Withdrawing Their Strike ### Category Denial-of-Service ### Conditions ### Hacking details 2つの方法で資金を引き出せせないようにすることができます。 1、owner()のアドレスを0にする 2、baseAssetがERC777トークンである場合 この2つの場合に、ユーザーは行使金額を引き出すことができず、資産は契約から抜け出せなくなります。 ### How to Fix オーナーフィーを回収するために、withdrawパターンを採用することをお勧めします。 出金時にオーナーアドレスに直接手数料を転送するのではなく、オーナーが受け取ることができる手数料の金額をステート変数に保存します。そして、オーナーがPuttyV2契約から手数料を引き出すことができる新しい関数を実装します。 次のような実装を考えてみましょう。以下の例では、オーナーへの料金転送の結果(成功または失敗)は、ユーザーのストライク引き出し処理に影響しないため、オーナーがユーザー拒否を実行する方法はない。 これにより、ユーザーはPutty内に保管されている資金の安全性について、より確実で信頼できるようになります。 ```solidity mapping(address => uint256) public ownerFees; function withdraw(Order memory order) public { ..SNIP.. // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ownerFees[order.baseAsset] += feeAmount } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); return; } ..SNIP.. } function withdrawFee(address baseAsset) public onlyOwner { uint256 _feeAmount = ownerFees[baseAsset]; ownerFees[baseAsset] = 0; ERC20(baseAsset).safeTransfer(owner(), _feeAmount); } ``` ## [M-07] 攻撃者は、ERC721をサポートしないNFT(cryptopunkなど)に対してショートプットのオプション注文を作成でき、ユーザーは注文を履行することができますが、オプションを行使することはできません。 ### Category ERC721 ### Conditions ### Hacking details 攻撃者は、クリプトパンクのショートプットオプションを作成することができます。ユーザーが注文を実行すると、baseAssetがコントラクトに転送されます。しかし、cryptopunkはERC721をサポートしていないため、safeTransferFrom関数呼び出しが失敗し、ユーザーはオプションを行使できない。攻撃者はプレミアムを取得し、オプションの期限が切れた後にbaseAssetを取り戻すことができる。 ### How to Fix 順番にnftsにホワイトリストを追加したり、cryptopunkでエクササイズをサポートすることを検討してください。 ## [M-08] Overlap Between ERC721.transferFrom() and ERC20.transferFrom() Allows order.erc20Assets or order.baseAsset To Be ERC721 Rather Than ERC20 ### Category ### Conditions ### Hacking details ERC20.transferFrom(address to, address from, uint256 amount) と ERC721.transferFrom(address to, address from, uint256 id) はどちらも同じ関数署名 0x23b872dd を持っています。この影響により、baseAssetやerc20AssetsがERC20ではなくERC721のアドレスになる可能性があります。 これらの関数は、NFTをプロトコルに正常に転送しますが、NFTをコントラクトから転送することはできません。これは、転送が ERC20.safeTransfer() で transfer(to, amount) を呼び出すためで、ERC721 の有効な関数シグネチャと一致しないためです。 したがって、この方法でfillOrder()を使ってコントラクトに転送されたERC721トークンは、exercise()もwithdraw()もコントラクトからトークンをうまく転送できないため、永久にその中に留まることになります。 ### How to Fix 承認されたERC721とERC20のトークンコントラクトをホワイトリストに登録することを検討してください。さらに、この2つのコントラクトをERC20とERC721の異なるホワイトリストに分け、それぞれのコントラクトが正しいカテゴリにあることを確認します。 ## [M-09] コントラクトは、手数料なしのフラッシュローンプールとして機能する ### Category ### Conditions 悪意のあるユーザは、PuttyV2契約を利用して、資産に手数料を支払うことなくフラッシュローンを行い、利益を得ることができる ### Hacking details 1. コントラクトは、標準のERC20以外のカスタムロジックを持つコントラクトをorder.baseAssetsが参照しているロングコール注文でPuttyV2.fillOrderを呼び出します。この注文では、PuttyV2コントラクトが負っているトークンとtokenAmountにerc20Assetsも指定します(erc721Assetsに類似)。 2. 実行が` ERC20(order.baseAsset).safeTransferFrom(order.maker, msg.sender, order.premium);` である場合、カスタムロジックはコントラクトアドレス order.baseAsset で実行される可能性があります。 3. その後、悪意のあるコントラクトは、ショートコールのポジションを行使するためにコールエクササイズを行います。このコールにより、`_transferERC20sOut`、`_transferERC721sOut`のロジックを実行して、注文で指定された資産を悪意のあるコントラクトに移し替えることになる。 4. このコントラクトは、その資産を利用して、他のプラットフォームで利益を上げるものです。その後、` ERC20(order.baseAsset).safeTransferFrom(order.maker, msg.sender, order.premium);`が実行が継続されます。 5. `fillOrder`の終了時に、`_transferERC20sIn`, `_transferERC721sIn`のロジックを実行し、PuttyV2に十分な資産を戻すだけで実行は終了します。 ### How to Fix ? ## [M-10] Putty position tokens may be minted to non ERC721 receivers ### Category ### Conditions ### Hacking details Puttyはコードベース全体でERC721 safeTransferとsafeTransferFromを使用して、ERC721トークンがERC721以外の受信機に転送されないようにしています。しかし、fillOrderの最初の位置のmintは_safeMintではなく_mintを使用し、ReceiverがERC721トークン転送を受け入れるかどうかをチェックしません。 ***PuttyV2#fillOrder*** ```solidity // create long/short position for maker _mint(order.maker, uint256(orderHash)); // create opposite long/short position for taker bytes32 oppositeOrderHash = hashOppositeOrder(order); positionId = uint256(oppositeOrderHash); _mint(msg.sender, positionId); ``` ***PuttyV2Nft#_mint*** ```solidity function _mint(address to, uint256 id) internal override { require(to != address(0), "INVALID_RECIPIENT"); require(_ownerOf[id] == address(0), "ALREADY_MINTED"); _ownerOf[id] = to; emit Transfer(address(0), to, id); } ``` 影響ERC721 トークンを受け取ることができないコントラクトがメーカーまたはテイカーにある場合、そのオプション ポジションはロックされ、譲渡不可能になる可能性があります。受け取り側のコントラクトがPuttyとやり取りする仕組みを提供しない場合、ポジションの行使やアセットの引き出しができなくなります。 ### How to Fix 推奨します。SolmateのERC721#_safeMintのrequireチェックを独自のmint関数で実装することを検討してください。 ```solidity function _safeMint(address to, uint256 id) internal virtual { _mint(to, id); require( to.code.length == 0 || ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == ERC721TokenReceiver.onERC721Received.selector, "UNSAFE_RECIPIENT" ); } ``` しかし、_safeMintを呼び出すと、再入可能性が生じることに注意してください!この変更を行う場合、ミントが効果ではなく相互作用として扱われることを確認し、再入可能性ガードを追加することを検討してください。 ```solidity /* ~~~ EFFECTS ~~~ */ // create opposite long/short position for taker bytes32 oppositeOrderHash = hashOppositeOrder(order); positionId = uint256(oppositeOrderHash); // save floorAssetTokenIds if filling a long call order if (order.isLong && order.isCall) { positionFloorAssetTokenIds[uint256(orderHash)] = floorAssetTokenIds; } // save the long position expiration positionExpirations[order.isLong ? uint256(orderHash) : positionId] = block.timestamp + order.duration; emit FilledOrder(orderHash, floorAssetTokenIds, order); /* ~~~ INTERACTIONS ~~~ */ _safeMint(order.maker, uint256(orderHash)); _safeMint(msg.sender, positionId); ``` ## [M-11] 手数料はユーザーの同意なく変更可能 ### Category contract ### Conditions 手数料は出金時に適用されますが、注文が成立し、その条件が合意されてから出金までの間に変更されることがあり、当該利用者が期待した資金が失われることになりる ### Hacking details - アリスとボブは手数料が0.1%の時に注文を満たすことに合意した - オプション期間中、手数料は3%に引き上げられる。 - 撤退時、彼らは権利行使価格の3%を支払うことになるが、そのような手数料があればそもそも注文を出さないだろう ### How to Fix order時の手数料を取得できるようにする ## [M-12] Options with a small strike price will round down to 0 and can prevent assets to be withdrawn ### Category Revert on Zero Value Transfers ### Conditions ### Hacking details 特定のERC-20トークンは、0値トークンの転送と復帰をサポートしていません。このようなトークンをorder.baseAssetとして使用すると、かなり小さなオプションストライクと低いプロトコル手数料率のために0に切り捨てられ、それらのポジションのための資産の引き出しができなくなる可能性があります。 **PuttyV2.sol#L499-L500** ```solidity // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); // @audit-info zero-value ERC20 token transfers can revert for certain tokens } ``` ERC20トークンの中には、ゼロバリューの転送でリバートされるものがあります(例:LEND)。order.baseAssetと小さなストライクプライスとして使用された場合、フィーストークンの転送はリバートされます。したがって、資産とストライキは撤回することができず、契約に固定されたままです。 例 order.baseAsset は、weired ERC-20 トークンの 1 つです。 order.strike = 999 (トークンの小数点以下の桁数によっては、非常に小さなオプションポジション) 手数料 = 1 (0.1%) ((999∗1)/1000=0.999) 0に切り捨て→ゼロ値送金の戻し取引 ### How to Fix ゼロバリューのトークン転送に対する簡単なチェックを追加。 ```solidity // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; if (feeAmount > 0) { ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } } ``` ## [M-13] 悪意のあるメーカーが注文期間を0に設定することができる ### Category setting parameter ### Conditions 悪意のあるメーカーは、最小注文期間を0に設定することができ、これは注文が成立した後、即座に失効することを意味します。取り手は引出しオプションのみを取得し、それも権利行使価格の手数料がかかるので、取り手はこの無意味な取引で損をすることになる。 ### Hacking details 1. 売り手がOrder durationを0にしてオーダーを作成 2. 買い手がこの注文を成立させる→注文期間0のため即座に終了 3. 買い手は権利行使価格の手数料を支払い撤退することしかできない ### How to Fix Order durationの最低期間の設定 ## [M-14] Order cancellation is prone to frontrunning and is dependent on a centralized database ### Category Front run ### Conditions ### Hacking details 注文のキャンセルは、メーカーが関数パラメータとして注文を入力し、cancel()を呼び出す必要があります。これは唯一のキャンセル方法ですが、2つの問題が発生する可能性があります。 この最初の問題は、MEV ユーザーがキャンセルをフロントランして注文を満たすためのオンチェーン信号であることです。 2つ目の問題は、注文をキャンセルするために中央集権的なサービスに依存することです。注文はオフチェーンで署名されるので、中央のデータベースに保存されることになります。エンドユーザーが、自分が出した注文をすべてローカルに記録することは、まずないでしょう。つまり、注文をキャンセルする場合、メーカーは集中型サービスに注文パラメータを要求する必要があります。もし、集中管理サービスがオフラインになった場合、注文データベースのコピーを持つ悪意のある者が、他の方法ではキャンセルされたはずの注文を満たすことができるようになる可能性があります。 1. ボブが注文に署名し、それがPuttyのサーバーに記録される。 2. アリスはPuttyのAPIを使用してすべての注文をミラーリングする。 3. Putty サーバーがオフラインになる。 4. ボブは、トークンの価格を変更すると注文が不利になるため、注文をキャンセルしようとします。 5. BobはPuttyサーバーがダウンし、使用したトークンの正確な量を覚えていないため、注文をキャンセルすることができない。 6. アリスは自分のローカルミラーにあるすべての注文を調べ、ボブの注文を含むキャンセルされていない注文を、自分にとって非常に有利な条件で成立させる。 ### How to Fix 標準的なオーダーキャンセル方法とは別に、呼び出し元のすべてのオーダーをキャンセルする追加メソッドを持つ。これは、ユーザーアドレスからnonceへのマッピングとして、「最小有効nonce」状態変数を使用して実現することができます。 ```solidity mapping(address => uint256) minimumValidNonce; ``` ユーザがminimumValidNonceをインクリメントできるようにする。呼び出し側が誤ってminimumValidNonceを2**256-1に増加させてオーダーを作成できないように、インクリメント関数が2**64以上のインクリメントを許可しないことを確認する。そして、order.nonce < minimumValidNonce の場合、オーダーを充填しないようにする。 一括キャンセルを実現するもう一つの方法は、カウンターを使用することです。例えば、Seaportはカウンターを使用しており、これは対応するカウンター状態変数と一致しなければならない追加の注文パラメータである。これは、カウンタ状態変数を1つ増加させることによって、メーカーが自分のすべての注文をキャンセルすることを可能にします。 これらの追加キャンセル方法のいずれかが、MEVボットに信号を送ることなく、また集中データベースに依存することなく注文のキャンセルを可能にします。 ## [M-15] ゼロストライクコールオプションは、システム手数料の支払いを回避することができる ### Category ### Conditions ゼロまたはゼロに近い権利行使価格のコールは、一般的なデリバティブの一種です。このようなデリバティブの場合、システムは手数料を受け取らないため、手数料は注文の権利行使価格の端数として設定されます。 また、OTMのコールオプションの場合、オプションそのものはほとんど価値がないのに、権利行使価格が大きくなるため、手数料が大きくなってしまうという問題があります。例えば、1kのETH BAYCコールに大した価値はないのに、手数料は通常の10倍、つまり相当な額になるのに、それを正当化する材料が何もない。 これは、コアシステムの手数料徴収を停止または歪めることができる設計上の仕様であるため、深刻度中とマークしています。 ### Hacking details 現在、手数料は注文の権利行使に連動しており、例えば深いITM(インザマネー)とOTM(アウトオブザマネー)のコールなど、異なるタイプの注文によって手数料が大きく異なる。 ```solidity // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); return; } ``` ### How to Fix 手数料は、簡単に操作できないオプション価値であり、システムの取引量に正確に対応するため、オプションプレミアムと連動させることを検討する。 つまり、手数料の徴収をfillOrderに移動することを検討する。 ```solidity // transfer premium to whoever is short from whomever is long if (order.isLong) { ERC20(order.baseAsset).safeTransferFrom(order.maker, msg.sender, order.premium); } else { // handle the case where the user uses native ETH instead of WETH to pay the premium if (weth == order.baseAsset && msg.value > 0) { // check enough ETH was sent to cover the premium require(msg.value == order.premium, "Incorrect ETH amount sent"); // convert ETH to WETH and send premium to maker // converting to WETH instead of forwarding native ETH to the maker has two benefits; // 1) active market makers will mostly be using WETH not native ETH // 2) attack surface for re-entrancy is reduced IWETH(weth).deposit{value: msg.value}(); IWETH(weth).transfer(order.maker, msg.value); } else { ERC20(order.baseAsset).safeTransferFrom(msg.sender, order.maker, order.premium); } } ``` ## [M-16] Use of Solidity version 0.8.13 which has two known issues applicable to PuttyV2 ### Category varsion ### Conditions ### Hacking details solidity version 0.8.13 では、PuttyV2 に該当する以下の2つの問題があります。 1.ABI-encodingに関連する脆弱性。 ref : https://blog.soliditylang.org/2022/05/18/solidity-0.8.14-release-announcement/ 関数hashOrder()およびhashOppositeOrder()には適用条件があるため、本脆弱性を悪用される可能性があります。 "...ネストされた配列を直接別の外部関数呼び出しに渡すか、その上でabi.encodeを使用する。" 2.インラインアセンブリのメモリ副作用に関するオプティマイザバグ」に関連する脆弱性 ref : https://blog.soliditylang.org/2022/06/15/solidity-0.8.15-release-announcement/ PuttyV2はopenzeppelinとsolmateのsolidityコントラクトを継承しており、どちらもインラインアセンブリを使用しており、コンパイル時に最適化が行われるようになっています。 ### How to Fix これらの問題が修正された最近の Solidity バージョン 0.8.15 を使用してください。