# Day 7 以下yoki記載 # [yoki] addApprovedTokens, addWhitelistで同じトークンを複数登録することが可能 ## 要約 addApprovedTokens, addWhitelistで重複してトークン登録することが可能なので、`_isXXX`関数におけるオーバーフローが生じる。 ## バグの詳細 addApprovedTokens, addWhitelistでは重複のチェックなく配列に格納しています。 ## インパクト `_isXXX`関数内において、`unchecked`を用いて加算しているので`addApprovedTokens`や`addWhitelist`の要素数が増えるとオーバーフローとなる可能性がある。 ## 対象となるコード ```.sol function addWhitelist(address _token) public { if (!_isXXX(_token, approvedTokens)) revert(); whitelist.push(_token); } ``` ```.sol function addApprovedTokens(address _token) public { if (msg.sender != owner) revert(); approvedTokens.push(_token); } ``` ## 使用したツール 特になし ## 改善方法 配列にpushする前に追加対象の値外既に存在するか確認する。 # [yoki] isETHをtrueにしてETH以外のトークンを指定すること(またはその逆)が可能である ## 要約 ## バグの詳細 `deposit`や`withdraw`関数にて`_isETH`を引数として持つが、 * ETHを扱うが`_isETH`を`false` * ETHを扱わないが`_isETH`を`true` といったリクエストが可能である。 ## インパクト 例えば、`deposit`関数利用時に任意のトークンを指定しと`_isETH`を`true`とした場合にはETHの送金処理が実行されることになる。 ## 対象となるコード ```.sol function deposit(uint _amount, address _token, bool _isETH) public payable { ``` ```.sol function withdraw( address _to, uint _amount, bool _isETH, address _token ) public payable { ``` ```.sol function _tokenTransfer(TransferInfo memory _info) private { if (_info.isETH) { emit DebugLogEvent("debug: This is eth"); (bool success, ) = _info.to.call{ value: _info.amount }(""); require(success, "Failed"); } else { emit DebugLogEvent("debug: This is not eth"); IERC20(_info.token).transferFrom(_info.from, _info.to, _info.amount); } } ``` ## 使用したツール 特になし ## 改善方法 ETHかどうかの判別をインターフェイスに持たせるのではなく、`msg.value`の値を利用してその判別を行う。 --- 以下mabukさん記載 # ETHのdepositが出来ない ## 要約 ETHをコントラクトにdeposit関数で送金すると必ずエラーとなる。つまり誰もETHをdeposit出来ない。 ## バグの詳細 コントラクトのコンパイルは出来るが、この実装だとコントラクトはETHの送金を受け取れない。 ETHの送金を受け取るにはdepositはpayableであり、かつreceive関数またはfallback関数が必要。 ## インパクト ETHのdepositが必ず失敗するので誰もETHのdepositが出来ない。 ## 対象となるコード ```solidity function deposit(uint _amount, address _token, bool _isETH) public { if (!_isXXX(_token, whitelist)) revert(); DepostInfo memory depositInfo; TransferInfo memory info = TransferInfo({ isETH: _isETH, token: _token; from: msg.sender, amount: _amount, to: address(this), }); _tokenTransfer(info); depositInfo.lastTime = uint40(block.timestamp); depositInfo.amount = _amount; depositAmt[msg.sender][_token] = depositInfo; } ``` ## 使用したツール hardhat ## 改善方法 - receive関数の追加 - depositにpayable修飾子を追加 ```solidity //before function deposit(uint _amount, address _token, bool _isETH) public { //after receive() external payable {} function deposit(uint _amount, address _token, bool _isETH) public payable { ``` # 送金するETHより小さい値を引数に指定してdeposit出来る ## 要約 ETHの場合、ウォレットから出ていく値とdepositに指定出来る値が異なっていてもエラーにならない ## バグの詳細 1. depositのamountを0.5ethに設定する 2. ウォレットからの送金額を1.0ethに設定する 3. depositを実行 4. コントラクトの残高は1.0eth増える 5. ウォレットの残高は1.0eth減る ## インパクト depositを行うユーザのethがGOXする可能性がある ## 対象となるコード ```solidity function deposit( uint256 _amount, address _token, bool _isETH ) ``` ## 使用したツール hardhat ## 改善方法 2つある 1. ETHと他のトークンのdeposit関数を分ける 2. ETHの場合はdeposit時にamountを設定不可とする # 残高がある時にdepositを呼ぶと資金を失う ## 要約 *前提: depositは残高が0の場合のみ受け付けるという仕様(何度も呼んで残高を増やしていくことは想定外とする)* 以前にdepositを行い、残高がまだある状態で再度depositを行うと、以前の残高は失われ新しい残高で上書きされる ## バグの詳細 1. 9/20に100トークンのデポジットを行う 2. lastTime:9/20, amount:100トークンが記録される 3. 9/30に10トークンのデポジットを行う 4. lastTime:9/20, amount:100トークンが記録は消え、lastTime:9/30, amount:10トークンの記録だけが残る ## インパクト ユーザーが資産を失う可能性がある ## 対象となるコード ```solidity depositInfo.lastTime = uint40(block.timestamp); depositInfo.amount = _amount; depositAmt[msg.sender][_token] = depositInfo; ``` ## 使用したツール 特になし ## 改善方法 残高が無いもしくは0である場合以外はエラーとする --- 以下hamaupさん記載 # [GTeam 0X] withdraw()預金残高を超えても引き出しができてしまう。 ## 要約  withdraw()で、預金残高を超えても引き出しができてしまう。 引き出しをした際に、預金残高を更新するように提案する。 ## バグの詳細 withdraw()を預金残高以下の引き出し額に指定して実行した場合、預金残高を保存している depositAmt[msg.sender][_token].amountの値が更新されない。 もう一度、withdraw()を実行した際に、また同じ預金残高の値を使用して引き出し処理をしてしまう。 ## インパクト コントラクト残高がなくなるまで引き出しされてしまう。 ## 対象となるコード ```solidity= function withdraw( address _to, uint _amount, bool _isETH, address _token ) public { if (!_isXXX(_token, whitelist)) revert(); TransferInfo memory info = TransferInfo({ isETH: _isETH, token: _token; from: address(this), amount: _amount, to: _to, }); uint canWithdrawAmount = depositAmt[msg.sender][_token].amount; require(_info.amount < canWithdrawAmount, "ERROR"); canWithdrawAmount = 0; _tokenTransfer(_info); uint rewardAmount = getReward(_token); IERC20(BBBToken).transfer(msg.sender, rewardAmount); } ``` ## 使用したツール 特になし ## 改善方法 withdraw() 引き出し額を差し引いて預金残高 `depositAmt[msg.sender][_token].amount` を更新する。 #### before ```solidity= uint canWithdrawAmount = depositAmt[msg.sender][_token].amount; require(_info.amount < canWithdrawAmount, "ERROR"); canWithdrawAmount = 0; ``` #### after ```solidity= uint canWithdrawAmount = depositAmt[msg.sender][_token].amount; require(_info.amount < canWithdrawAmount, "ERROR"); canWithdrawAmount = 0; depositAmt[msg.sender][_token].amount -= _amount ``` # [GTeam 0X] withdraw()引き出し額を0に指定した場合でもrewardを受け取れてしまう。 ## 要約 withdraw()引き出し額を0に指定した場合でもrewardを受け取れてしまう。 その場合は、報酬金`reward` を送れないようにする。 ## バグの詳細 少しでも預金残高がある場合、withdraw()で引き出し額 `_amount` を0に指定しても、報酬額`reward` を決定され送信されてしまう。 1. 少額をdeposit()する。 2. withdraw()引き出し額 `_amount` を0に指定する 3. 引き出し額 `_amount` は、転送する額に`info.amount`保持される。 4. 預金残高 `depositAmt[msg.sender][_token].amount` が1でもある場合、 以下条件パスする。 ```solidity= require(_info.amount < canWithdrawAmount, "ERROR"); ``` 5. getReward()内で預金残高で報酬金`reward` を決定 6. withdraw()を実行した者`msg.sender`に報酬金`reward` を送信 ## インパクト 引き出し額 `_amount` を0に指定して引き出しを繰り返されると用意している報酬額`reward`がすべてなくなってしまう。 ## 対象となるコード ```solidity= function withdraw( address _to, uint _amount, bool _isETH, address _token ) public { if (!_isXXX(_token, whitelist)) revert(); TransferInfo memory info = TransferInfo({ isETH: _isETH, token: _token; from: address(this), amount: _amount, to: _to, }); uint canWithdrawAmount = depositAmt[msg.sender][_token].amount; require(_info.amount < canWithdrawAmount, "ERROR"); canWithdrawAmount = 0; _tokenTransfer(_info); uint rewardAmount = getReward(_token); IERC20(BBBToken).transfer(msg.sender, rewardAmount); } ``` ## 使用したツール 特になし ## 改善方法 # [GTeam 0X]の改善方法を併用して、引き出し額 `_amount` が0では報酬金`reward` を送れないようにする。 #### before ```solidity= uint canWithdrawAmount = depositAmt[msg.sender][_token].amount; require(_info.amount < canWithdrawAmount, "ERROR"); canWithdrawAmount = 0; ``` #### after ```solidity= uint canWithdrawAmount = depositAmt[msg.sender][_token].amount; require(_info.amount < canWithdrawAmount, "ERROR"); require(_info.amount > 0, "ERROR"); canWithdrawAmount = 0; depositAmt[msg.sender][_token].amount -= _amount ``` [GTeam 03] 預け入れから引き出しまでの期間が長いほど報酬金`reward`が少なくなる。 ## 要約 預け入れから引き出しまでの期間が長いほど報酬金`reward`が少なくなってしまい、即座に預け入れ引き出しを繰り返す利用方法が増えてしまう。 報酬金`reward`の計算方法とdeposit()のlasttimeの値の保存方法の検討を提案する。 ## バグの詳細 報酬金`reward`計算式です。 ```solidity= reward = (REWARD_RATE / (block.timestamp - lastTime)) * amount; ``` 1. 初めてdeposit()を実行した時に、lastTimeにタイムスタンプが入ります。 deposit() ```solidity= depositInfo.lastTime = uint40(block.timestamp); ``` 2. widthdraw()で報奨金計算 getReward()を呼び出した時に、lastTimeにタイムスタンプが入ります。 getReward() ```solidity= uint lastTime = depositAmt[msg.sender][token].lastTime; ``` 3. 報奨金が計算されます。その際、widthdraw()で報奨金計算した時から初めてdeposit()を実行した時の差分が分母に入ります。 getReward() ```solidity= reward = (REWARD_RATE / (block.timestamp - lastTime)) * amount; ``` 例えば、初めてdeposit()を実行した後に、もう一度deposit()をした場合、lastTimeが2回目に実行したタイムスタンプに置き換えられる。 deposit() ```solidity= uint lastTime = depositAmt[msg.sender][token].lastTime; ``` その場合、現在のタイムスタンプから初めてdeposit()を実行した期間より現在のタイムスタンプから2回目にdeposit()を実行した期間の方が短い、つまり報酬金計算式の分母が小さくなるため、預け入れから引き出しまでの期間が長いほど報酬金`reward`が少なくなってしまう。 また報酬金計算式内にblock.timestampを使用しているので、誤差が出る可能性があり、不正なマイナーによる改ざんもありセキュリティ面で安全ではない。 deposit()のlasttimeの値は、預け入れする度に上書きされてしまう。引き出しを行わず、預け入れを繰り返し行った場合、最後の預け入れをした時のタイムスタンプに置き換わってしまう。 ## インパクト 短く預け引き出す方が報酬が高くなってしまうため、即座に預け入れ引き出しを繰り返す利用方法が増えてしまう。 ## 対象となるコード ```solidity= function getReward(address token) public view returns (uint reward) { uint amount = depositAmt[msg.sender][token].amount; uint lastTime = depositAmt[msg.sender][token].lastTime; reward = (REWARD_RATE / (block.timestamp - lastTime)) * amount; } ``` ## 使用したツール 特になし ## 改善方法 報酬金`reward`の計算方法とdeposit()のlasttimeへの値の保存方法の検討を提案する。