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: 17/77
Findings: 4
Award: $341.16
π 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
The function auctionSurplus
has the following checks that _params.surplusTransferPercentage
is not greater than WAD
:
if(_params.surplusTransferPercentage > WAD) revert AccEng_surplusTransferPercentOverLimit();
The check has the consequence of only allowing 99% of the _params.surplusAmount
to be auctioned out, with ONE_HUNDRED_WAD
being 100%:
AccountingEngine.sol#L212-L217
// auction surplus percentage if (_params.surplusTransferPercentage < ONE_HUNDRED_WAD) { _id = surplusAuctionHouse.startAuction({ _amountToSell: _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage), _initialBid: 0 });
Preventing any range of surplus amount to be auctioned is an implementation error as discussed with the sponsor:
auctionSurplus
should run when _params.surplusTransferPercentage is greater than 0% and less than 100% in terms of WAD.
It will not be possible to auction less than 99% of the surplus amount.
Manual Review
Consider modifying the check like this:
- if(_params.surplusTransferPercentage > WAD) revert AccEng_surplusTransferPercentOverLimit(); + if(_params.surplusTransferPercentage > ONE_HUNDRED_WAD) revert AccEng_surplusTransferPercentOverLimit();
Or you could also remove the check altogether because this line will already revert if _params.surplusTransferPercentage
is greater than 100%:
AccountingEngine.sol#L215
_amountToSell: _params.surplusAmount.wmul(ONE_HUNDRED_WAD - _params.surplusTransferPercentage),
Error
#0 - c4-pre-sort
2023-10-26T04:39:26Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T04:39:52Z
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:32Z
MiloTruck marked the issue as satisfactory
70.4484 USDC - $70.45
https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/CamelotRelayer.sol#L20 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/UniV3Relayer.sol#L18 https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/oracles/CamelotRelayer.sol#L41-L42
Testing addresses for Camelot and UniswapV3 factories are still used in the code:
address internal constant _CAMELOT_FACTORY = GOERLI_CAMELOT_V3_FACTORY;
address internal constant _UNI_V3_FACTORY = GOERLI_UNISWAP_V3_FACTORY;
Additionally, the correct interface to get the camelotPair
is commented out:
// camelotPair = ICamelotFactory(_CAMELOT_FACTORY).getPair(_baseToken, _quoteToken); camelotPair = IAlgebraFactory(_CAMELOT_FACTORY).poolByPair(_baseToken, _quoteToken);
It will prevent integration with UniswapV3 and Camelot.
Manual Review
Make the changes necessary to be compatible with Arbitrum One.
Error
#0 - c4-pre-sort
2023-10-26T04:40:05Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T04:40:34Z
raymondfam marked the issue as low quality report
#2 - c4-pre-sort
2023-10-26T04:40:56Z
raymondfam marked the issue as duplicate of #119
#3 - c4-judge
2023-11-02T06:21:17Z
MiloTruck marked the issue as selected for report
#4 - MiloTruck
2023-11-02T06:23:47Z
While the protocol team is most likely aware of this and these addresses are probably set to testnet ones currently for testing purposes, it is an undeniable fact that if the current code was to be deployed without any modifications, the CamelotRelayer
and UniV3Relayer
contracts would not function as expected.
As such, I believe medium severity is appropriate.
#5 - c4-judge
2023-11-02T08:46:41Z
MiloTruck marked the issue as satisfactory
#6 - pi0neerpat
2023-11-10T07:09:21Z
The problem stated is that the Camelot and Uniswap factory addresses set in the Registry are set to Goerli addresses, not Mainnet. In production, we will swap Goerli addresses for Mainnet. Perhaps there is a better way to manage this than manually changing the constant variable in the Registry, however the recommendation does provide a helpful suggestion.
#7 - mcgrathcoutinho
2023-11-10T20:03:07Z
Hi @MiloTruck, I believe this issue is QA/Low-severity at best since the team is clearly aware of this and has intentionally used Goerli addresses for testnet purposes. Additionally as the sponsor mentioned above In production, we will swap Goerli addresses for Mainnet
further clarifies this. The only advice/value provided to the sponsor is to not hardcode the addresses and use another way to manage setting these addresses.
#8 - MiloTruck
2023-11-11T01:54:40Z
Hi @mcgrathcoutinho, all findings have to judged with the assumption that the code in-scope will be deployed without modifications, unless stated otherwise by the sponsor beforehand in the contest's README.
While I do agree that the sponsor was probably aware of this, this issue is still a valid medium severity bug according to C4 rules.
#9 - daopunk
2023-11-13T20:46:49Z
Okay, agreed. Although we were previously aware of this issue, we did not explicitly state that these variables would be updated before mainnet deployment, so therefore it is a valid find. Since this issue would prevent the liquidity pool relayers from functioning and prevent a price reading, it seems fair to label it medium severity.
π Selected for report: lsaudit
Also found by: 0xPsuedoPandit, 0xhacksmithh, Stormreckson, ZanyBonzy, klau5, tnquanghuy0512, twicek, xAriextz
81.7698 USDC - $81.77
Judge has assessed an item in Issue #300 as 2 risk. The relevant finding follows:
[L-01] transferSAFEOwnership does not reset users allowed to modify a safe on behalf of an owner transferSAFEOwnership is not deleting potential allowed users in safeCan upon transferring a safe ownership. This could very much be an intended design (keeping in βmemoryβ the allowed users (delegatees) for a given owner), in case an owner wants to send an NFT-Vault and receive it back, the allowed users would remain the same. But it would also mean that if a NFT-Vault owner allows Bob to execute actions on his safe, then send his NFT to someone and wait 1 year before getting it back, he will still allow Bob automatically after receiving the NFT which could be detrimental if the owner does not trust Bob anymore.
ODSafeManager.sol#L136-L152
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; emit TransferSAFEOwnership(msg.sender, _safe, _dst);
#0 - c4-judge
2023-11-03T17:06:50Z
MiloTruck marked the issue as duplicate of #142
#1 - c4-judge
2023-11-03T17:06:55Z
MiloTruck marked the issue as satisfactory
π Selected for report: MrPotatoMagic
Also found by: 0xMosh, 0xPsuedoPandit, 0xhacksmithh, 8olidity, Al-Qa-qa, Baki, Bughunter101, Krace, Stormreckson, T1MOH, Tendency, eeshenggoh, fibonacci, hals, immeas, kutugu, lsaudit, m4k2, mrudenko, okolicodes, phoenixV110, spark, twicek, xAriextz
8.3007 USDC - $8.30
transferSAFEOwnership
does not reset users allowed to modify a safe on behalf of an ownertransferSAFEOwnership
is not deleting potential allowed users in safeCan
upon transferring a safe ownership. This could very much be an intended design (keeping in "memory" the allowed users (delegatees) for a given owner), in case an owner wants to send an NFT-Vault and receive it back, the allowed users would remain the same. But it would also mean that if a NFT-Vault owner allows Bob to execute actions on his safe, then send his NFT to someone and wait 1 year before getting it back, he will still allow Bob automatically after receiving the NFT which could be detrimental if the owner does not trust Bob anymore.
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; emit TransferSAFEOwnership(msg.sender, _safe, _dst); }
ODGovernor
contract does not allow for cancelling proposalsODGovernor
does not expose _cancel
externally nor calls it internally, therefore it will not be possible to cancel proposals in the current implementation. However, I would advise great care if modifications are made to allow canceling proposals in the future as it could lead to this issue.
function _cancel( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal override(Governor, GovernorTimelockControl) returns (uint256) { return super._cancel(targets, values, calldatas, descriptionHash); }
auctionSurplus
The following check in auctionSurplus
is redundant:
AccountingEngine.sol#L225
if (extraSurplusReceiver == address(0)) revert AccEng_NullSurplusReceiver();
It is already done at the beginning of the function:
AccountingEngine.sol#L198-L202
function auctionSurplus() external returns (uint256 _id) { if(_params.surplusTransferPercentage > WAD) revert AccEng_surplusTransferPercentOverLimit(); if (_params.surplusAmount == 0) revert AccEng_NullAmount(); if (extraSurplusReceiver == address(0)) revert AccEng_NullSurplusReceiver(); if (block.timestamp < lastSurplusTime + _params.surplusDelay) revert AccEng_SurplusCooldown();
This condition ODProxy(_userRegistry[_user]).OWNER() != _user
in the following code will never happen because the only way to fill the _userRegistry[_user]
is via _build
and in this function ODProxy
is correctly initialized with _user
.
Vault721.sol#L154-L156
function _isNotProxy(address _user) internal view returns (bool) { return _userRegistry[_user] == address(0) || ODProxy(_userRegistry[_user]).OWNER() != _user; }
#0 - c4-pre-sort
2023-10-27T00:40:33Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-11-03T16:41:02Z
MiloTruck marked the issue as grade-b