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: 4/77
Findings: 4
Award: $2,455.71
π Selected for report: 2
π Solo Findings: 1
π Selected for report: MrPotatoMagic
Also found by: Bughunter101, COSMIC-BEE-REACH, HChang26, Stormreckson, T1MOH, Tendency, hals, josephdara, klau5, merlin, tnquanghuy0512, twcctop
18.5709 USDC - $18.57
Judge has assessed an item in Issue #165 as 2 risk. The relevant finding follows:
Allowed user have too much priviledge Links to affected code https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/ODSafeManager.sol#L105
Impact Allowed user can revoke approval of other allowed address.
Proof of Concept The allowed user can allow other user. If allowed address is compromised, it can cancel approval of other allowed user.
modifier safeAllowed(uint256 _safe) { address _owner = _safeData[_safe].owner; if (msg.sender != _owner && safeCan[_owner][_safe][msg.sender] == 0) revert SafeNotAllowed(); _; }
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); } Tools Used Manual Review
Recommended Mitigation Steps Restrict only the owner to allowSAFE.
#0 - c4-judge
2023-11-03T16:59:44Z
MiloTruck marked the issue as duplicate of #171
#1 - c4-judge
2023-11-03T16:59:49Z
MiloTruck marked the issue as partial-50
#2 - MiloTruck
2023-11-03T17:00:06Z
Partial credit as the explanation of the finding isn't very clear
π Selected for report: klau5
2222.4853 USDC - $2,222.49
If src has less lockedCollateral and generatedDebt than safeHandler, enterSystem
cannot succeed.
The enterSystem
function is behaving differently than intended.
The ODSafeManager.enterSystem
function retrieves the amount of safeHandler's lockedCollateral
and generatedDebt
and sends it to safeHandler. The purpose of this function is to send _src's lockedCollateral
and generatedDebt
to safeHandler, so it is sending wrong amounts.
function enterSystem(address _src, uint256 _safe) external handlerAllowed(_src) safeAllowed(_safe) { SAFEData memory _sData = _safeData[_safe]; @> ISAFEEngine.SAFE memory _safeInfo = ISAFEEngine(safeEngine).safes(_sData.collateralType, _sData.safeHandler); @> int256 _deltaCollateral = _safeInfo.lockedCollateral.toInt(); @> int256 _deltaDebt = _safeInfo.generatedDebt.toInt(); ISAFEEngine(safeEngine).transferSAFECollateralAndDebt( @> _sData.collateralType, _src, _sData.safeHandler, _deltaCollateral, _deltaDebt ); emit EnterSystem(msg.sender, _src, _safe); }
Manual Review
Use lockedCollateral
and generatedDebt
of _src.
function enterSystem(address _src, uint256 _safe) external handlerAllowed(_src) safeAllowed(_safe) { SAFEData memory _sData = _safeData[_safe]; + ISAFEEngine.SAFE memory _safeInfo = ISAFEEngine(safeEngine).safes(_sData.collateralType, _src); - ISAFEEngine.SAFE memory _safeInfo = ISAFEEngine(safeEngine).safes(_sData.collateralType, _sData.safeHandler); int256 _deltaCollateral = _safeInfo.lockedCollateral.toInt(); int256 _deltaDebt = _safeInfo.generatedDebt.toInt(); ISAFEEngine(safeEngine).transferSAFECollateralAndDebt( _sData.collateralType, _src, _sData.safeHandler, _deltaCollateral, _deltaDebt ); emit EnterSystem(msg.sender, _src, _safe); }
Other
#0 - c4-pre-sort
2023-10-26T03:56:58Z
raymondfam marked the issue as low quality report
#1 - c4-pre-sort
2023-10-26T03:57:17Z
raymondfam marked the issue as primary issue
#2 - raymondfam
2023-10-26T03:57:27Z
Insufficient proof.
#3 - c4-judge
2023-11-02T19:23:03Z
MiloTruck marked the issue as selected for report
#4 - c4-judge
2023-11-02T19:23:07Z
MiloTruck marked the issue as satisfactory
#5 - MiloTruck
2023-11-02T19:25:12Z
The warden has demonstrated how enterSystem()
calls transferSAFECollateralAndDebt()
with collateral and debt amounts of the wrong safe handler, causing its functionality to be broken. As such, I agree with medium severity.
#6 - pi0neerpat
2023-11-10T07:11:04Z
This appears to be accurate, as the collateral and debt amounts are being recorded from the destination address instead of the source address.
π Selected for report: klau5
Also found by: 0x6d6164616e, Arz, T1MOH, immeas, josephdara, nican0r, tnquanghuy0512
132.876 USDC - $132.88
https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/src/contracts/oracles/CamelotRelayer.sol#L70 https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/src/contracts/oracles/CamelotRelayer.sol#L74 https://github.com/open-dollar/od-contracts/blob/v1.5.5-audit/src/contracts/oracles/CamelotRelayer.sol#L93
The price lookup feature of the CamelotRelayer contract does not work.
The CamelotRelayer contract uses Uniswap V3's OracleLibrary to retrieve price information from Camelot pool. However, Camelot pool does not have the exact same interface as Uniswap V3, so the transaction will be reverted
The getResultWithValidity
function of the CamelotRelayer contract calls OracleLibrary.getOldestObservationSecondsAgo
and OracleLibrary.consult
. And read
function of the CamelotRelayer contract calls OracleLibrary.consult
.
function getResultWithValidity() external view returns (uint256 _result, bool _validity) { // If the pool doesn't have enough history return false @> if (OracleLibrary.getOldestObservationSecondsAgo(camelotPair) < quotePeriod) { return (0, false); } // Consult the query with a TWAP period of quotePeriod @> (int24 _arithmeticMeanTick,) = OracleLibrary.consult(camelotPair, quotePeriod); // Calculate the quote amount uint256 _quoteAmount = OracleLibrary.getQuoteAtTick({ tick: _arithmeticMeanTick, baseAmount: baseAmount, baseToken: baseToken, quoteToken: quoteToken }); // Process the quote result to 18 decimal quote _result = _parseResult(_quoteAmount); _validity = true; } function read() external view returns (uint256 _result) { // This call may revert with 'OLD!' if the pool doesn't have enough cardinality or initialized history @> (int24 _arithmeticMeanTick,) = OracleLibrary.consult(camelotPair, quotePeriod); uint256 _quoteAmount = OracleLibrary.getQuoteAtTick({ tick: _arithmeticMeanTick, baseAmount: baseAmount, baseToken: baseToken, quoteToken: quoteToken }); _result = _parseResult(_quoteAmount); }
This is the OracleLibrary code from Uniswap V3. The getOldestObservationSecondsAgo
function calls the pool contract's slot0
, observations
. The consult
function calls the observe
function of pool contract.
function getOldestObservationSecondsAgo(address pool) internal view returns (uint32 secondsAgo) { @> (, , uint16 observationIndex, uint16 observationCardinality, , , ) = IUniswapV3Pool(pool).slot0(); require(observationCardinality > 0, 'NI'); (uint32 observationTimestamp, , , bool initialized) = @> IUniswapV3Pool(pool).observations((observationIndex + 1) % observationCardinality); // The next index might not be initialized if the cardinality is in the process of increasing // In this case the oldest observation is always in index 0 if (!initialized) { @> (observationTimestamp, , , ) = IUniswapV3Pool(pool).observations(0); } secondsAgo = uint32(block.timestamp) - observationTimestamp; } function consult(address pool, uint32 secondsAgo) internal view returns (int24 arithmeticMeanTick, uint128 harmonicMeanLiquidity) { require(secondsAgo != 0, 'BP'); uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = secondsAgo; secondsAgos[1] = 0; (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = @> IUniswapV3Pool(pool).observe(secondsAgos); ... }
However, the functions slot0
, observations
, and observe
do not exist in Camelot's Pool. I found Camelot's pool through CAMELOT_V3_FACTORY (0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B) at Registry.s.sol.
If you call poolDeployer
view function at CAMELOT_V3_FACTORY, it returns 0x6Dd3FB9653B10e806650F107C3B5A0a6fF974F65, and you can see the Pool code in this contract. https://arbiscan.io/address/0x6dd3fb9653b10e806650f107c3b5a0a6ff974f65#code
Manual Review
Donβt use Uniswap V3 OracleLibrary for Camelot. You need to implement your own logic to communicate with the Camelot pool. These are functions/variables in Camelot pool that are similar to Uniswap V3 Poolβs.
slot0
β globalState
: https://arbiscan.io/address/0x6dd3fb9653b10e806650f107c3b5a0a6ff974f65#code#F15#L24observations
β timepoints
: https://arbiscan.io/address/0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B#code#F3#L22
createPool
and attached at new pool. https://arbiscan.io/address/0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B#code#F1#L67observe
β getTimepoints
https://arbiscan.io/address/0x6dd3fb9653b10e806650f107c3b5a0a6ff974f65#code#F3#L176Other
#0 - c4-pre-sort
2023-10-26T03:52:32Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T03:52:54Z
raymondfam marked the issue as duplicate of #75
#2 - c4-judge
2023-11-02T04:49:06Z
MiloTruck marked the issue as selected for report
#3 - MiloTruck
2023-11-02T04:52:44Z
The warden has demonstrated how the implementation of CamelotRelayer.sol
is incompatible with Camelot pools, thereby breaking the functionality of the contract. Since there isn't any loss of funds, medium severity is appropriate.
This report was selected as best since its recommended mitigation is the most detailed.
#4 - c4-judge
2023-11-02T08:46:11Z
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
Judge has assessed an item in Issue #165 as 2 risk. The relevant finding follows:
Clear safeCan in transferSAFEOwnership Links to affected code https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/ODSafeManager.sol#L136
Impact Old approval remains even if user gets SAFE again.
Proof of Concept There is no removal safeCan at transferSAFEOwnership . When the user gets SAFE again, the old approval is still remained.
modifier safeAllowed(uint256 _safe) { address _owner = _safeData[_safe].owner; if (msg.sender != _owner && safeCan[_owner][_safe][msg.sender] == 0) revert SafeNotAllowed(); _; }
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);
} Tools Used Manual Review
Recommended Mitigation Steps Delete safeCan at transferSAFEOwnership . You might use Set instead of mapping.
#0 - c4-judge
2023-11-03T17:00:52Z
MiloTruck marked the issue as duplicate of #142
#1 - c4-judge
2023-11-03T17:01:09Z
MiloTruck marked the issue as satisfactory