Platform: Code4rena
Start Date: 18/10/2023
Pot Size: $36,500 USDC
Total HM: 17
Participants: 77
Period: 7 days
Judge: MiloTruck
Total Solo HM: 5
Id: 297
League: ETH
Rank: 9/77
Findings: 7
Award: $700.75
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: falconhoof
Also found by: 0x6d6164616e, Baki, Beosin, Krace, T1MOH, bitsurfer, immeas, pep7siup, tnquanghuy0512, twicek
180.637 USDC - $180.64
In the Math library, 1 WAD
is equal to 1e18 which is assumed 1
For example: wmul(2e18, 4e18) = 8e18
In AccountingEngine contract, the devs assumed that 1 WAD
is equal to 0.01 (1%), 100 WAD
is equal to 1 (100%). Hence, the function auctionSurplus()
will make start the auction with selling amount way bigger than expected. Moreover, if the selling amount is bigger than the balance of AccountingEngine contract, it will cause DOS
function auctionSurplus() external returns (uint256 _id) { ... // auction surplus percentage if (_params.surplusTransferPercentage < ONE_HUNDRED_WAD) { _id = surplusAuctionHouse.startAuction({ @@> _amountToSell: _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage), _initialBid: 0 }); lastSurplusTime = block.timestamp; emit AuctionSurplus(_id, 0, _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage)); } ... }
We have the table of scenario:
_params.surplusAmount | _params.surplusTransferPercentage | ONE_HUNDRED_WAD - _params.surplusTransferPercentage | _amountToSell expected | _amountToSell reality | _amountToSell reality/expected ratio |
---|---|---|---|---|---|
1e18 | 0.1e18 (10%) | 99.9e18 | 0.9e18 | 99.9e18 | 111 time |
1e18 | 0.3e18 (30%) | 99.7e18 | 0.7e18 | 99.7e18 | 142 time |
1e18 | 0.5e18 (50%) | 99.5e18 | 0.5e18 | 99.5e18 | 199 time |
1e18 | 0.8e18 (80%) | 99.2e18 | 0.2e18 | 99.2e18 | 496 time |
1e18 | 1e18 - 1 (~100%) | 1 | 1 | ~99e18 | ~99e18 time |
Manual review
Change this in ACcountingEngine.auctionSurplus()
- if (_params.surplusTransferPercentage < ONE_HUNDRED_WAD) { + if (_params.surplusTransferPercentage < WAD) { _id = surplusAuctionHouse.startAuction({ - _amountToSell: _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage), + _amountToSell: _params.surplusAmount.wmul(WAD - _params.surplusTransferPercentage), _initialBid: 0 }); lastSurplusTime = block.timestamp; - emit AuctionSurplus(_id, 0, _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage)); + emit AuctionSurplus(_id, 0, _params.surplusAmount.wmul(WAD - _params.surplusTransferPercentage)); }
Math
#0 - c4-pre-sort
2023-10-26T16:33:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T16:33:47Z
raymondfam marked the issue as duplicate of #26
#2 - c4-judge
2023-11-03T14:07:02Z
MiloTruck changed the severity to 3 (High Risk)
#3 - c4-judge
2023-11-03T14:08:28Z
MiloTruck marked the issue as satisfactory
🌟 Selected for report: 0xmystery
Also found by: 0x6d6164616e, 0xWaitress, 0xsurena, Tendency, ZanyBonzy, cryptothemex, hals, lsaudit, ni8mare, niki, phoenixV110, spark, tnquanghuy0512, twcctop
26.0735 USDC - $26.07
https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/CamelotRelayer.sol#L58 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/UniV3Relayer.sol#L64
It's quite often that ERC20s have decimals higher than 18. Because of that, constructor in CamelotRelayer and UniV3Relayer will get reverted with quote token that has high decimals because of underflow in this line
multiplier = 18 - IERC20Metadata(_quoteToken).decimals();
Manual Review
Make multiplier
variable has int256
type.
ERC20
#0 - c4-pre-sort
2023-10-26T17:49:56Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T17:50:07Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2023-10-27T05:08:06Z
raymondfam marked the issue as duplicate of #323
#3 - c4-judge
2023-11-02T08:45:36Z
MiloTruck marked the issue as satisfactory
🌟 Selected for report: tnquanghuy0512
Also found by: 0xprinc, Baki, COSMIC-BEE-REACH, MrPotatoMagic, Tendency
218.7259 USDC - $218.73
https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/SAFEHandler.sol#L1-L19 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/ODSafeManager.sol#L112-L115 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/ODSafeManager.sol#L59-L62
SafeHandler contract doesn't have any method to call to ODSafeManager.allowHandler()
. Because of that, functions in ODSafeManager contract which use handlerAllowed
modifier will never be called successfully. There're two function quitSystem()
and enterSystem()
that use handlerAllowed
modifier.
This is the whole SAFEHandler contract, it doesn't have any method that call to ODSafeManager.allowHandler()
// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.19; import {ISAFEEngine} from '@interfaces/ISAFEEngine.sol'; contract SAFEHandler { constructor(address _safeEngine) { ISAFEEngine(_safeEngine).approveSAFEModification(msg.sender); } }
Hence, it can't call to allowHandler()
, which give users permission to execute action within the safe
function allowHandler(address _usr, uint256 _ok) external { handlerCan[msg.sender][_usr] = _ok; emit AllowHandler(msg.sender, _usr, _ok); }
Because of that, no one can surpass the handlerAllowed
modifier with valid handler address:
modifier handlerAllowed(address _handler) { if (msg.sender != _handler && handlerCan[_handler][msg.sender] == 0) revert HandlerNotAllowed(); _; }
enterSystem()
and quitSystem()
use handlerAllowed
modifier:
function enterSystem(address _src, uint256 _safe) external handlerAllowed(_src) safeAllowed(_safe) function quitSystem(uint256 _safe, address _dst) external safeAllowed(_safe) handlerAllowed(_dst)
Manual Review
I think of two way to achieve this:
ODSafeManager.allowHandler()
SAFE_ID
in SafeHandler contract and change this to ODSafeManager.allowHandler()
:- function allowHandler(uint256 _safeId, address _usr, uint256 _ok) external { + function allowHandler(address _usr, uint256 _ok) external { + SAFEData memory _sData = _safeData[_safeId]; + require(_sData.owner == msg.sender); - handlerCan[msg.sender][_usr] = _ok; + handlerCan[_sData.safeHandler][_usr] = _ok; - emit AllowHandler(msg.sender, _usr, _ok); }
Other
#0 - c4-pre-sort
2023-10-26T17:10:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T17:10:57Z
raymondfam marked the issue as duplicate of #76
#2 - c4-pre-sort
2023-10-26T19:04:53Z
raymondfam marked the issue as duplicate of #380
#3 - c4-judge
2023-11-02T18:23:28Z
MiloTruck marked the issue as not a duplicate
#4 - c4-judge
2023-11-02T18:23:33Z
MiloTruck marked the issue as primary issue
#5 - c4-judge
2023-11-02T18:51:02Z
MiloTruck marked the issue as selected for report
#6 - c4-judge
2023-11-02T18:51:06Z
MiloTruck marked the issue as satisfactory
#7 - MiloTruck
2023-11-02T18:53:14Z
The warden has demonstrated how allowHandler()
can never be called with msg.sender
as a user's safe handler, causing enterSystem()
and quitSystem()
to become uncallable. As this affects the functionality of the protocol but does not cause any loss of assets or affect the protocol's core functionality, I believe medium severity is appropriate.
54.1911 USDC - $54.19
There's two thing that we need to resolve here:
block.number
in Arbitrum, with votingDelay
votingPeriod
is calculated by block.number as a clock, it even easier for malicious to manipulate.Similar issue: https://github.com/code-423n4/2023-06-lybra-findings/issues/114
VotingDelay definition: Delay, between the proposal is created and the vote starts
VotingPeriod definition: Delay between the vote start and vote end
Manual Review
Make the initial votingDelay and votingPeriod longer to resolve issue 1 Use block.timestamp rather than block.number to resolve issue 2
Timing
#0 - c4-pre-sort
2023-10-26T18:33:54Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-10-26T18:34:02Z
raymondfam marked the issue as duplicate of #73
#2 - c4-judge
2023-11-02T07:06:55Z
MiloTruck changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-11-02T08:47:10Z
MiloTruck marked the issue as satisfactory
🌟 Selected for report: MrPotatoMagic
Also found by: Bughunter101, COSMIC-BEE-REACH, HChang26, Stormreckson, T1MOH, Tendency, hals, josephdara, klau5, merlin, tnquanghuy0512, twcctop
37.1417 USDC - $37.14
In order for other user to call ODSafeManager.modifySAFECollateralization()
ODSafeManager.transferCollateral()
ODSafeManager.transferInternalCoins()
ODSafeManager.quitSystem()
ODSafeManager.enterSystem()
ODSafeManager.moveSAFE()
ODSafeManager.removeSAFE()
, the safe owner has to set the their permission to owner's safe by call ODSafeManager.allowSAFE()
.
The problem here is that anyone after getting their permission to the safe can add/remove others permission in that safe.
Moreover, the owner basically can't revoke all the permissions if the owner is not a contract (which is highly the case), since revoking permission using transferSAFEOwnership()
can only revoke once per transaction. If the owner try to give permission to multicall contract in order to revoke all the malicious permission, malicious users can just simply remove multicall contract's permission in between the two transaction
allowSAFE()
can be called by allowed user to remove/add others user:
function allowSAFE(uint256 _safe, address _usr, uint256 _ok) external safeAllowed(_safe) { address _owner = _safeData[_safe].owner; safeCan[_owner][_safe][_usr] = _ok; emit AllowSAFE(msg.sender, _safe, _usr, _ok); }
modifier safeAllowed(uint256 _safe) { address _owner = _safeData[_safe].owner; if (msg.sender != _owner && safeCan[_owner][_safe][msg.sender] == 0) revert SafeNotAllowed(); _; }
Function that can be called by allowed user (functions that have safeAllowed
modifier):
function modifySAFECollateralization(uint256 _safe, int256 _deltaCollateral, int256 _deltaDebt) external safeAllowed(_safe) function transferCollateral(uint256 _safe, address _dst, uint256 _wad) external safeAllowed(_safe) function transferCollateral(bytes32 _cType, uint256 _safe, address _dst, uint256 _wad) external safeAllowed(_safe) function transferInternalCoins(uint256 _safe, address _dst, uint256 _rad) external safeAllowed(_safe) function quitSystem(uint256 _safe, address _dst) external safeAllowed(_safe) handlerAllowed(_dst) function enterSystem(address _src, uint256 _safe) external handlerAllowed(_src) safeAllowed(_safe) function moveSAFE(uint256 _safeSrc, uint256 _safeDst) external safeAllowed(_safeSrc) safeAllowed(_safeDst) function removeSAFE(uint256 _safe) external safeAllowed(_safe) function protectSAFE(uint256 _safe, address _liquidationEngine, address _saviour) external safeAllowed(_safe)
Manual Review
Make the function allowSAFE()
can only be called by safe owner
- function allowSAFE(uint256 _safe, address _usr, uint256 _ok) external safeAllowed(_safe) { + function allowSAFE(uint256 _safe, address _usr, uint256 _ok) external { address _owner = _safeData[_safe].owner; + require(msg.sender == _owner, "Not Owner"); safeCan[_owner][_safe][_usr] = _ok; emit AllowSAFE(msg.sender, _safe, _usr, _ok); }
Access Control
#0 - c4-pre-sort
2023-10-26T05:26:14Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T05:26:28Z
raymondfam marked the issue as duplicate of #171
#2 - c4-judge
2023-11-02T08:44:22Z
MiloTruck marked the issue as satisfactory
🌟 Selected for report: klau5
Also found by: 0x6d6164616e, Arz, T1MOH, immeas, josephdara, nican0r, tnquanghuy0512
102.2123 USDC - $102.21
https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/CamelotRelayer.sol#L10 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/CamelotRelayer.sol#L70 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/CamelotRelayer.sol#L70-L76 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/CamelotRelayer.sol#L93-L94
CamelotRelayer contract's purpose is using CamelotV3 protocol as a price oracle. But CamelotRelayer treated Camelot protocol as similar as UniswapV3 protocol. CamelotV3 us based from Algebra v1.9 which is not quite alike as UniswapV3. Because of this, CamelotRelayer contract will not useable.
From Camelot docs: Camelot's V3 AMM is based on Algebra's V1.9
In CamelotRelayer contract, it uses UniswapV3's OracleLibrary
import {OracleLibrary} from '@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol';
It uses 3 functions in UniswapV3's OracleLibrary:
Those 3 functions in OracleLibrary call to 3 function in UniwapV3Pool:
This is the USDT-USDC CamelotPair contract's read functions, notice here that there's no slot0()
, observations()
, observe()
read function. Hence, CamelotRelayer.getResultWithValidity()
and CamelotRelayer.read()
will forever DOS
Manual Review
The devs need to read carefully Algebra's docs to fix this
Other
#0 - c4-pre-sort
2023-10-26T17:31:37Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T17:31:47Z
raymondfam marked the issue as duplicate of #75
#2 - c4-judge
2023-11-02T08:46:10Z
MiloTruck marked the issue as satisfactory
🌟 Selected for report: lsaudit
Also found by: 0xPsuedoPandit, 0xhacksmithh, Stormreckson, ZanyBonzy, klau5, tnquanghuy0512, twicek, xAriextz
81.7698 USDC - $81.77
ODSafeManager.transferSAFEOwnership()
function delete safeCan
of the previous owner. Because of that, when one of the old owner owns the NFTs again, there can be a risk of now-malicious old permission that can execute actions to the safe.
Imagine this scenario:
allowSAFE()
to user B to do somethingfunction transferSAFEOwnership(uint256 _safe, address _dst) external { require(msg.sender == address(vault721), 'SafeMngr: Only Vault721'); if (_dst == address(0)) revert ZeroAddress(); SAFEData memory _sData = _safeData[_safe]; if (_dst == _sData.owner) revert AlreadySafeOwner(); _usrSafes[_sData.owner].remove(_safe); _usrSafesPerCollat[_sData.owner][_sData.collateralType].remove(_safe); _usrSafes[_dst].add(_safe); _usrSafesPerCollat[_dst][_sData.collateralType].add(_safe); _safeData[_safe].owner = _dst; <@@ NOTICE here that's the function doesn't `delete safeCan[_sData.owner][_safe]` here emit TransferSAFEOwnership(msg.sender, _safe, _dst); }
Manual Review
function transferSAFEOwnership(uint256 _safe, address _dst) external { require(msg.sender == address(vault721), 'SafeMngr: Only Vault721'); if (_dst == address(0)) revert ZeroAddress(); SAFEData memory _sData = _safeData[_safe]; if (_dst == _sData.owner) revert AlreadySafeOwner(); _usrSafes[_sData.owner].remove(_safe); _usrSafesPerCollat[_sData.owner][_sData.collateralType].remove(_safe); _usrSafes[_dst].add(_safe); _usrSafesPerCollat[_dst][_sData.collateralType].add(_safe); _safeData[_safe].owner = _dst; + delete safeCan[_sData.owner][_safe]; emit TransferSAFEOwnership(msg.sender, _safe, _dst); }
Other
#0 - c4-pre-sort
2023-10-26T19:13:45Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-10-26T19:14:08Z
raymondfam marked the issue as duplicate of #41
#2 - c4-pre-sort
2023-10-27T04:35:45Z
raymondfam marked the issue as duplicate of #142
#3 - c4-pre-sort
2023-10-27T04:36:18Z
raymondfam marked the issue as sufficient quality report
#4 - c4-judge
2023-11-02T08:47:37Z
MiloTruck marked the issue as satisfactory