Platform: Code4rena
Start Date: 15/03/2024
Pot Size: $60,500 USDC
Total HM: 16
Participants: 43
Period: 21 days
Judge: hansfriese
Total Solo HM: 5
Id: 348
League: ETH
Rank: 13/43
Findings: 2
Award: $428.40
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: hihen
Also found by: 0xSecuri, 0xbepresent, Bigsam, Bube, Infect3d, Pechenite, Rolezn, Topmark, albahaca, caglankaan, cheatc0d3, klau5, nonseodion, oualidpro, pfapostol, serial-coder, slvDev
191.7284 USDC - $191.73
Issue | Instances | |
---|---|---|
[L001] | Array lengths not checked | 1 |
[L002] | Unsafe downcast | 7 |
[L003] | No limits when setting state variable amounts | 4 |
[L004] | Double type casts create complexity within the code | 1 |
[L005] | Constructor contains no validation | 2 |
[L006] | For loops in public or external functions should be avoided due to high gas costs and possible DOS | 3 |
[L007] | Function calls within for loops | 2 |
[L008] | Multiplication on the result of a division | 3 |
[L009] | Missing checks for address(0x0) in the constructor | 3 |
[L010] | Prefer continue over revert model in iteration | 2 |
If the length of the arrays are not required to be of the same length, user operations may not be fully executed due to a mismatch in the number of items iterated over, versus the number of items provided in the second array.
File: facets/ShortOrdersFacet.sol 35 function createLimitShort( 36 address asset, 37 uint80 price, 38 uint88 ercAmount, 39 MTypes.OrderHint[] memory orderHintArray, 40 uint16[] memory shortHintArray, 41 uint16 shortOrderCR 42 ) external isNotFrozen(asset) onlyValidAsset(asset) nonReentrant { 43 MTypes.CreateLimitShortParam memory p; 44 STypes.Asset storage Asset = s.asset[asset]; 45 STypes.Order memory incomingShort; 46 47 // @dev create "empty" SR. Fail early. 48 incomingShort.shortRecordId = LibShortRecord.createShortRecord(asset, msg.sender, SR.Closed, 0, 0, 0, 0, 0); 49 50 uint256 cr = LibOrders.convertCR(shortOrderCR); 51 if ((shortOrderCR + C.BID_CR) < Asset.initialCR || cr >= C.CRATIO_MAX_INITIAL) { 52 revert Errors.InvalidCR(); 53 } 54 55 // @dev minShortErc needs to be modified (bigger) when cr < 1 56 p.minShortErc = cr < 1 ether ? LibAsset.minShortErc(asset).mul(1 ether + cr.inv()) : LibAsset.minShortErc(asset); 57 p.eth = price.mul(ercAmount); 58 p.minAskEth = LibAsset.minAskEth(asset); 59 if (ercAmount < p.minShortErc || p.eth < p.minAskEth) revert Errors.OrderUnderMinimumSize(); 60 61 // For a short, need enough collateral to cover minting ERC (calculated using initialCR) 62 if (s.vaultUser[Asset.vault][msg.sender].ethEscrowed < p.eth.mul(cr)) revert Errors.InsufficientETHEscrowed(); 63 64 incomingShort.addr = msg.sender; 65 incomingShort.price = price; 66 incomingShort.ercAmount = ercAmount; 67 incomingShort.id = Asset.orderIdCounter; 68 incomingShort.orderType = O.LimitShort; 69 incomingShort.creationTime = LibOrders.getOffsetTime(); 70 incomingShort.shortOrderCR = shortOrderCR; // 170 -> 1.70x 71 72 p.startingId = s.bids[asset][C.HEAD].nextId; 73 STypes.Order storage highestBid = s.bids[asset][p.startingId]; 74 // @dev if match and match price is gt .5% to saved oracle in either direction, update startingShortId 75 if (highestBid.price >= incomingShort.price && highestBid.orderType == O.LimitBid) { 76 LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingShort, shortHintArray); 77 } 78 79 p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(asset); 80 if (LibSRUtil.checkRecoveryModeViolation(asset, cr, p.oraclePrice)) { 81 revert Errors.BelowRecoveryModeCR(); 82 } 83 84 // @dev reading spot oracle price 85 if (incomingShort.price < p.oraclePrice) { 86 LibOrders.addShort(asset, incomingShort, orderHintArray); 87 } else { 88 LibOrders.sellMatchAlgo(asset, incomingShort, orderHintArray, p.minAskEth); 89 } 90 }
When a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs.
<details> <summary>Click to show 7 findings</summary>File: facets/BridgeRouterFacet.sol 87 uint88 dethAmount = uint88(IBridge(bridge).depositEth{value: msg.value}()); // Assumes 1 ETH = 1 DETH 68 uint88 dethAmount = uint88(IBridge(bridge).deposit(msg.sender, amount)); // @dev(safe-cast)
File: facets/PrimaryLiquidationFacet.sol 231 return a < b ? uint88(a) : b;
File: libraries/LibOracle.sol 151 s.bids[asset][C.HEAD].ercAmount = uint80(oraclePrice); 164 return uint80(s.bids[asset][C.HEAD].ercAmount);
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L164:164
File: libraries/LibOrders.sol 903 uint88 minShortErc = uint88(LibAsset.minShortErc(asset));
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L903:903
</details>File: libraries/UniswapOracleLibrary.sol 62 int24 tick = int24(tickCumulativesDelta / int32(secondsAgo));
It is important to ensure state variables numbers are set to a reasonable value.
<details> <summary>Click to show 4 findings</summary>File: libraries/LibOracle.sol 152 s.bids[asset][C.HEAD].creationTime = oracleTime;
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L152:152
File: libraries/LibOrders.sol 334 orders[asset][C.HEAD].prevId = id; 488 orders[asset][C.HEAD].nextId = id; 542 orders[asset][nextAskId].prevId = headId;
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L542:542
</details>Double type casting should be avoided in Solidity contracts to prevent unintended consequences and ensure accurate data representation. Performing multiple type casts in succession can lead to unexpected truncation, rounding errors, or loss of precision, potentially compromising the contract's functionality and reliability. Furthermore, double type casting can make the code less readable and harder to maintain, increasing the likelihood of errors and misunderstandings during development and debugging. To ensure precise and consistent data handling, developers should use appropriate data types and avoid unnecessary or excessive type casting, promoting a more robust and dependable contract execution.
File: facets/RedemptionFacet.sol bytes8(uint64(p.currentCR)),
In Solidity, when values are being assigned in constructors to unsigned or integer variables, it's crucial to ensure the provided values adhere to the protocol's specific operational boundaries as laid out in the project specifications and documentation. If the constructors lack appropriate validation checks, there's a risk of setting state variables with values that could cause unexpected and potentially detrimental behavior within the contract's operations, violating the intended logic of the protocol. This can compromise the contract's security and impact the maintainability and reliability of the system. In order to avoid such issues, it is recommended to incorporate rigorous validation checks in constructors. These checks should align with the project's defined rules and constraints, making use of Solidity's built-in require function to enforce these conditions. If the validation checks fail, the require function will cause the transaction to revert, ensuring the integrity and adherence to the protocol's expected behavior.
File: facets/BridgeRouterFacet.sol 29 constructor(address _rethBridge, address _stethBridge) { 30 rethBridge = _rethBridge; 31 stethBridge = _stethBridge; 32 }
File: facets/PrimaryLiquidationFacet.sol 30 constructor(address _dusd) { 31 dusd = _dusd; 32 }
public
or external
functions should be avoided due to high gas costs and possible DOS:In Solidity, for loops can potentially cause Denial of Service (DoS) attacks if not handled carefully. DoS attacks can occur when an attacker intentionally exploits the gas cost of a function, causing it to run out of gas or making it too expensive for other users to call. Below are some scenarios where for loops can lead to DoS attacks: Nested for loops can become exceptionally gas expensive and should be used sparingly.
File: facets/RedemptionFacet.sol 56 function proposeRedemption( 57 address asset, 58 MTypes.ProposalInput[] calldata proposalInput, 59 uint88 redemptionAmount, 60 uint88 maxRedemptionFee 61 ) external isNotFrozen(asset) nonReentrant { 62 if (proposalInput.length > type(uint8).max) revert Errors.TooManyProposals(); 63 MTypes.ProposeRedemption memory p; 64 p.asset = asset; 65 STypes.AssetUser storage redeemerAssetUser = s.assetUser[p.asset][msg.sender]; 66 uint256 minShortErc = LibAsset.minShortErc(p.asset); 67 68 if (redemptionAmount < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 69 70 if (redeemerAssetUser.ercEscrowed < redemptionAmount) revert Errors.InsufficientERCEscrowed(); 71 72 // @dev redeemerAssetUser.SSTORE2Pointer gets reset to address(0) after actual redemption 73 if (redeemerAssetUser.SSTORE2Pointer != address(0)) revert Errors.ExistingProposedRedemptions(); 74 75 p.oraclePrice = LibOracle.getPrice(p.asset); 76 77 bytes memory slate; 78 for (uint8 i = 0; i < proposalInput.length; i++) { 79 p.shorter = proposalInput[i].shorter; 80 p.shortId = proposalInput[i].shortId; 81 p.shortOrderId = proposalInput[i].shortOrderId; 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 84 85 /// Evaluate proposed shortRecord 86 87 if (!validRedemptionSR(currentSR, msg.sender, p.shorter, minShortErc)) continue; 88 89 currentSR.updateErcDebt(p.asset); 90 p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); 91 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 93 if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; 94 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 96 if (p.totalAmountProposed + currentSR.ercDebt <= redemptionAmount) { 97 p.amountProposed = currentSR.ercDebt; 98 } else { 99 p.amountProposed = redemptionAmount - p.totalAmountProposed; 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 101 if (currentSR.ercDebt - p.amountProposed < minShortErc) break; 102 } 103 104 /// At this point, the shortRecord passes all checks and will be included in the slate 105 106 p.previousCR = p.currentCR; 107 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 110 STypes.Order storage shortOrder = s.shorts[asset][p.shortOrderId]; 111 if (currentSR.status == SR.PartialFill && shortOrder.ercAmount < minShortErc) { 112 if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); 113 LibOrders.cancelShort(asset, p.shortOrderId); 114 } 115 116 p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed); 117 if (p.colRedeemed > currentSR.collateral) { 118 p.colRedeemed = currentSR.collateral; 119 } 120 121 currentSR.collateral -= p.colRedeemed; 122 currentSR.ercDebt -= p.amountProposed; 123 124 p.totalAmountProposed += p.amountProposed; 125 p.totalColRedeemed += p.colRedeemed; 126 127 // @dev directly write the properties of MTypes.ProposalData into bytes 128 // instead of usual abi.encode to save on extra zeros being written 129 slate = bytes.concat( 130 slate, 131 bytes20(p.shorter), 132 bytes1(p.shortId), 133 bytes8(uint64(p.currentCR)), 134 bytes11(p.amountProposed), 135 bytes11(p.colRedeemed) 136 ); 137 138 LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); 139 p.redemptionCounter++; 140 if (redemptionAmount - p.totalAmountProposed < minShortErc) break; 141 } 142 143 if (p.totalAmountProposed < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 144 145 // @dev SSTORE2 the entire proposalData after validating proposalInput 146 redeemerAssetUser.SSTORE2Pointer = SSTORE2.write(slate); 147 redeemerAssetUser.slateLength = p.redemptionCounter; 148 redeemerAssetUser.oraclePrice = p.oraclePrice; 149 redeemerAssetUser.ercEscrowed -= p.totalAmountProposed; 150 151 STypes.Asset storage Asset = s.asset[p.asset]; 152 Asset.ercDebt -= p.totalAmountProposed; 153 154 uint32 protocolTime = LibOrders.getOffsetTime(); 155 redeemerAssetUser.timeProposed = protocolTime; 156 // @dev Calculate the dispute period 157 // @dev timeToDispute is immediate for shorts with CR <= 1.1x 158 159 /* 160 +-------+------------+ 161 | CR(X) | Hours(Y) | 162 +-------+------------+ 163 | 1.1 | 0 | 164 | 1.2 | .333 | 165 | 1.3 | .75 | 166 | 1.5 | 1.5 | 167 | 1.7 | 3 | 168 | 2.0 | 6 | 169 +-------+------------+ 170 171 Creating fixed points and interpolating between points on the graph without using exponentials 172 Using simple y = mx + b formula 173 174 where x = currentCR - previousCR 175 m = (y2-y1)/(x2-x1) 176 b = previous fixed point (Y) 177 */ 178 179 uint256 m; 180 181 if (p.currentCR > 1.7 ether) { 182 m = uint256(3 ether).div(0.3 ether); 183 redeemerAssetUser.timeToDispute = protocolTime + uint32((m.mul(p.currentCR - 1.7 ether) + 3 ether) * 1 hours / 1 ether); 184 } else if (p.currentCR > 1.5 ether) { 185 m = uint256(1.5 ether).div(0.2 ether); 186 redeemerAssetUser.timeToDispute = 187 protocolTime + uint32((m.mul(p.currentCR - 1.5 ether) + 1.5 ether) * 1 hours / 1 ether); 188 } else if (p.currentCR > 1.3 ether) { 189 m = uint256(0.75 ether).div(0.2 ether); 190 redeemerAssetUser.timeToDispute = 191 protocolTime + uint32((m.mul(p.currentCR - 1.3 ether) + 0.75 ether) * 1 hours / 1 ether); 192 } else if (p.currentCR > 1.2 ether) { 193 m = uint256(0.417 ether).div(0.1 ether); 194 redeemerAssetUser.timeToDispute = 195 protocolTime + uint32((m.mul(p.currentCR - 1.2 ether) + C.ONE_THIRD) * 1 hours / 1 ether); 196 } else if (p.currentCR > 1.1 ether) { 197 m = uint256(C.ONE_THIRD.div(0.1 ether)); 198 redeemerAssetUser.timeToDispute = protocolTime + uint32(m.mul(p.currentCR - 1.1 ether) * 1 hours / 1 ether); 199 } 200 201 redeemerAssetUser.oraclePrice = p.oraclePrice; 202 redeemerAssetUser.timeProposed = LibOrders.getOffsetTime(); 203 204 uint88 redemptionFee = calculateRedemptionFee(asset, p.totalColRedeemed, p.totalAmountProposed); 205 if (redemptionFee > maxRedemptionFee) revert Errors.RedemptionFeeTooHigh(); 206 207 STypes.VaultUser storage VaultUser = s.vaultUser[Asset.vault][msg.sender]; 208 if (VaultUser.ethEscrowed < redemptionFee) revert Errors.InsufficientETHEscrowed(); 209 VaultUser.ethEscrowed -= redemptionFee; 210 emit Events.ProposeRedemption(p.asset, msg.sender); 211 } 224 function disputeRedemption(address asset, address redeemer, uint8 incorrectIndex, address disputeShorter, uint8 disputeShortId) 225 external 226 isNotFrozen(asset) 227 nonReentrant 228 { 229 if (redeemer == msg.sender) revert Errors.CannotDisputeYourself(); 230 MTypes.DisputeRedemption memory d; 231 d.asset = asset; 232 d.redeemer = redeemer; 233 234 STypes.AssetUser storage redeemerAssetUser = s.assetUser[d.asset][d.redeemer]; 235 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 236 237 if (LibOrders.getOffsetTime() >= redeemerAssetUser.timeToDispute) revert Errors.TimeToDisputeHasElapsed(); 238 239 MTypes.ProposalData[] memory decodedProposalData = 240 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, redeemerAssetUser.slateLength); 241 242 for (uint256 i = 0; i < decodedProposalData.length; i++) { 243 if (decodedProposalData[i].shorter == disputeShorter && decodedProposalData[i].shortId == disputeShortId) { 244 revert Errors.CannotDisputeWithRedeemerProposal(); 245 } 246 } 247 248 STypes.ShortRecord storage disputeSR = s.shortRecords[d.asset][disputeShorter][disputeShortId]; 249 // Match continue (skip) conditions in proposeRedemption() 250 uint256 minShortErc = LibAsset.minShortErc(d.asset); 251 if (!validRedemptionSR(disputeSR, d.redeemer, disputeShorter, minShortErc)) revert Errors.InvalidRedemption(); 252 253 MTypes.ProposalData memory incorrectProposal = decodedProposalData[incorrectIndex]; 254 MTypes.ProposalData memory currentProposal; 255 STypes.Asset storage Asset = s.asset[d.asset]; 256 257 uint256 disputeCR = disputeSR.getCollateralRatio(redeemerAssetUser.oraclePrice); 258 259 if (disputeCR < incorrectProposal.CR && disputeSR.updatedAt + C.DISPUTE_REDEMPTION_BUFFER <= redeemerAssetUser.timeProposed) 260 { 261 // @dev All proposals from the incorrectIndex onward will be removed 262 // @dev Thus the proposer can only redeem a portion of their original slate 263 for (uint256 i = incorrectIndex; i < decodedProposalData.length; i++) { 264 currentProposal = decodedProposalData[i]; 265 266 STypes.ShortRecord storage currentSR = s.shortRecords[d.asset][currentProposal.shorter][currentProposal.shortId]; 267 currentSR.collateral += currentProposal.colRedeemed; 268 currentSR.ercDebt += currentProposal.ercDebtRedeemed; 269 270 d.incorrectCollateral += currentProposal.colRedeemed; 271 d.incorrectErcDebt += currentProposal.ercDebtRedeemed; 272 } 273 274 s.vault[Asset.vault].dethCollateral += d.incorrectCollateral; 275 Asset.dethCollateral += d.incorrectCollateral; 276 Asset.ercDebt += d.incorrectErcDebt; 277 278 // @dev Update the redeemer's SSTORE2Pointer 279 if (incorrectIndex > 0) { 280 redeemerAssetUser.slateLength = incorrectIndex; 281 } else { 282 // @dev this implies everything in the redeemer's proposal was incorrect 283 delete redeemerAssetUser.SSTORE2Pointer; 284 emit Events.DisputeRedemptionAll(d.asset, redeemer); 285 } 286 287 // @dev Penalty is based on the proposal with highest CR (decodedProposalData is sorted) 288 // @dev PenaltyPct is bound between CallerFeePct and 33% to prevent exploiting primaryLiquidation fees 289 uint256 penaltyPct = LibOrders.min( 290 LibOrders.max(LibAsset.callerFeePct(d.asset), (currentProposal.CR - disputeCR).div(currentProposal.CR)), 0.33 ether 291 ); 292 293 uint88 penaltyAmt = d.incorrectErcDebt.mulU88(penaltyPct); 294 295 // @dev Give redeemer back ercEscrowed that is no longer used to redeem (penalty applied) 296 redeemerAssetUser.ercEscrowed += (d.incorrectErcDebt - penaltyAmt); 297 s.assetUser[d.asset][msg.sender].ercEscrowed += penaltyAmt; 298 } else { 299 revert Errors.InvalidRedemptionDispute(); 300 } 301 } 310 function claimRedemption(address asset) external isNotFrozen(asset) nonReentrant { 311 uint256 vault = s.asset[asset].vault; 312 STypes.AssetUser storage redeemerAssetUser = s.assetUser[asset][msg.sender]; 313 STypes.VaultUser storage redeemerVaultUser = s.vaultUser[vault][msg.sender]; 314 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 315 if (LibOrders.getOffsetTime() < redeemerAssetUser.timeToDispute) revert Errors.TimeToDisputeHasNotElapsed(); 316 317 MTypes.ProposalData[] memory decodedProposalData = 318 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, redeemerAssetUser.slateLength); 319 320 uint88 totalColRedeemed; 321 for (uint256 i = 0; i < decodedProposalData.length; i++) { 322 MTypes.ProposalData memory currentProposal = decodedProposalData[i]; 323 totalColRedeemed += currentProposal.colRedeemed; 324 _claimRemainingCollateral({ 325 asset: asset, 326 vault: vault, 327 shorter: currentProposal.shorter, 328 shortId: currentProposal.shortId 329 }); 330 } 331 redeemerVaultUser.ethEscrowed += totalColRedeemed; 332 delete redeemerAssetUser.SSTORE2Pointer; 333 emit Events.ClaimRedemption(asset, msg.sender); 334 }
Making function calls within loops in Solidity can lead to inefficient gas usage, potential bottlenecks, and increased vulnerability to attacks. Each function call or external call consumes gas, and when executed within a loop, the gas cost multiplies, potentially causing the transaction to run out of gas or exceed block gas limits. This can result in transaction failure or unpredictable behavior.
File: facets/RedemptionFacet.sol 321 for (uint256 i = 0; i < decodedProposalData.length; i++) { 322 MTypes.ProposalData memory currentProposal = decodedProposalData[i]; 323 totalColRedeemed += currentProposal.colRedeemed; 324 _claimRemainingCollateral({ 325 asset: asset, 326 vault: vault, 327 shorter: currentProposal.shorter, 328 shortId: currentProposal.shortId 329 }); 330 } 78 for (uint8 i = 0; i < proposalInput.length; i++) { 79 p.shorter = proposalInput[i].shorter; 80 p.shortId = proposalInput[i].shortId; 81 p.shortOrderId = proposalInput[i].shortOrderId; 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 84 85 /// Evaluate proposed shortRecord 86 87 if (!validRedemptionSR(currentSR, msg.sender, p.shorter, minShortErc)) continue; 88 89 currentSR.updateErcDebt(p.asset); 90 p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); 91 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 93 if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; 94 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 96 if (p.totalAmountProposed + currentSR.ercDebt <= redemptionAmount) { 97 p.amountProposed = currentSR.ercDebt; 98 } else { 99 p.amountProposed = redemptionAmount - p.totalAmountProposed; 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 101 if (currentSR.ercDebt - p.amountProposed < minShortErc) break; 102 } 103 104 /// At this point, the shortRecord passes all checks and will be included in the slate 105 106 p.previousCR = p.currentCR; 107 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 110 STypes.Order storage shortOrder = s.shorts[asset][p.shortOrderId]; 111 if (currentSR.status == SR.PartialFill && shortOrder.ercAmount < minShortErc) { 112 if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); 113 LibOrders.cancelShort(asset, p.shortOrderId); 114 } 115 116 p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed); 117 if (p.colRedeemed > currentSR.collateral) { 118 p.colRedeemed = currentSR.collateral; 119 } 120 121 currentSR.collateral -= p.colRedeemed; 122 currentSR.ercDebt -= p.amountProposed; 123 124 p.totalAmountProposed += p.amountProposed; 125 p.totalColRedeemed += p.colRedeemed; 126 127 // @dev directly write the properties of MTypes.ProposalData into bytes 128 // instead of usual abi.encode to save on extra zeros being written 129 slate = bytes.concat( 130 slate, 131 bytes20(p.shorter), 132 bytes1(p.shortId), 133 bytes8(uint64(p.currentCR)), 134 bytes11(p.amountProposed), 135 bytes11(p.colRedeemed) 136 ); 137 138 LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); 139 p.redemptionCounter++; 140 if (redemptionAmount - p.totalAmountProposed < minShortErc) break; 141 }
Dividing an integer by another integer will often result in loss of precision. When the result is multiplied by another number, the loss of precision is magnified, often to material magnitudes. (X / Z) * Y should be re-written as (X * Y) / Z.
File: libraries/LibOracle.sol 93 uint256 twapPriceNormalized = twapPrice * (1 ether / C.DECIMAL_USDC); 141 uint256 twapPriceNormalized = twapPrice * (1 ether / C.DECIMAL_USDC);
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L141:141
File: libraries/LibOrders.sol 47 uint88 shares = eth * (timeTillMatch / 1 days);
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L47:47
File: facets/BridgeRouterFacet.sol 30 rethBridge = _rethBridge; 31 stethBridge = _stethBridge;
File: facets/PrimaryLiquidationFacet.sol 31 dusd = _dusd;
Preferably, it's better to skip operations on array indices when a condition is not met instead of reverting the entire transaction. Reverting could be exploited by malicious actors who might intentionally introduce array objects failing conditional checks, disrupting group operations. It's advisable to skip array indices rather than revert, unless there are valid security or logic reasons for doing otherwise
File: facets/RedemptionFacet.sol 78 for (uint8 i = 0; i < proposalInput.length; i++) { 79 p.shorter = proposalInput[i].shorter; 80 p.shortId = proposalInput[i].shortId; 81 p.shortOrderId = proposalInput[i].shortOrderId; 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 84 85 /// Evaluate proposed shortRecord 86 87 if (!validRedemptionSR(currentSR, msg.sender, p.shorter, minShortErc)) continue; 88 89 currentSR.updateErcDebt(p.asset); 90 p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); 91 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 93 if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; 94 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 96 if (p.totalAmountProposed + currentSR.ercDebt <= redemptionAmount) { 97 p.amountProposed = currentSR.ercDebt; 98 } else { 99 p.amountProposed = redemptionAmount - p.totalAmountProposed; 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 101 if (currentSR.ercDebt - p.amountProposed < minShortErc) break; 102 } 103 104 /// At this point, the shortRecord passes all checks and will be included in the slate 105 106 p.previousCR = p.currentCR; 107 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 110 STypes.Order storage shortOrder = s.shorts[asset][p.shortOrderId]; 111 if (currentSR.status == SR.PartialFill && shortOrder.ercAmount < minShortErc) { 112 if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); 113 LibOrders.cancelShort(asset, p.shortOrderId); 114 } 115 116 p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed); 117 if (p.colRedeemed > currentSR.collateral) { 118 p.colRedeemed = currentSR.collateral; 119 } 120 121 currentSR.collateral -= p.colRedeemed; 122 currentSR.ercDebt -= p.amountProposed; 123 124 p.totalAmountProposed += p.amountProposed; 125 p.totalColRedeemed += p.colRedeemed; 126 127 // @dev directly write the properties of MTypes.ProposalData into bytes 128 // instead of usual abi.encode to save on extra zeros being written 129 slate = bytes.concat( 130 slate, 131 bytes20(p.shorter), 132 bytes1(p.shortId), 133 bytes8(uint64(p.currentCR)), 134 bytes11(p.amountProposed), 135 bytes11(p.colRedeemed) 136 ); 137 138 LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); 139 p.redemptionCounter++; 140 if (redemptionAmount - p.totalAmountProposed < minShortErc) break; 141 } 242 for (uint256 i = 0; i < decodedProposalData.length; i++) { 243 if (decodedProposalData[i].shorter == disputeShorter && decodedProposalData[i].shortId == disputeShortId) { 244 revert Errors.CannotDisputeWithRedeemerProposal(); 245 } 246 }
Issue | Instances | |
---|---|---|
[NC001] | The nonReentrant modifier should occur before all other modifiers | 6 |
[NC002] | Imports could be organized more systematically | 4 |
[NC003] | Constants in comparisons should appear on the left side | 41 |
[NC004] | else-block not required | 32 |
[NC005] | If-statement can be converted to a ternary | 4 |
[NC006] | Variable names that consist of all capital letters should be reserved for constant/immutable variables | 4 |
[NC007] | Consider using delete rather than assigning false/zero to clear values | 14 |
[NC008] | Variable names for immutable s should use CONSTANT_CASE | 3 |
[NC009] | Lines are too long | 53 |
[NC010] | File is missing NatSpec comments | 4 |
[NC011] | Function declarations should have NatSpec descriptions | 36 |
[NC012] | Contract declarations should have @notice tags | 10 |
[NC013] | Invalid NatSpec comment style | 93 |
[NC014] | Inconsistent spacing in comments | 2 |
[NC015] | Not using the named return variables anywhere in the function is confusing | 15 |
[NC016] | Contracts should have full test coverage | 1 |
[NC017] | Large or complicated code bases should implement invariant tests | 1 |
[NC018] | Consider using block.number instead of block.timestamp | 6 |
[NC019] | Consider bounding input array length | 2 |
[NC020] | Variables should be named in mixedCase style | 46 |
[NC021] | Function names should use lowerCamelCase | 3 |
[NC022] | Consider adding a deny-list | 4 |
[NC023] | Events are missing sender information | 1 |
[NC024] | Zero as a function argument should have a descriptive meaning | 23 |
[NC025] | It is standard for all external and public functions to be override from an interface | 12 |
[NC026] | Consider adding formal verification proofs | 1 |
[NC027] | Setters should prevent re-setting of the same value | 1 |
[NC028] | Consider splitting long calculations | 6 |
[NC029] | High cyclomatic complexity | 9 |
[NC030] | Unused import | 11 |
[NC031] | Unsafe conversion from unsigned to signed values | 2 |
[NC032] | Style guide: State and local variables should be named using lowerCamelCase | 82 |
[NC033] | Function definitions should have NatSpec @dev annotations | 57 |
[NC034] | Function definitions should have NatSpec @notice annotations | 55 |
[NC035] | Interface declarations should have NatSpec @title annotations | 1 |
[NC036] | Interface declarations should have NatSpec @author annotations | 1 |
[NC037] | Interface declarations should have NatSpec @notice annotations | 1 |
[NC038] | Interface declarations should have NatSpec @dev annotations | 1 |
[NC039] | Library declarations should have Natspec @title annotations | 5 |
[NC040] | Library declarations should have Natspec @author annotations | 6 |
[NC041] | Library declarations should have Natspec @notice annotations | 5 |
[NC042] | Library declarations should have Natspec @dev annotations | 6 |
[NC043] | Contract definitions should have Natspec @title annotations | 4 |
[NC044] | Contract definitions should have Natspec @author annotations | 4 |
[NC045] | Contract definitions should have Natspec @notice annotations | 4 |
[NC046] | Contract definitions should have Natspec @dev annotations | 4 |
[NC047] | State variable declarations should have Natspec @notice annotations | 3 |
[NC048] | State variable declarations should have Natspec @dev annotations | 3 |
[NC049] | Functions should have Natspec @return annotations | 31 |
[NC050] | Functions should have Natspec @param annotations | 54 |
[NC051] | Missing events in sensitive functions | 1 |
[NC052] | Unused file | 6 |
[NC053] | Consider only defining one library/interface/contract per sol file | 1 |
[NC054] | Use a struct to encapsulate multiple function parameters | 14 |
nonReentrant
modifier
should occur before all other modifiers:This is a best-practice to protect against reentrancy in other modifiers.
<details> <summary>Click to show 6 findings</summary>File: facets/PrimaryLiquidationFacet.sol 47 function liquidate(address asset, address shorter, uint8 id, uint16[] memory shortHintArray, uint16 shortOrderId) 48 external 49 isNotFrozen(asset) 50 nonReentrant 51 onlyValidShortRecord(asset, shorter, id) 52 returns (uint88, uint88) 53 { 54 if (msg.sender == shorter) revert Errors.CannotLiquidateSelf(); 55 // @dev TAPP partially reimburses gas fees, capped at 10 to limit arbitrary high cost 56 if (shortHintArray.length > 10) revert Errors.TooManyHints(); 57 58 // @dev Ensures SR has enough ercDebt/collateral to make caller fee worthwhile 59 LibSRUtil.checkCancelShortOrder({ 60 asset: asset, 61 initialStatus: s.shortRecords[asset][shorter][id].status, 62 shortOrderId: shortOrderId, 63 shortRecordId: id, 64 shorter: shorter 65 }); 66 67 // @dev liquidate requires more up-to-date oraclePrice 68 LibOrders.updateOracleAndStartingShortViaTimeBidOnly(asset, shortHintArray); 69 70 MTypes.PrimaryLiquidation memory m = _setLiquidationStruct(asset, shorter, id, shortOrderId); 71 72 // @dev Can liquidate as long as CR is low enough 73 if (m.cRatio >= LibAsset.liquidationCR(m.asset)) { 74 // If CR is too high, check for recovery mode and violation to continue liquidation 75 if (!LibSRUtil.checkRecoveryModeViolation(m.asset, m.cRatio, m.oraclePrice)) revert Errors.SufficientCollateral(); 76 } 77 78 // revert if no asks, or price too high 79 _checklowestSell(m); 80 81 _performForcedBid(m, shortHintArray); 82 83 _liquidationFeeHandler(m); 84 85 _fullorPartialLiquidation(m); 86 87 emit Events.Liquidate(asset, shorter, id, msg.sender, m.ercDebtMatched); 88 89 return (m.gasFee, m.ethFilled); 90 }
File: facets/RedemptionFacet.sol 56 function proposeRedemption( 57 address asset, 58 MTypes.ProposalInput[] calldata proposalInput, 59 uint88 redemptionAmount, 60 uint88 maxRedemptionFee 61 ) external isNotFrozen(asset) nonReentrant { 62 if (proposalInput.length > type(uint8).max) revert Errors.TooManyProposals(); 63 MTypes.ProposeRedemption memory p; 64 p.asset = asset; 65 STypes.AssetUser storage redeemerAssetUser = s.assetUser[p.asset][msg.sender]; 66 uint256 minShortErc = LibAsset.minShortErc(p.asset); 67 68 if (redemptionAmount < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 69 70 if (redeemerAssetUser.ercEscrowed < redemptionAmount) revert Errors.InsufficientERCEscrowed(); 71 72 // @dev redeemerAssetUser.SSTORE2Pointer gets reset to address(0) after actual redemption 73 if (redeemerAssetUser.SSTORE2Pointer != address(0)) revert Errors.ExistingProposedRedemptions(); 74 75 p.oraclePrice = LibOracle.getPrice(p.asset); 76 77 bytes memory slate; 78 for (uint8 i = 0; i < proposalInput.length; i++) { 79 p.shorter = proposalInput[i].shorter; 80 p.shortId = proposalInput[i].shortId; 81 p.shortOrderId = proposalInput[i].shortOrderId; 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 84 85 /// Evaluate proposed shortRecord 86 87 if (!validRedemptionSR(currentSR, msg.sender, p.shorter, minShortErc)) continue; 88 89 currentSR.updateErcDebt(p.asset); 90 p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); 91 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 93 if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; 94 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 96 if (p.totalAmountProposed + currentSR.ercDebt <= redemptionAmount) { 97 p.amountProposed = currentSR.ercDebt; 98 } else { 99 p.amountProposed = redemptionAmount - p.totalAmountProposed; 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 101 if (currentSR.ercDebt - p.amountProposed < minShortErc) break; 102 } 103 104 /// At this point, the shortRecord passes all checks and will be included in the slate 105 106 p.previousCR = p.currentCR; 107 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 110 STypes.Order storage shortOrder = s.shorts[asset][p.shortOrderId]; 111 if (currentSR.status == SR.PartialFill && shortOrder.ercAmount < minShortErc) { 112 if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); 113 LibOrders.cancelShort(asset, p.shortOrderId); 114 } 115 116 p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed); 117 if (p.colRedeemed > currentSR.collateral) { 118 p.colRedeemed = currentSR.collateral; 119 } 120 121 currentSR.collateral -= p.colRedeemed; 122 currentSR.ercDebt -= p.amountProposed; 123 124 p.totalAmountProposed += p.amountProposed; 125 p.totalColRedeemed += p.colRedeemed; 126 127 // @dev directly write the properties of MTypes.ProposalData into bytes 128 // instead of usual abi.encode to save on extra zeros being written 129 slate = bytes.concat( 130 slate, 131 bytes20(p.shorter), 132 bytes1(p.shortId), 133 bytes8(uint64(p.currentCR)), 134 bytes11(p.amountProposed), 135 bytes11(p.colRedeemed) 136 ); 137 138 LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); 139 p.redemptionCounter++; 140 if (redemptionAmount - p.totalAmountProposed < minShortErc) break; 141 } 142 143 if (p.totalAmountProposed < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 144 145 // @dev SSTORE2 the entire proposalData after validating proposalInput 146 redeemerAssetUser.SSTORE2Pointer = SSTORE2.write(slate); 147 redeemerAssetUser.slateLength = p.redemptionCounter; 148 redeemerAssetUser.oraclePrice = p.oraclePrice; 149 redeemerAssetUser.ercEscrowed -= p.totalAmountProposed; 150 151 STypes.Asset storage Asset = s.asset[p.asset]; 152 Asset.ercDebt -= p.totalAmountProposed; 153 154 uint32 protocolTime = LibOrders.getOffsetTime(); 155 redeemerAssetUser.timeProposed = protocolTime; 156 // @dev Calculate the dispute period 157 // @dev timeToDispute is immediate for shorts with CR <= 1.1x 158 159 /* 160 +-------+------------+ 161 | CR(X) | Hours(Y) | 162 +-------+------------+ 163 | 1.1 | 0 | 164 | 1.2 | .333 | 165 | 1.3 | .75 | 166 | 1.5 | 1.5 | 167 | 1.7 | 3 | 168 | 2.0 | 6 | 169 +-------+------------+ 170 171 Creating fixed points and interpolating between points on the graph without using exponentials 172 Using simple y = mx + b formula 173 174 where x = currentCR - previousCR 175 m = (y2-y1)/(x2-x1) 176 b = previous fixed point (Y) 177 */ 178 179 uint256 m; 180 181 if (p.currentCR > 1.7 ether) { 182 m = uint256(3 ether).div(0.3 ether); 183 redeemerAssetUser.timeToDispute = protocolTime + uint32((m.mul(p.currentCR - 1.7 ether) + 3 ether) * 1 hours / 1 ether); 184 } else if (p.currentCR > 1.5 ether) { 185 m = uint256(1.5 ether).div(0.2 ether); 186 redeemerAssetUser.timeToDispute = 187 protocolTime + uint32((m.mul(p.currentCR - 1.5 ether) + 1.5 ether) * 1 hours / 1 ether); 188 } else if (p.currentCR > 1.3 ether) { 189 m = uint256(0.75 ether).div(0.2 ether); 190 redeemerAssetUser.timeToDispute = 191 protocolTime + uint32((m.mul(p.currentCR - 1.3 ether) + 0.75 ether) * 1 hours / 1 ether); 192 } else if (p.currentCR > 1.2 ether) { 193 m = uint256(0.417 ether).div(0.1 ether); 194 redeemerAssetUser.timeToDispute = 195 protocolTime + uint32((m.mul(p.currentCR - 1.2 ether) + C.ONE_THIRD) * 1 hours / 1 ether); 196 } else if (p.currentCR > 1.1 ether) { 197 m = uint256(C.ONE_THIRD.div(0.1 ether)); 198 redeemerAssetUser.timeToDispute = protocolTime + uint32(m.mul(p.currentCR - 1.1 ether) * 1 hours / 1 ether); 199 } 200 201 redeemerAssetUser.oraclePrice = p.oraclePrice; 202 redeemerAssetUser.timeProposed = LibOrders.getOffsetTime(); 203 204 uint88 redemptionFee = calculateRedemptionFee(asset, p.totalColRedeemed, p.totalAmountProposed); 205 if (redemptionFee > maxRedemptionFee) revert Errors.RedemptionFeeTooHigh(); 206 207 STypes.VaultUser storage VaultUser = s.vaultUser[Asset.vault][msg.sender]; 208 if (VaultUser.ethEscrowed < redemptionFee) revert Errors.InsufficientETHEscrowed(); 209 VaultUser.ethEscrowed -= redemptionFee; 210 emit Events.ProposeRedemption(p.asset, msg.sender); 211 } 224 function disputeRedemption(address asset, address redeemer, uint8 incorrectIndex, address disputeShorter, uint8 disputeShortId) 225 external 226 isNotFrozen(asset) 227 nonReentrant 228 { 229 if (redeemer == msg.sender) revert Errors.CannotDisputeYourself(); 230 MTypes.DisputeRedemption memory d; 231 d.asset = asset; 232 d.redeemer = redeemer; 233 234 STypes.AssetUser storage redeemerAssetUser = s.assetUser[d.asset][d.redeemer]; 235 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 236 237 if (LibOrders.getOffsetTime() >= redeemerAssetUser.timeToDispute) revert Errors.TimeToDisputeHasElapsed(); 238 239 MTypes.ProposalData[] memory decodedProposalData = 240 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, redeemerAssetUser.slateLength); 241 242 for (uint256 i = 0; i < decodedProposalData.length; i++) { 243 if (decodedProposalData[i].shorter == disputeShorter && decodedProposalData[i].shortId == disputeShortId) { 244 revert Errors.CannotDisputeWithRedeemerProposal(); 245 } 246 } 247 248 STypes.ShortRecord storage disputeSR = s.shortRecords[d.asset][disputeShorter][disputeShortId]; 249 // Match continue (skip) conditions in proposeRedemption() 250 uint256 minShortErc = LibAsset.minShortErc(d.asset); 251 if (!validRedemptionSR(disputeSR, d.redeemer, disputeShorter, minShortErc)) revert Errors.InvalidRedemption(); 252 253 MTypes.ProposalData memory incorrectProposal = decodedProposalData[incorrectIndex]; 254 MTypes.ProposalData memory currentProposal; 255 STypes.Asset storage Asset = s.asset[d.asset]; 256 257 uint256 disputeCR = disputeSR.getCollateralRatio(redeemerAssetUser.oraclePrice); 258 259 if (disputeCR < incorrectProposal.CR && disputeSR.updatedAt + C.DISPUTE_REDEMPTION_BUFFER <= redeemerAssetUser.timeProposed) 260 { 261 // @dev All proposals from the incorrectIndex onward will be removed 262 // @dev Thus the proposer can only redeem a portion of their original slate 263 for (uint256 i = incorrectIndex; i < decodedProposalData.length; i++) { 264 currentProposal = decodedProposalData[i]; 265 266 STypes.ShortRecord storage currentSR = s.shortRecords[d.asset][currentProposal.shorter][currentProposal.shortId]; 267 currentSR.collateral += currentProposal.colRedeemed; 268 currentSR.ercDebt += currentProposal.ercDebtRedeemed; 269 270 d.incorrectCollateral += currentProposal.colRedeemed; 271 d.incorrectErcDebt += currentProposal.ercDebtRedeemed; 272 } 273 274 s.vault[Asset.vault].dethCollateral += d.incorrectCollateral; 275 Asset.dethCollateral += d.incorrectCollateral; 276 Asset.ercDebt += d.incorrectErcDebt; 277 278 // @dev Update the redeemer's SSTORE2Pointer 279 if (incorrectIndex > 0) { 280 redeemerAssetUser.slateLength = incorrectIndex; 281 } else { 282 // @dev this implies everything in the redeemer's proposal was incorrect 283 delete redeemerAssetUser.SSTORE2Pointer; 284 emit Events.DisputeRedemptionAll(d.asset, redeemer); 285 } 286 287 // @dev Penalty is based on the proposal with highest CR (decodedProposalData is sorted) 288 // @dev PenaltyPct is bound between CallerFeePct and 33% to prevent exploiting primaryLiquidation fees 289 uint256 penaltyPct = LibOrders.min( 290 LibOrders.max(LibAsset.callerFeePct(d.asset), (currentProposal.CR - disputeCR).div(currentProposal.CR)), 0.33 ether 291 ); 292 293 uint88 penaltyAmt = d.incorrectErcDebt.mulU88(penaltyPct); 294 295 // @dev Give redeemer back ercEscrowed that is no longer used to redeem (penalty applied) 296 redeemerAssetUser.ercEscrowed += (d.incorrectErcDebt - penaltyAmt); 297 s.assetUser[d.asset][msg.sender].ercEscrowed += penaltyAmt; 298 } else { 299 revert Errors.InvalidRedemptionDispute(); 300 } 301 } 310 function claimRedemption(address asset) external isNotFrozen(asset) nonReentrant { 311 uint256 vault = s.asset[asset].vault; 312 STypes.AssetUser storage redeemerAssetUser = s.assetUser[asset][msg.sender]; 313 STypes.VaultUser storage redeemerVaultUser = s.vaultUser[vault][msg.sender]; 314 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 315 if (LibOrders.getOffsetTime() < redeemerAssetUser.timeToDispute) revert Errors.TimeToDisputeHasNotElapsed(); 316 317 MTypes.ProposalData[] memory decodedProposalData = 318 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, redeemerAssetUser.slateLength); 319 320 uint88 totalColRedeemed; 321 for (uint256 i = 0; i < decodedProposalData.length; i++) { 322 MTypes.ProposalData memory currentProposal = decodedProposalData[i]; 323 totalColRedeemed += currentProposal.colRedeemed; 324 _claimRemainingCollateral({ 325 asset: asset, 326 vault: vault, 327 shorter: currentProposal.shorter, 328 shortId: currentProposal.shortId 329 }); 330 } 331 redeemerVaultUser.ethEscrowed += totalColRedeemed; 332 delete redeemerAssetUser.SSTORE2Pointer; 333 emit Events.ClaimRedemption(asset, msg.sender); 334 } 347 function claimRemainingCollateral(address asset, address redeemer, uint8 claimIndex, uint8 id) 348 external 349 isNotFrozen(asset) 350 nonReentrant 351 { 352 STypes.AssetUser storage redeemerAssetUser = s.assetUser[asset][redeemer]; 353 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 354 if (redeemerAssetUser.timeToDispute > LibOrders.getOffsetTime()) revert Errors.TimeToDisputeHasNotElapsed(); 355 356 // @dev Only need to read up to the position of the SR to be claimed 357 MTypes.ProposalData[] memory decodedProposalData = 358 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, claimIndex + 1); 359 MTypes.ProposalData memory claimProposal = decodedProposalData[claimIndex]; 360 361 if (claimProposal.shorter != msg.sender && claimProposal.shortId != id) revert Errors.CanOnlyClaimYourShort(); 362 363 STypes.Asset storage Asset = s.asset[asset]; 364 _claimRemainingCollateral({asset: asset, vault: Asset.vault, shorter: msg.sender, shortId: id}); 365 }
</details>File: facets/ShortOrdersFacet.sol 35 function createLimitShort( 36 address asset, 37 uint80 price, 38 uint88 ercAmount, 39 MTypes.OrderHint[] memory orderHintArray, 40 uint16[] memory shortHintArray, 41 uint16 shortOrderCR 42 ) external isNotFrozen(asset) onlyValidAsset(asset) nonReentrant { 43 MTypes.CreateLimitShortParam memory p; 44 STypes.Asset storage Asset = s.asset[asset]; 45 STypes.Order memory incomingShort; 46 47 // @dev create "empty" SR. Fail early. 48 incomingShort.shortRecordId = LibShortRecord.createShortRecord(asset, msg.sender, SR.Closed, 0, 0, 0, 0, 0); 49 50 uint256 cr = LibOrders.convertCR(shortOrderCR); 51 if ((shortOrderCR + C.BID_CR) < Asset.initialCR || cr >= C.CRATIO_MAX_INITIAL) { 52 revert Errors.InvalidCR(); 53 } 54 55 // @dev minShortErc needs to be modified (bigger) when cr < 1 56 p.minShortErc = cr < 1 ether ? LibAsset.minShortErc(asset).mul(1 ether + cr.inv()) : LibAsset.minShortErc(asset); 57 p.eth = price.mul(ercAmount); 58 p.minAskEth = LibAsset.minAskEth(asset); 59 if (ercAmount < p.minShortErc || p.eth < p.minAskEth) revert Errors.OrderUnderMinimumSize(); 60 61 // For a short, need enough collateral to cover minting ERC (calculated using initialCR) 62 if (s.vaultUser[Asset.vault][msg.sender].ethEscrowed < p.eth.mul(cr)) revert Errors.InsufficientETHEscrowed(); 63 64 incomingShort.addr = msg.sender; 65 incomingShort.price = price; 66 incomingShort.ercAmount = ercAmount; 67 incomingShort.id = Asset.orderIdCounter; 68 incomingShort.orderType = O.LimitShort; 69 incomingShort.creationTime = LibOrders.getOffsetTime(); 70 incomingShort.shortOrderCR = shortOrderCR; // 170 -> 1.70x 71 72 p.startingId = s.bids[asset][C.HEAD].nextId; 73 STypes.Order storage highestBid = s.bids[asset][p.startingId]; 74 // @dev if match and match price is gt .5% to saved oracle in either direction, update startingShortId 75 if (highestBid.price >= incomingShort.price && highestBid.orderType == O.LimitBid) { 76 LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingShort, shortHintArray); 77 } 78 79 p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(asset); 80 if (LibSRUtil.checkRecoveryModeViolation(asset, cr, p.oraclePrice)) { 81 revert Errors.BelowRecoveryModeCR(); 82 } 83 84 // @dev reading spot oracle price 85 if (incomingShort.price < p.oraclePrice) { 86 LibOrders.addShort(asset, incomingShort, orderHintArray); 87 } else { 88 LibOrders.sellMatchAlgo(asset, incomingShort, orderHintArray, p.minAskEth); 89 } 90 }
This issue arises when the contract's interface is not imported first, followed by each of the interfaces it uses, followed by all other files.
<details> <summary>Click to show 4 findings</summary>File: facets/BridgeRouterFacet.sol 6 import {IBridge} from "contracts/interfaces/IBridge.sol";
File: facets/PrimaryLiquidationFacet.sol 6 import {IDiamond} from "interfaces/IDiamond.sol";
File: libraries/LibOracle.sol 8 import {IDiamond} from "interfaces/IDiamond.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L8:8
File: libraries/LibOrders.sol 6 import {IDiamond} from "interfaces/IDiamond.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L6:6
</details>This issue arises when constants in comparisons appear on the right side, which can lead to typo bugs.
<details> <summary>Click to show 41 findings</summary>File: facets/BridgeRouterFacet.sol 102 if (dethAmount == 0) revert Errors.ParameterIsZero(); 109 if (dethAssessable > 0) { 111 if (withdrawalFeePct > 0) { 134 if (dethAmount == 0) revert Errors.ParameterIsZero(); 164 if (vault == 0) revert Errors.InvalidBridge();
File: facets/PrimaryLiquidationFacet.sol 56 if (shortHintArray.length > 10) revert Errors.TooManyHints();
File: facets/RedemptionFacet.sol 181 if (p.currentCR > 1.7 ether) { 184 } else if (p.currentCR > 1.5 ether) { 188 } else if (p.currentCR > 1.3 ether) { 192 } else if (p.currentCR > 1.2 ether) { 196 } else if (p.currentCR > 1.1 ether) { 279 if (incorrectIndex > 0) { 371 if (shortRecord.ercDebt == 0 && shortRecord.status == SR.FullyFilled) { 397 assert(newBaseRate > 0); // Base rate is always non-zero after redemption
File: facets/ShortOrdersFacet.sol 56 p.minShortErc = cr < 1 ether ? LibAsset.minShortErc(asset).mul(1 ether + cr.inv()) : LibAsset.minShortErc(asset);
File: libraries/LibBytes.sol 14 require(slate.length % 51 == 0, "Invalid data length");
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L14:14
File: libraries/LibOracle.sol 30 uint256 basePriceInEth = basePrice > 0 ? uint256(basePrice * C.BASE_ORACLE_DECIMALS).inv() : 0; 60 if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice(); 76 bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 80 bool priceDeviation = protocolPrice > 0 && chainlinkDiff.div(protocolPrice) > 0.5 ether; 89 if (twapPrice == 0) { 104 if (wethBal < 100 ether) { 125 bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 126 || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0; 134 if (twapPrice == 0) revert Errors.InvalidTwapPrice(); 139 if (wethBal < 100 ether) revert Errors.InsufficientEthInLiquidityPool(); 169 if (LibOrders.getOffsetTime() - getTime(asset) < 15 minutes) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L169:169
File: libraries/LibOrders.sol 371 if (hintId == 0 || nextId == 0) { 501 if (b.matchedAskId != 0) { 505 if (b.matchedShortId != 0) { 576 if (incomingAsk.ercAmount.mul(highestBid.price) == 0) { 677 SR status = incomingShort.ercAmount == 0 ? SR.FullyFilled : SR.PartialFill; 792 orderPriceGtThreshold = (incomingOrder.price - savedPrice).div(savedPrice) > 0.005 ether; 794 orderPriceGtThreshold = (savedPrice - incomingOrder.price).div(savedPrice) > 0.005 ether; 805 if (timeDiff >= 15 minutes) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L805:805
File: libraries/LibSRUtil.sol 35 if (yield > 0) { 113 if (Asset.ercDebt > 0) { 131 if (short.ercDebt == 0) revert Errors.OriginalShortRecordRedeemed(); 158 if (ercDebt > 0) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L158:158
</details>File: libraries/UniswapOracleLibrary.sol 52 if (secondsAgo <= 0) revert Errors.InvalidTWAPSecondsAgo(); 65 if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int32(secondsAgo) != 0)) {
One level of nesting can be removed by not having an else block when the if-block returns
<details> <summary>Click to show 32 findings</summary>File: facets/BridgeRouterFacet.sol 173 if (dethTotalNew >= dethTotal) { 174 // when yield is positive 1 deth = 1 eth 175 return amount; 176 } else { 177 // negative yield means 1 deth < 1 eth 178 // @dev don't use mulU88 in rare case of overflow 179 return amount.mul(dethTotalNew).divU88(dethTotal); 180 }
File: facets/RedemptionFacet.sol 38 if (shortRecord.status == SR.Closed || shortRecord.ercDebt < minShortErc || proposer == shorter) { 39 return false; 40 } else { 41 return true; 42 }
File: libraries/LibBridgeRouter.sol 48 if (bridgePointer == VAULT.BRIDGE_RETH) { 49 // Withdraw RETH 50 creditReth = VaultUser.bridgeCreditReth; 51 if (creditReth >= amount) { 52 VaultUser.bridgeCreditReth -= amount; 53 return 0; 54 } 55 56 VaultUser.bridgeCreditReth = 0; 57 amount -= creditReth; 58 creditSteth = VaultUser.bridgeCreditSteth; 59 if (creditSteth < C.ROUNDING_ZERO) { 60 // Valid withdraw when no STETH credits 61 return amount; 62 } else { 63 if (IBridge(stethBridge).getDethValue() < C.ROUNDING_ZERO) { 64 // Can withdraw RETH using STETH credit when STETH bridge is empty 65 if (creditSteth >= amount) { 66 VaultUser.bridgeCreditSteth -= amount; 67 return 0; 68 } else { 69 VaultUser.bridgeCreditSteth = 0; 70 return amount - creditSteth; 71 } 72 } else { 73 // Must use available bridge credits on withdrawal 74 // @dev Prevents abusing bridge for arbitrage 75 revert Errors.MustUseExistingBridgeCredit(); 76 } 77 } 78 } else { 79 // Withdraw STETH 80 creditSteth = VaultUser.bridgeCreditSteth; 81 if (creditSteth >= amount) { 82 VaultUser.bridgeCreditSteth -= amount; 83 return 0; 84 } 85 86 VaultUser.bridgeCreditSteth = 0; 87 amount -= creditSteth; 88 creditReth = VaultUser.bridgeCreditReth; 89 if (creditReth < C.ROUNDING_ZERO) { 90 // Valid withdraw when no RETH credits 91 return amount; 92 } else { 93 if (IBridge(rethBridge).getDethValue() < C.ROUNDING_ZERO) { 94 // Can withdraw STETH using RETH credit when RETH bridge is empty 95 if (creditReth >= amount) { 96 VaultUser.bridgeCreditReth -= amount; 97 return 0; 98 } else { 99 VaultUser.bridgeCreditReth = 0; 100 return amount - creditReth; 101 } 102 } else { 103 // Must use available bridge credits on withdrawal 104 // @dev Prevents abusing bridge for arbitrage 105 revert Errors.MustUseExistingBridgeCredit(); 106 } 107 } 108 } 59 if (creditSteth < C.ROUNDING_ZERO) { 60 // Valid withdraw when no STETH credits 61 return amount; 62 } else { 63 if (IBridge(stethBridge).getDethValue() < C.ROUNDING_ZERO) { 64 // Can withdraw RETH using STETH credit when STETH bridge is empty 65 if (creditSteth >= amount) { 66 VaultUser.bridgeCreditSteth -= amount; 67 return 0; 68 } else { 69 VaultUser.bridgeCreditSteth = 0; 70 return amount - creditSteth; 71 } 72 } else { 73 // Must use available bridge credits on withdrawal 74 // @dev Prevents abusing bridge for arbitrage 75 revert Errors.MustUseExistingBridgeCredit(); 76 } 77 } 63 if (IBridge(stethBridge).getDethValue() < C.ROUNDING_ZERO) { 64 // Can withdraw RETH using STETH credit when STETH bridge is empty 65 if (creditSteth >= amount) { 66 VaultUser.bridgeCreditSteth -= amount; 67 return 0; 68 } else { 69 VaultUser.bridgeCreditSteth = 0; 70 return amount - creditSteth; 71 } 72 } else { 73 // Must use available bridge credits on withdrawal 74 // @dev Prevents abusing bridge for arbitrage 75 revert Errors.MustUseExistingBridgeCredit(); 76 } 65 if (creditSteth >= amount) { 66 VaultUser.bridgeCreditSteth -= amount; 67 return 0; 68 } else { 69 VaultUser.bridgeCreditSteth = 0; 70 return amount - creditSteth; 71 } 89 if (creditReth < C.ROUNDING_ZERO) { 90 // Valid withdraw when no RETH credits 91 return amount; 92 } else { 93 if (IBridge(rethBridge).getDethValue() < C.ROUNDING_ZERO) { 94 // Can withdraw STETH using RETH credit when RETH bridge is empty 95 if (creditReth >= amount) { 96 VaultUser.bridgeCreditReth -= amount; 97 return 0; 98 } else { 99 VaultUser.bridgeCreditReth = 0; 100 return amount - creditReth; 101 } 102 } else { 103 // Must use available bridge credits on withdrawal 104 // @dev Prevents abusing bridge for arbitrage 105 revert Errors.MustUseExistingBridgeCredit(); 106 } 107 } 93 if (IBridge(rethBridge).getDethValue() < C.ROUNDING_ZERO) { 94 // Can withdraw STETH using RETH credit when RETH bridge is empty 95 if (creditReth >= amount) { 96 VaultUser.bridgeCreditReth -= amount; 97 return 0; 98 } else { 99 VaultUser.bridgeCreditReth = 0; 100 return amount - creditReth; 101 } 102 } else { 103 // Must use available bridge credits on withdrawal 104 // @dev Prevents abusing bridge for arbitrage 105 revert Errors.MustUseExistingBridgeCredit(); 106 } 95 if (creditReth >= amount) { 96 VaultUser.bridgeCreditReth -= amount; 97 return 0; 98 } else { 99 VaultUser.bridgeCreditReth = 0; 100 return amount - creditReth; 101 } 125 if (factorReth > factorSteth) { 126 // rETH market premium relative to stETH 127 if (bridgePointer == VAULT.BRIDGE_RETH) { 128 // Only charge fee if withdrawing rETH 129 return factorReth.div(factorSteth) - 1 ether; 130 } 131 } else if (factorSteth > factorReth) { 132 // stETH market premium relative to rETH 133 if (bridgePointer == VAULT.BRIDGE_STETH) { 134 // Only charge fee if withdrawing stETH 135 return factorSteth.div(factorReth) - 1 ether; 136 } 137 } else { 138 // Withdrawing less premium LST or premiums are equivalent 139 return 0; 140 } 131 } else if (factorSteth > factorReth) { 132 // stETH market premium relative to rETH 133 if (bridgePointer == VAULT.BRIDGE_STETH) { 134 // Only charge fee if withdrawing stETH 135 return factorSteth.div(factorReth) - 1 ether; 136 } 137 } else { 138 // Withdrawing less premium LST or premiums are equivalent 139 return 0; 140 }
File: libraries/LibOracle.sol 28 if (oracle == baseOracle) { 29 // @dev multiply base oracle by 10**10 to give it 18 decimals of precision 30 uint256 basePriceInEth = basePrice > 0 ? uint256(basePrice * C.BASE_ORACLE_DECIMALS).inv() : 0; 31 basePriceInEth = baseOracleCircuitBreaker(protocolPrice, baseRoundID, basePrice, baseTimeStamp, basePriceInEth); 32 return basePriceInEth; 33 } else { 34 // prettier-ignore 35 ( 36 uint80 roundID, 37 int256 price, 38 /*uint256 startedAt*/ 39 , 40 uint256 timeStamp, 41 /*uint80 answeredInRound*/ 42 ) = oracle.latestRoundData(); 43 uint256 priceInEth = uint256(price).div(uint256(basePrice)); 44 oracleCircuitBreaker(roundID, baseRoundID, price, basePrice, timeStamp, baseTimeStamp); 45 return priceInEth; 46 } 48 if (oracle == baseOracle) { 49 return twapCircuitBreaker(); 50 } else { 51 // prettier-ignore 52 ( 53 uint80 roundID, 54 int256 price, 55 /*uint256 startedAt*/ 56 , 57 uint256 timeStamp, 58 /*uint80 answeredInRound*/ 59 ) = oracle.latestRoundData(); 60 if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice(); 61 62 uint256 twapInv = twapCircuitBreaker(); 63 uint256 priceInEth = uint256(price * C.BASE_ORACLE_DECIMALS).mul(twapInv); 64 return priceInEth; 65 } 83 if (invalidFetchData) { 84 return twapCircuitBreaker(); 85 } else if (priceDeviation) { 86 // Check valid twap price 87 try IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes) returns (uint256 twapPrice) 88 { 89 if (twapPrice == 0) { 90 return chainlinkPriceInEth; 91 } 92 93 uint256 twapPriceNormalized = twapPrice * (1 ether / C.DECIMAL_USDC); 94 uint256 twapPriceInEth = twapPriceNormalized.inv(); 95 uint256 twapDiff = twapPriceInEth > protocolPrice ? twapPriceInEth - protocolPrice : protocolPrice - twapPriceInEth; 96 97 // Save the price that is closest to saved oracle price 98 if (chainlinkDiff <= twapDiff) { 99 return chainlinkPriceInEth; 100 } else { 101 // Check valid twap liquidity 102 IERC20 weth = IERC20(C.WETH); 103 uint256 wethBal = weth.balanceOf(C.USDC_WETH); 104 if (wethBal < 100 ether) { 105 return chainlinkPriceInEth; 106 } 107 return twapPriceInEth; 108 } 109 } catch { 110 return chainlinkPriceInEth; 111 } 112 } else { 113 return chainlinkPriceInEth; 114 } 85 } else if (priceDeviation) { 86 // Check valid twap price 87 try IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes) returns (uint256 twapPrice) 88 { 89 if (twapPrice == 0) { 90 return chainlinkPriceInEth; 91 } 92 93 uint256 twapPriceNormalized = twapPrice * (1 ether / C.DECIMAL_USDC); 94 uint256 twapPriceInEth = twapPriceNormalized.inv(); 95 uint256 twapDiff = twapPriceInEth > protocolPrice ? twapPriceInEth - protocolPrice : protocolPrice - twapPriceInEth; 96 97 // Save the price that is closest to saved oracle price 98 if (chainlinkDiff <= twapDiff) { 99 return chainlinkPriceInEth; 100 } else { 101 // Check valid twap liquidity 102 IERC20 weth = IERC20(C.WETH); 103 uint256 wethBal = weth.balanceOf(C.USDC_WETH); 104 if (wethBal < 100 ether) { 105 return chainlinkPriceInEth; 106 } 107 return twapPriceInEth; 108 } 109 } catch { 110 return chainlinkPriceInEth; 111 } 112 } else { 113 return chainlinkPriceInEth; 114 } 98 if (chainlinkDiff <= twapDiff) { 99 return chainlinkPriceInEth; 100 } else { 101 // Check valid twap liquidity 102 IERC20 weth = IERC20(C.WETH); 103 uint256 wethBal = weth.balanceOf(C.USDC_WETH); 104 if (wethBal < 100 ether) { 105 return chainlinkPriceInEth; 106 } 107 return twapPriceInEth; 108 } 169 if (LibOrders.getOffsetTime() - getTime(asset) < 15 minutes) { 170 return getPrice(asset); 171 } else { 172 return getOraclePrice(asset); 173 }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L169:173
File: libraries/LibOrders.sol 241 if (check1 && check2) { 242 return C.EXACT; 243 } else if (!check1) { 244 return C.PREV; 245 } else if (!check2) { 246 return C.NEXT; 247 } 243 } else if (!check1) { 244 return C.PREV; 245 } else if (!check2) { 246 return C.NEXT; 247 } 272 if (check1 && check2) { 273 return C.EXACT; 274 } else if (!check1) { 275 return C.PREV; 276 } else if (!check2) { 277 return C.NEXT; 278 } 274 } else if (!check1) { 275 return C.PREV; 276 } else if (!check2) { 277 return C.NEXT; 278 } 381 if (direction == C.EXACT) { 382 return hintId; 383 } else if (direction == C.NEXT) { 384 return getOrderId(orders, asset, C.NEXT, nextId, incomingOrder.price, incomingOrder.orderType); 385 } else { 386 uint16 prevId = orders[asset][hintId].prevId; 387 return getOrderId(orders, asset, C.PREV, prevId, incomingOrder.price, incomingOrder.orderType); 388 } 383 } else if (direction == C.NEXT) { 384 return getOrderId(orders, asset, C.NEXT, nextId, incomingOrder.price, incomingOrder.orderType); 385 } else { 386 uint16 prevId = orders[asset][hintId].prevId; 387 return getOrderId(orders, asset, C.PREV, prevId, incomingOrder.price, incomingOrder.orderType); 388 } 412 if (orderType == O.LimitAsk || orderType == O.LimitShort) { 413 return verifySellId(orders, asset, prevId, newPrice, nextId); 414 } else if (orderType == O.LimitBid) { 415 return verifyBidId(asset, prevId, newPrice, nextId); 416 } 421 if (o == O.LimitBid || o == O.MarketBid) { 422 return O.LimitBid; 423 } else if (o == O.LimitAsk || o == O.MarketAsk) { 424 return O.LimitAsk; 425 } else if (o == O.LimitShort) { 426 return O.LimitShort; 427 } 423 } else if (o == O.LimitAsk || o == O.MarketAsk) { 424 return O.LimitAsk; 425 } else if (o == O.LimitShort) { 426 return O.LimitShort; 427 } 574 if (incomingAsk.price <= highestBid.price) { 575 // Consider ask filled if only dust amount left 576 if (incomingAsk.ercAmount.mul(highestBid.price) == 0) { 577 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, false); 578 incomingAsk.ercAmount = 0; 579 matchIncomingSell(asset, incomingAsk, matchTotal); 580 return; 581 } 582 matchHighestBid(incomingAsk, highestBid, asset, matchTotal); 583 if (incomingAsk.ercAmount > highestBid.ercAmount) { 584 incomingAsk.ercAmount -= highestBid.ercAmount; 585 highestBid.ercAmount = 0; 586 matchOrder(s.bids, asset, highestBid.id); 587 588 // loop 589 startingId = highestBid.nextId; 590 if (startingId == C.TAIL) { 591 matchIncomingSell(asset, incomingAsk, matchTotal); 592 593 if (incomingAsk.ercAmount.mul(incomingAsk.price) >= minAskEth) { 594 addSellOrder(incomingAsk, asset, orderHintArray); 595 } 596 s.bids[asset][C.HEAD].nextId = C.TAIL; 597 return; 598 } 599 } else { 600 if (incomingAsk.ercAmount == highestBid.ercAmount) { 601 matchOrder(s.bids, asset, highestBid.id); 602 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, true); 603 } else { 604 highestBid.ercAmount -= incomingAsk.ercAmount; 605 s.bids[asset][highestBid.id].ercAmount = highestBid.ercAmount; 606 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, false); 607 // Check reduced dust threshold for existing limit orders 608 if (highestBid.ercAmount.mul(highestBid.price) < LibAsset.minBidEth(asset).mul(C.DUST_FACTOR)) { 609 cancelBid(asset, highestBid.id); 610 } 611 } 612 incomingAsk.ercAmount = 0; 613 matchIncomingSell(asset, incomingAsk, matchTotal); 614 return; 615 } 616 } else { 617 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, false); 618 matchIncomingSell(asset, incomingAsk, matchTotal); 619 620 if (incomingAsk.ercAmount.mul(incomingAsk.price) >= minAskEth) { 621 addSellOrder(incomingAsk, asset, orderHintArray); 622 } 623 return; 624 } 583 if (incomingAsk.ercAmount > highestBid.ercAmount) { 584 incomingAsk.ercAmount -= highestBid.ercAmount; 585 highestBid.ercAmount = 0; 586 matchOrder(s.bids, asset, highestBid.id); 587 588 // loop 589 startingId = highestBid.nextId; 590 if (startingId == C.TAIL) { 591 matchIncomingSell(asset, incomingAsk, matchTotal); 592 593 if (incomingAsk.ercAmount.mul(incomingAsk.price) >= minAskEth) { 594 addSellOrder(incomingAsk, asset, orderHintArray); 595 } 596 s.bids[asset][C.HEAD].nextId = C.TAIL; 597 return; 598 } 599 } else { 600 if (incomingAsk.ercAmount == highestBid.ercAmount) { 601 matchOrder(s.bids, asset, highestBid.id); 602 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, true); 603 } else { 604 highestBid.ercAmount -= incomingAsk.ercAmount; 605 s.bids[asset][highestBid.id].ercAmount = highestBid.ercAmount; 606 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, false); 607 // Check reduced dust threshold for existing limit orders 608 if (highestBid.ercAmount.mul(highestBid.price) < LibAsset.minBidEth(asset).mul(C.DUST_FACTOR)) { 609 cancelBid(asset, highestBid.id); 610 } 611 } 612 incomingAsk.ercAmount = 0; 613 matchIncomingSell(asset, incomingAsk, matchTotal); 614 return; 615 } 765 if (isExactStartingShort) { 766 Asset.startingShortId = shortHintId; 767 return; 768 } else if (startingShortWithinOracleRange) { 769 // @dev prevShortPrice >= oraclePrice 770 Asset.startingShortId = prevId; 771 return; 772 } else if (allShortUnderOraclePrice) { 773 Asset.startingShortId = C.HEAD; 774 return; 775 } 768 } else if (startingShortWithinOracleRange) { 769 // @dev prevShortPrice >= oraclePrice 770 Asset.startingShortId = prevId; 771 return; 772 } else if (allShortUnderOraclePrice) { 773 Asset.startingShortId = C.HEAD; 774 return; 775 } 838 } else if (order.creationTime == orderHint.creationTime) { 839 return orderHint.hintId; 840 } else if (!anyOrderHintPrevMatched && order.prevOrderType == O.Matched) { 841 anyOrderHintPrevMatched = true; 842 }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L838:842
File: libraries/LibSRUtil.sol 59 if (shorter == msg.sender) { 60 // If call comes from exitShort() or combineShorts() then always cancel 61 LibOrders.cancelShort(asset, shortOrderId); 62 assert(shortRecord.status != SR.PartialFill); 63 return true; 64 } else if (shortRecord.ercDebt < LibAsset.minShortErc(asset)) { 65 // If call comes from liquidate() and SR ercDebt under minShortErc 66 LibOrders.cancelShort(asset, shortOrderId); 67 return true; 68 }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L59:68
</details>The code can be made more compact while also increasing readability by converting the following if-statements to ternaries (e.g. foo += (x > y) ? a : b)
<details> <summary>Click to show 4 findings</summary>File: libraries/LibOrders.sol 90 if (order.price > s.bids[asset][nextId].price || nextId == C.TAIL) { 91 hintId = C.HEAD; 92 } else { 93 hintId = findOrderHintId(s.bids, asset, orderHintArray); 94 } 111 if (order.price < s.asks[asset][nextId].price || nextId == C.TAIL) { 112 hintId = C.HEAD; 113 } else { 114 hintId = findOrderHintId(s.asks, asset, orderHintArray); 115 } 132 if (order.price < s.shorts[asset][nextId].price || nextId == C.TAIL) { 133 hintId = C.HEAD; 134 } else { 135 hintId = findOrderHintId(s.shorts, asset, orderHintArray); 136 } 791 if (incomingOrder.price >= savedPrice) { 792 orderPriceGtThreshold = (incomingOrder.price - savedPrice).div(savedPrice) > 0.005 ether; 793 } else { 794 orderPriceGtThreshold = (savedPrice - incomingOrder.price).div(savedPrice) > 0.005 ether; 795 }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L791:795
</details>If the variable needs to be different based on which class it comes from, a view/pure function should be used instead.
<details> <summary>Click to show 4 findings</summary>File: facets/PrimaryLiquidationFacet.sol 165 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)]; 211 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)]; 241 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)];
File: libraries/LibBytes.sol 24 uint64 CR; // bytes8
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L24:24
</details>The delete
keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.
File: libraries/LibBridgeRouter.sol 56 VaultUser.bridgeCreditReth = 0; 69 VaultUser.bridgeCreditSteth = 0; 86 VaultUser.bridgeCreditSteth = 0; 99 VaultUser.bridgeCreditReth = 0; 167 VaultUserFrom.bridgeCreditReth = 0; 176 VaultUserFrom.bridgeCreditSteth = 0; 188 VaultUserFrom.bridgeCreditReth = 0; 189 VaultUserFrom.bridgeCreditSteth = 0;
File: libraries/LibOrders.sol 578 incomingAsk.ercAmount = 0; 585 highestBid.ercAmount = 0; 612 incomingAsk.ercAmount = 0;
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L612:612
File: libraries/LibSRUtil.sol 138 short.tokenId = 0; 148 nft.shortOrderId = 0;
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L148:148
</details>File: libraries/UniswapOracleLibrary.sol 56 secondsAgos[1] = 0;
immutable
s should use CONSTANT_CASE:For immutable
variable names, each word should use all capital letters, with underscores separating each word (CONSTANT_CASE).
File: facets/BridgeRouterFacet.sol 26 address private immutable rethBridge; 27 address private immutable stethBridge;
File: facets/PrimaryLiquidationFacet.sol 28 address private immutable dusd;
Usually lines in source code are limited to 80 characters. Today's screens are much larger so it's reasonable to stretch this in some cases. The solidity style guide recommends a maximumum line length of 120 characters, so the lines below should be split when they reach that length.
Note: these are missed from bots
<details> <summary>Click to show 53 findings</summary>File: facets/BridgeRouterFacet.sol // @dev Deters attempts to take advantage of long delays between updates to the yield rate, by creating large temporary positions
File: facets/PrimaryLiquidationFacet.sol IDiamond(payable(address(this))).createForcedBid(address(this), m.asset, _bidPrice, m.short.ercDebt, shortHintArray); LibSRUtil.disburseCollateral(m.asset, m.shorter, m.short.collateral, m.short.dethYieldRate, m.short.updatedAt);
File: facets/RedemptionFacet.sol function validRedemptionSR(STypes.ShortRecord storage shortRecord, address proposer, address shorter, uint256 minShortErc) // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); redeemerAssetUser.timeToDispute = protocolTime + uint32((m.mul(p.currentCR - 1.7 ether) + 3 ether) * 1 hours / 1 ether); function disputeRedemption(address asset, address redeemer, uint8 incorrectIndex, address disputeShorter, uint8 disputeShortId) if (disputeCR < incorrectProposal.CR && disputeSR.updatedAt + C.DISPUTE_REDEMPTION_BUFFER <= redeemerAssetUser.timeProposed) STypes.ShortRecord storage currentSR = s.shortRecords[d.asset][currentProposal.shorter][currentProposal.shortId]; LibOrders.max(LibAsset.callerFeePct(d.asset), (currentProposal.CR - disputeCR).div(currentProposal.CR)), 0.33 ether
File: facets/ShortOrdersFacet.sol p.minShortErc = cr < 1 ether ? LibAsset.minShortErc(asset).mul(1 ether + cr.inv()) : LibAsset.minShortErc(asset); LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingShort, shortHintArray);
File: libraries/LibBridgeRouter.sol function withdrawalFeePct(uint256 bridgePointer, address rethBridge, address stethBridge) internal view returns (uint256 fee) { uint256 unitWstethTWAP = OracleLibrary.estimateTWAP(1 ether, 30 minutes, VAULT.WSTETH_WETH, VAULT.WSTETH, C.WETH);
File: libraries/LibBytes.sol function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) { colRedeemed := add(0xffffffffffffffffffffff, shr(80, fullWord)) // (256-88-88 = 80), mask of bytes11 = 0xff * 11
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L42:42
File: libraries/LibOracle.sol try baseOracle.latestRoundData() returns (uint80 baseRoundID, int256 basePrice, uint256, uint256 baseTimeStamp, uint80) { basePriceInEth = baseOracleCircuitBreaker(protocolPrice, baseRoundID, basePrice, baseTimeStamp, basePriceInEth); chainlinkPriceInEth > protocolPrice ? chainlinkPriceInEth - protocolPrice : protocolPrice - chainlinkPriceInEth; try IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes) returns (uint256 twapPrice) uint256 twapDiff = twapPriceInEth > protocolPrice ? twapPriceInEth - protocolPrice : protocolPrice - twapPriceInEth;
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L95:95
File: libraries/LibOrders.sol function increaseSharesOnMatch(address asset, STypes.Order memory order, MTypes.Match memory matchTotal, uint88 eth) internal { function addSellOrder(STypes.Order memory incomingOrder, address asset, MTypes.OrderHint[] memory orderHintArray) private { emit Events.CreateOrder(asset, incomingOrder.addr, incomingOrder.orderType, incomingOrder.id, incomingOrder.ercAmount); function cancelOrder(mapping(address => mapping(uint16 => STypes.Order)) storage orders, address asset, uint16 id) internal { function matchOrder(mapping(address => mapping(uint16 => STypes.Order)) storage orders, address asset, uint16 id) internal { * @dev Instead of canceling each one, just wait till the last match and only swap prevId/nextId there, since the rest are gone function matchIncomingSell(address asset, STypes.Order memory incomingOrder, MTypes.Match memory matchTotal) private { function matchIncomingShort(address asset, STypes.Order memory incomingShort, MTypes.Match memory matchTotal) private { matchTotal.colUsed += incomingSell.price.mulU88(fillErc).mulU88(LibOrders.convertCR(incomingSell.shortOrderCR)); if (shortOrderType == O.Cancelled || shortOrderType == O.Matched || shortOrderType == O.Uninitialized) { bool startingShortWithinOracleRange = shortPrice <= oraclePrice.mul(1.005 ether) && prevShortPrice >= oraclePrice; // @dev Update on match if order matches and price diff between order price and oracle > chainlink threshold (i.e. eth .5%) // @dev Possible for this function to never get called if updateOracleAndStartingShortViaThreshold() gets called often enough uint88 collateralDiff = shortOrder.price.mulU88(debtDiff).mulU88(LibOrders.convertCR(shortOrder.shortOrderCR)); // Approximates the match price compared to the oracle price and accounts for any discount by increasing dethTithePercent
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L954:954
File: libraries/LibSRUtil.sol function disburseCollateral(address asset, address shorter, uint88 collateral, uint256 dethYieldRate, uint32 updatedAt) @dev If somebody exits a short, gets liquidated, decreases their collateral before YIELD_DELAY_SECONDS duration is up, function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) if (shortOrder.shortRecordId != shortRecordId || shortOrder.addr != shorter) revert Errors.InvalidShortOrder(); function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) if (shortOrder.shortRecordId != shortRecordId || shortOrder.addr != shorter) revert Errors.InvalidShortOrder();
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L84:84
</details>File: libraries/UniswapOracleLibrary.sol // https://github.com/Uniswap/v3-periphery/blob/b325bb0905d922ae61fcc7df85ee802e8df5e96c/contracts/libraries/OracleLibrary.sol baseToken < quoteToken ? U256.mulDiv(ratioX192, baseAmount, 1 << 192) : U256.mulDiv(1 << 192, baseAmount, ratioX192); baseToken < quoteToken ? U256.mulDiv(ratioX128, baseAmount, 1 << 128) : U256.mulDiv(1 << 128, baseAmount, ratioX128); // @dev Returns the cumulative tick and liquidity as of each timestamp secondsAgo from the current block timestamp
The file does not contain any of the NatSpec comments (@inheritdoc, @param, @return, @notice), which are important for documentation and user confirmation.
<details> <summary>Click to show 4 findings</summary>File: libraries/LibBridgeRouter.sol // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.21; import {IBridge} from "contracts/interfaces/IBridge.sol"; import {STypes} from "contracts/libraries/DataTypes.sol"; import {AppStorage, appStorage} from "contracts/libraries/AppStorage.sol"; import {OracleLibrary} from "contracts/libraries/UniswapOracleLibrary.sol"; import {C, VAULT} from "contracts/libraries/Constants.sol"; import {Errors} from "contracts/libraries/Errors.sol"; import {U256, U88} from "contracts/libraries/PRBMathHelper.sol"; // import {console} from "contracts/libraries/console.sol"; library LibBridgeRouter { using U256 for uint256; using U88 for uint88; // Credit user account with dETH and bridge credit if applicable function addDeth(uint256 vault, uint256 bridgePointer, uint88 amount) internal { AppStorage storage s = appStorage(); STypes.VaultUser storage VaultUser = s.vaultUser[vault][msg.sender]; if (vault == VAULT.ONE) { // Only VAULT.ONE has mixed LST if (bridgePointer == VAULT.BRIDGE_RETH) { VaultUser.bridgeCreditReth += amount; } else { VaultUser.bridgeCreditSteth += amount; } } VaultUser.ethEscrowed += amount; s.vault[vault].dethTotal += amount; } // Determine how much dETH is NOT covered by bridge credits during withdrawal function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) internal returns (uint88) { AppStorage storage s = appStorage(); STypes.VaultUser storage VaultUser = s.vaultUser[vault][msg.sender]; uint88 creditReth; uint88 creditSteth; if (bridgePointer == VAULT.BRIDGE_RETH) { // Withdraw RETH creditReth = VaultUser.bridgeCreditReth; if (creditReth >= amount) { VaultUser.bridgeCreditReth -= amount; return 0; } VaultUser.bridgeCreditReth = 0; amount -= creditReth; creditSteth = VaultUser.bridgeCreditSteth; if (creditSteth < C.ROUNDING_ZERO) { // Valid withdraw when no STETH credits return amount; } else { if (IBridge(stethBridge).getDethValue() < C.ROUNDING_ZERO) { // Can withdraw RETH using STETH credit when STETH bridge is empty if (creditSteth >= amount) { VaultUser.bridgeCreditSteth -= amount; return 0; } else { VaultUser.bridgeCreditSteth = 0; return amount - creditSteth; } } else { // Must use available bridge credits on withdrawal // @dev Prevents abusing bridge for arbitrage revert Errors.MustUseExistingBridgeCredit(); } } } else { // Withdraw STETH creditSteth = VaultUser.bridgeCreditSteth; if (creditSteth >= amount) { VaultUser.bridgeCreditSteth -= amount; return 0; } VaultUser.bridgeCreditSteth = 0; amount -= creditSteth; creditReth = VaultUser.bridgeCreditReth; if (creditReth < C.ROUNDING_ZERO) { // Valid withdraw when no RETH credits return amount; } else { if (IBridge(rethBridge).getDethValue() < C.ROUNDING_ZERO) { // Can withdraw STETH using RETH credit when RETH bridge is empty if (creditReth >= amount) { VaultUser.bridgeCreditReth -= amount; return 0; } else { VaultUser.bridgeCreditReth = 0; return amount - creditReth; } } else { // Must use available bridge credits on withdrawal // @dev Prevents abusing bridge for arbitrage revert Errors.MustUseExistingBridgeCredit(); } } } } // Bridge fees exist only to prevent free arbitrage, fee charged is the premium/discount differential // @dev Only applicable to VAULT.ONE which has mixed LST function withdrawalFeePct(uint256 bridgePointer, address rethBridge, address stethBridge) internal view returns (uint256 fee) { IBridge bridgeReth = IBridge(rethBridge); IBridge bridgeSteth = IBridge(stethBridge); // Calculate rETH market premium/discount (factor) uint256 unitRethTWAP = OracleLibrary.estimateTWAP(1 ether, 30 minutes, VAULT.RETH_WETH, VAULT.RETH, C.WETH); uint256 unitRethOracle = bridgeReth.getUnitDethValue(); uint256 factorReth = unitRethTWAP.div(unitRethOracle); // Calculate stETH market premium/discount (factor) uint256 unitWstethTWAP = OracleLibrary.estimateTWAP(1 ether, 30 minutes, VAULT.WSTETH_WETH, VAULT.WSTETH, C.WETH); uint256 unitWstethOracle = bridgeSteth.getUnitDethValue(); uint256 factorSteth = unitWstethTWAP.div(unitWstethOracle); if (factorReth > factorSteth) { // rETH market premium relative to stETH if (bridgePointer == VAULT.BRIDGE_RETH) { // Only charge fee if withdrawing rETH return factorReth.div(factorSteth) - 1 ether; } } else if (factorSteth > factorReth) { // stETH market premium relative to rETH if (bridgePointer == VAULT.BRIDGE_STETH) { // Only charge fee if withdrawing stETH return factorSteth.div(factorReth) - 1 ether; } } else { // Withdrawing less premium LST or premiums are equivalent return 0; } } // @dev Only relevant to NFT SR that is being transferred, used to deter workarounds to the bridge credit system function transferBridgeCredit(address asset, address from, address to, uint88 collateral) internal { AppStorage storage s = appStorage(); STypes.Asset storage Asset = s.asset[asset]; uint256 vault = Asset.vault; if (vault == VAULT.ONE) { STypes.VaultUser storage VaultUserFrom = s.vaultUser[vault][from]; uint88 creditReth = VaultUserFrom.bridgeCreditReth; uint88 creditSteth = VaultUserFrom.bridgeCreditSteth; STypes.VaultUser storage VaultUserTo = s.vaultUser[vault][to]; if (creditReth < C.ROUNDING_ZERO && creditSteth < C.ROUNDING_ZERO) { // No bridge credits return; } if (creditReth > C.ROUNDING_ZERO && creditSteth < C.ROUNDING_ZERO) { // Only creditReth if (creditReth > collateral) { VaultUserFrom.bridgeCreditReth -= collateral; VaultUserTo.bridgeCreditReth += collateral; } else { VaultUserFrom.bridgeCreditReth = 0; VaultUserTo.bridgeCreditReth += creditReth; } } else if (creditReth < C.ROUNDING_ZERO && creditSteth > C.ROUNDING_ZERO) { // Only creditSteth if (creditSteth > collateral) { VaultUserFrom.bridgeCreditSteth -= collateral; VaultUserTo.bridgeCreditSteth += collateral; } else { VaultUserFrom.bridgeCreditSteth = 0; VaultUserTo.bridgeCreditSteth += creditSteth; } } else { // Both creditReth and creditSteth uint88 creditTotal = creditReth + creditSteth; if (creditTotal > collateral) { creditReth = creditReth.divU88(creditTotal).mulU88(collateral); creditSteth = creditSteth.divU88(creditTotal).mulU88(collateral); VaultUserFrom.bridgeCreditReth -= creditReth; VaultUserFrom.bridgeCreditSteth -= creditSteth; } else { VaultUserFrom.bridgeCreditReth = 0; VaultUserFrom.bridgeCreditSteth = 0; } VaultUserTo.bridgeCreditReth += creditReth; VaultUserTo.bridgeCreditSteth += creditSteth; } } } // Update user account upon dETH withdrawal function removeDeth(uint256 vault, uint88 amount, uint88 fee) internal { AppStorage storage s = appStorage(); s.vaultUser[vault][msg.sender].ethEscrowed -= (amount + fee); s.vault[vault].dethTotal -= amount; } }
File: libraries/LibBytes.sol // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.21; import {MTypes} from "contracts/libraries/DataTypes.sol"; import {SSTORE2} from "solmate/utils/SSTORE2.sol"; // import {console} from "contracts/libraries/console.sol"; library LibBytes { // Custom decode since SSTORE.write was written directly in proposeRedemption function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) { bytes memory slate = SSTORE2.read(SSTORE2Pointer); // ProposalData is 51 bytes require(slate.length % 51 == 0, "Invalid data length"); MTypes.ProposalData[] memory data = new MTypes.ProposalData[](slateLength); for (uint256 i = 0; i < slateLength; i++) { // 32 offset for array length, mulitply by each ProposalData uint256 offset = i * 51 + 32; address shorter; // bytes20 uint8 shortId; // bytes1 uint64 CR; // bytes8 uint88 ercDebtRedeemed; // bytes11 uint88 colRedeemed; // bytes11 assembly { // mload works 32 bytes at a time let fullWord := mload(add(slate, offset)) // read 20 bytes shorter := shr(96, fullWord) // 0x60 = 96 (256-160) // read 8 bytes shortId := and(0xff, shr(88, fullWord)) // 0x58 = 88 (96-8), mask of bytes1 = 0xff * 1 // read 64 bytes CR := and(0xffffffffffffffff, shr(24, fullWord)) // 0x18 = 24 (88-64), mask of bytes8 = 0xff * 8 fullWord := mload(add(slate, add(offset, 29))) // (29 offset) // read 88 bytes ercDebtRedeemed := shr(168, fullWord) // (256-88 = 168) // read 88 bytes colRedeemed := add(0xffffffffffffffffffffff, shr(80, fullWord)) // (256-88-88 = 80), mask of bytes11 = 0xff * 11 } data[i] = MTypes.ProposalData({ shorter: shorter, shortId: shortId, CR: CR, ercDebtRedeemed: ercDebtRedeemed, colRedeemed: colRedeemed }); } return data; } }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L1:1
File: libraries/LibOracle.sol // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.21; import {U256} from "contracts/libraries/PRBMathHelper.sol"; import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IDiamond} from "interfaces/IDiamond.sol"; import {AppStorage, appStorage} from "contracts/libraries/AppStorage.sol"; import {C} from "contracts/libraries/Constants.sol"; import {LibOrders} from "contracts/libraries/LibOrders.sol"; import {Errors} from "contracts/libraries/Errors.sol"; // import {console} from "contracts/libraries/console.sol"; library LibOracle { using U256 for uint256; function getOraclePrice(address asset) internal view returns (uint256) { AppStorage storage s = appStorage(); AggregatorV3Interface baseOracle = AggregatorV3Interface(s.baseOracle); uint256 protocolPrice = getPrice(asset); AggregatorV3Interface oracle = AggregatorV3Interface(s.asset[asset].oracle); if (address(oracle) == address(0)) revert Errors.InvalidAsset(); try baseOracle.latestRoundData() returns (uint80 baseRoundID, int256 basePrice, uint256, uint256 baseTimeStamp, uint80) { if (oracle == baseOracle) { // @dev multiply base oracle by 10**10 to give it 18 decimals of precision uint256 basePriceInEth = basePrice > 0 ? uint256(basePrice * C.BASE_ORACLE_DECIMALS).inv() : 0; basePriceInEth = baseOracleCircuitBreaker(protocolPrice, baseRoundID, basePrice, baseTimeStamp, basePriceInEth); return basePriceInEth; } else { // prettier-ignore ( uint80 roundID, int256 price, /*uint256 startedAt*/ , uint256 timeStamp, /*uint80 answeredInRound*/ ) = oracle.latestRoundData(); uint256 priceInEth = uint256(price).div(uint256(basePrice)); oracleCircuitBreaker(roundID, baseRoundID, price, basePrice, timeStamp, baseTimeStamp); return priceInEth; } } catch { if (oracle == baseOracle) { return twapCircuitBreaker(); } else { // prettier-ignore ( uint80 roundID, int256 price, /*uint256 startedAt*/ , uint256 timeStamp, /*uint80 answeredInRound*/ ) = oracle.latestRoundData(); if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice(); uint256 twapInv = twapCircuitBreaker(); uint256 priceInEth = uint256(price * C.BASE_ORACLE_DECIMALS).mul(twapInv); return priceInEth; } } } function baseOracleCircuitBreaker( uint256 protocolPrice, uint80 roundId, int256 chainlinkPrice, uint256 timeStamp, uint256 chainlinkPriceInEth ) private view returns (uint256 _protocolPrice) { bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 || block.timestamp > 2 hours + timeStamp; uint256 chainlinkDiff = chainlinkPriceInEth > protocolPrice ? chainlinkPriceInEth - protocolPrice : protocolPrice - chainlinkPriceInEth; bool priceDeviation = protocolPrice > 0 && chainlinkDiff.div(protocolPrice) > 0.5 ether; // @dev if there is issue with chainlink, get twap price. Verify twap and compare with chainlink if (invalidFetchData) { return twapCircuitBreaker(); } else if (priceDeviation) { // Check valid twap price try IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes) returns (uint256 twapPrice) { if (twapPrice == 0) { return chainlinkPriceInEth; } uint256 twapPriceNormalized = twapPrice * (1 ether / C.DECIMAL_USDC); uint256 twapPriceInEth = twapPriceNormalized.inv(); uint256 twapDiff = twapPriceInEth > protocolPrice ? twapPriceInEth - protocolPrice : protocolPrice - twapPriceInEth; // Save the price that is closest to saved oracle price if (chainlinkDiff <= twapDiff) { return chainlinkPriceInEth; } else { // Check valid twap liquidity IERC20 weth = IERC20(C.WETH); uint256 wethBal = weth.balanceOf(C.USDC_WETH); if (wethBal < 100 ether) { return chainlinkPriceInEth; } return twapPriceInEth; } } catch { return chainlinkPriceInEth; } } else { return chainlinkPriceInEth; } } function oracleCircuitBreaker( uint80 roundId, uint80 baseRoundId, int256 chainlinkPrice, int256 baseChainlinkPrice, uint256 timeStamp, uint256 baseTimeStamp ) private view { bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0; if (invalidFetchData) revert Errors.InvalidPrice(); } function twapCircuitBreaker() private view returns (uint256 twapPriceInEth) { // Check valid price uint256 twapPrice = IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes); if (twapPrice == 0) revert Errors.InvalidTwapPrice(); // Check valid liquidity IERC20 weth = IERC20(C.WETH); uint256 wethBal = weth.balanceOf(C.USDC_WETH); if (wethBal < 100 ether) revert Errors.InsufficientEthInLiquidityPool(); uint256 twapPriceNormalized = twapPrice * (1 ether / C.DECIMAL_USDC); return twapPriceNormalized.inv(); } /* @dev C.HEAD to marks the start/end of the linked list, so the only properties needed are id/nextId/prevId. Helper methods are used to set the values of oraclePrice and oracleTime since they are set to different properties */ function setPriceAndTime(address asset, uint256 oraclePrice, uint32 oracleTime) internal { AppStorage storage s = appStorage(); s.bids[asset][C.HEAD].ercAmount = uint80(oraclePrice); s.bids[asset][C.HEAD].creationTime = oracleTime; } // @dev Intentionally using creationTime for oracleTime. function getTime(address asset) internal view returns (uint256 creationTime) { AppStorage storage s = appStorage(); return s.bids[asset][C.HEAD].creationTime; } // @dev Intentionally using ercAmount for oraclePrice. Storing as price may lead to bugs in the match algos. function getPrice(address asset) internal view returns (uint80 oraclePrice) { AppStorage storage s = appStorage(); return uint80(s.bids[asset][C.HEAD].ercAmount); } // @dev Allows caller to save gas since reading spot price costs ~16K function getSavedOrSpotOraclePrice(address asset) internal view returns (uint256) { if (LibOrders.getOffsetTime() - getTime(asset) < 15 minutes) { return getPrice(asset); } else { return getOraclePrice(asset); } } }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L1:1
File: libraries/LibSRUtil.sol // SPDX-License-Identifier: GPL-3.0-only pragma solidity 0.8.21; import {U88, U256} from "contracts/libraries/PRBMathHelper.sol"; import {STypes, SR} from "contracts/libraries/DataTypes.sol"; import {AppStorage, appStorage} from "contracts/libraries/AppStorage.sol"; import {C} from "contracts/libraries/Constants.sol"; import {LibOrders} from "contracts/libraries/LibOrders.sol"; import {LibShortRecord} from "contracts/libraries/LibShortRecord.sol"; import {LibAsset} from "contracts/libraries/LibAsset.sol"; import {Errors} from "contracts/libraries/Errors.sol"; import {LibBridgeRouter} from "contracts/libraries/LibBridgeRouter.sol"; // import {console} from "contracts/libraries/console.sol"; // extra ShortRecord helpers, similar to LibShortRecord library LibSRUtil { using U88 for uint88; using U256 for uint256; function disburseCollateral(address asset, address shorter, uint88 collateral, uint256 dethYieldRate, uint32 updatedAt) internal { AppStorage storage s = appStorage(); STypes.Asset storage Asset = s.asset[asset]; uint256 vault = Asset.vault; STypes.Vault storage Vault = s.vault[vault]; Vault.dethCollateral -= collateral; Asset.dethCollateral -= collateral; // Distribute yield uint88 yield = collateral.mulU88(Vault.dethYieldRate - dethYieldRate); if (yield > 0) { /* @dev If somebody exits a short, gets liquidated, decreases their collateral before YIELD_DELAY_SECONDS duration is up, they lose their yield to the TAPP */ bool isNotRecentlyModified = LibOrders.getOffsetTime() - updatedAt > C.YIELD_DELAY_SECONDS; if (isNotRecentlyModified) { s.vaultUser[vault][shorter].ethEscrowed += yield; } else { s.vaultUser[vault][address(this)].ethEscrowed += yield; } } } function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) internal returns (bool isCancelled) { AppStorage storage s = appStorage(); if (initialStatus == SR.PartialFill) { STypes.Order storage shortOrder = s.shorts[asset][shortOrderId]; STypes.ShortRecord storage shortRecord = s.shortRecords[asset][shorter][shortRecordId]; if (shortOrder.shortRecordId != shortRecordId || shortOrder.addr != shorter) revert Errors.InvalidShortOrder(); if (shorter == msg.sender) { // If call comes from exitShort() or combineShorts() then always cancel LibOrders.cancelShort(asset, shortOrderId); assert(shortRecord.status != SR.PartialFill); return true; } else if (shortRecord.ercDebt < LibAsset.minShortErc(asset)) { // If call comes from liquidate() and SR ercDebt under minShortErc LibOrders.cancelShort(asset, shortOrderId); return true; } } } function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) internal returns (bool isCancelled) { AppStorage storage s = appStorage(); STypes.ShortRecord storage shortRecord = s.shortRecords[asset][shorter][shortRecordId]; uint256 minShortErc = LibAsset.minShortErc(asset); if (initialStatus == SR.PartialFill) { // Verify shortOrder STypes.Order storage shortOrder = s.shorts[asset][shortOrderId]; if (shortOrder.shortRecordId != shortRecordId || shortOrder.addr != shorter) revert Errors.InvalidShortOrder(); if (shortRecord.status == SR.Closed) { // Check remaining shortOrder if (shortOrder.ercAmount < minShortErc) { // @dev The resulting SR will not have PartialFill status after cancel LibOrders.cancelShort(asset, shortOrderId); isCancelled = true; } } else { // Check remaining shortOrder and remaining shortRecord if (shortOrder.ercAmount + shortRecord.ercDebt < minShortErc) revert Errors.CannotLeaveDustAmount(); } } else if (shortRecord.status != SR.Closed && shortRecord.ercDebt < minShortErc) { revert Errors.CannotLeaveDustAmount(); } } function checkRecoveryModeViolation(address asset, uint256 shortRecordCR, uint256 oraclePrice) internal view returns (bool recoveryViolation) { AppStorage storage s = appStorage(); uint256 recoveryCR = LibAsset.recoveryCR(asset); if (shortRecordCR < recoveryCR) { // Only check asset CR if low enough STypes.Asset storage Asset = s.asset[asset]; if (Asset.ercDebt > 0) { // If Asset.ercDebt == 0 then assetCR is NA uint256 assetCR = Asset.dethCollateral.div(oraclePrice.mul(Asset.ercDebt)); if (assetCR < recoveryCR) { // Market is in recovery mode and shortRecord CR too low return true; } } } } function transferShortRecord(address from, address to, uint40 tokenId) internal { AppStorage storage s = appStorage(); STypes.NFT storage nft = s.nftMapping[tokenId]; address asset = s.assetMapping[nft.assetId]; STypes.ShortRecord storage short = s.shortRecords[asset][from][nft.shortRecordId]; if (short.status == SR.Closed) revert Errors.OriginalShortRecordCancelled(); if (short.ercDebt == 0) revert Errors.OriginalShortRecordRedeemed(); // @dev shortOrderId is already validated in mintNFT if (short.status == SR.PartialFill) { LibOrders.cancelShort(asset, nft.shortOrderId); } short.tokenId = 0; LibShortRecord.deleteShortRecord(asset, from, nft.shortRecordId); LibBridgeRouter.transferBridgeCredit(asset, from, to, short.collateral); uint8 id = LibShortRecord.createShortRecord( asset, to, SR.FullyFilled, short.collateral, short.ercDebt, short.ercDebtRate, short.dethYieldRate, tokenId ); nft.owner = to; nft.shortRecordId = id; nft.shortOrderId = 0; } function updateErcDebt(STypes.ShortRecord storage short, address asset) internal { AppStorage storage s = appStorage(); // Distribute ercDebt uint64 ercDebtRate = s.asset[asset].ercDebtRate; uint88 ercDebt = short.ercDebt.mulU88(ercDebtRate - short.ercDebtRate); if (ercDebt > 0) { short.ercDebt += ercDebt; short.ercDebtRate = ercDebtRate; } } }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L1:1
</details>Function declarations should be preceded by a NatSpec comment.
<details> <summary>Click to show 36 findings</summary>File: facets/BridgeRouterFacet.sol 29 constructor(address _rethBridge, address _stethBridge) {
File: facets/PrimaryLiquidationFacet.sol 30 constructor(address _dusd) { 229 function min88(uint256 a, uint88 b) private pure returns (uint88) {
File: facets/RedemptionFacet.sol 31 function validRedemptionSR(STypes.ShortRecord storage shortRecord, address proposer, address shorter, uint256 minShortErc)
File: libraries/LibBridgeRouter.sol 21 function addDeth(uint256 vault, uint256 bridgePointer, uint88 amount) internal { 39 function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) 198 function removeDeth(uint256 vault, uint88 amount, uint88 fee) internal {
File: libraries/LibBytes.sol 11 function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L11:11
File: libraries/LibOracle.sol 19 function getOraclePrice(address asset) internal view returns (uint256) { 69 function baseOracleCircuitBreaker( 117 function oracleCircuitBreaker( 131 function twapCircuitBreaker() private view returns (uint256 twapPriceInEth) { 149 function setPriceAndTime(address asset, uint256 oraclePrice, uint32 oracleTime) internal {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L149:149
File: libraries/LibOrders.sol 35 function convertCR(uint16 cr) internal pure returns (uint256) { 40 function increaseSharesOnMatch(address asset, STypes.Order memory order, MTypes.Match memory matchTotal, uint88 eth) internal { 55 function currentOrders(mapping(address => mapping(uint16 => STypes.Order)) storage orders, address asset) 78 function isShort(STypes.Order memory order) internal pure returns (bool) { 82 function addBid(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 103 function addAsk(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 628 function matchIncomingSell(address asset, STypes.Order memory incomingOrder, MTypes.Match memory matchTotal) private { 810 function updateStartingShortIdViaShort(address asset, STypes.Order memory incomingShort) internal { 826 function findOrderHintId( 854 function cancelBid(address asset, uint16 id) internal { 868 function cancelAsk(address asset, uint16 id) internal { 882 function cancelShort(address asset, uint16 id) internal { 955 function handlePriceDiscount(address asset, uint80 price) internal { 985 function min(uint256 a, uint256 b) internal pure returns (uint256) { 989 function max(uint256 a, uint256 b) internal pure returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L989:989
File: libraries/LibSRUtil.sol 22 function disburseCollateral(address asset, address shorter, uint88 collateral, uint256 dethYieldRate, uint32 updatedAt) 49 function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 72 function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 102 function checkRecoveryModeViolation(address asset, uint256 shortRecordCR, uint256 oraclePrice) 124 function transferShortRecord(address from, address to, uint40 tokenId) internal { 151 function updateErcDebt(STypes.ShortRecord storage short, address asset) internal {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L151:151
</details>File: libraries/UniswapOracleLibrary.sol 11 function observe(uint32[] calldata secondsAgos) 47 function estimateTWAP(uint128 amountIn, uint32 secondsAgo, address pool, address baseToken, address quoteToken)
@notice
tags:@notice
is used to explain to end users what the contract does, and the compiler interprets ///
or /**
comments as this tag if one wasn't explicitly provided.
File: facets/BridgeRouterFacet.sol 18 contract BridgeRouterFacet is Modifiers {
File: facets/PrimaryLiquidationFacet.sol 21 contract PrimaryLiquidationFacet is Modifiers {
File: facets/RedemptionFacet.sol 21 contract RedemptionFacet is Modifiers {
File: facets/ShortOrdersFacet.sol 18 contract ShortOrdersFacet is Modifiers {
File: libraries/LibBridgeRouter.sol 16 library LibBridgeRouter {
File: libraries/LibBytes.sol 9 library LibBytes {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L9:9
File: libraries/LibOracle.sol 16 library LibOracle {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L16:16
File: libraries/LibOrders.sol 20 library LibOrders {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L20:20
File: libraries/LibSRUtil.sol 18 library LibSRUtil {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L18:18
</details>File: libraries/UniswapOracleLibrary.sol 10 interface IUniswapV3Pool {
NatSpec must begin with ///
, or use /* ... */
syntax.
File: facets/BridgeRouterFacet.sol 67 // @dev amount after deposit might be less, if bridge takes a fee 147 // @dev Deters attempts to take advantage of long delays between updates to the yield rate, by creating large temporary positions 178 // @dev don't use mulU88 in rare case of overflow
File: facets/PrimaryLiquidationFacet.sol 55 // @dev TAPP partially reimburses gas fees, capped at 10 to limit arbitrary high cost 58 // @dev Ensures SR has enough ercDebt/collateral to make caller fee worthwhile 67 // @dev liquidate requires more up-to-date oraclePrice 72 // @dev Can liquidate as long as CR is low enough 95 // @dev startingShortId is updated via updateOracleAndStartingShortViaTimeBidOnly() prior to call 158 // @dev Provide higher price to better ensure it can fully fill the liquidation 164 // @dev Increase ethEscrowed by shorter's full collateral for forced bid 177 // @dev Max ethDebt can only be the ethEscrowed in the TAPP 186 // @dev Liquidation contract will be the caller. Virtual accounting done later for shorter or TAPP 192 // @dev virtually burning the repurchased debt 197 // @dev manually setting basefee to 1,000,000 in foundry.toml; 198 // @dev By basing gasFee off of baseFee instead of priority, adversaries are prevent from draining the TAPP 217 // @dev TAPP already received the gasFee for being the forcedBid caller. tappFee nets out.
File: facets/RedemptionFacet.sol 36 // @dev Matches check in onlyValidShortRecord but with a more restrictive ercDebt condition 37 // @dev Proposer can't redeem on self 72 // @dev redeemerAssetUser.SSTORE2Pointer gets reset to address(0) after actual redemption 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 127 // @dev directly write the properties of MTypes.ProposalData into bytes 145 // @dev SSTORE2 the entire proposalData after validating proposalInput 156 // @dev Calculate the dispute period 157 // @dev timeToDispute is immediate for shorts with CR <= 1.1x 261 // @dev All proposals from the incorrectIndex onward will be removed 262 // @dev Thus the proposer can only redeem a portion of their original slate 278 // @dev Update the redeemer's SSTORE2Pointer 282 // @dev this implies everything in the redeemer's proposal was incorrect 287 // @dev Penalty is based on the proposal with highest CR (decodedProposalData is sorted) 288 // @dev PenaltyPct is bound between CallerFeePct and 33% to prevent exploiting primaryLiquidation fees 295 // @dev Give redeemer back ercEscrowed that is no longer used to redeem (penalty applied) 356 // @dev Only need to read up to the position of the SR to be claimed 372 // @dev Refund shorter the remaining collateral only if fully redeemed and not claimed already 375 // @dev Shorter shouldn't lose any unclaimed yield because dispute time > YIELD_DELAY_SECONDS 381 // @dev inspired by https://docs.liquity.org/faq/lusd-redemptions#how-is-the-redemption-fee-calculated 391 // @dev Calculate Asset.ercDebt prior to proposal 393 // @dev Derived via this forumula: baseRateNew = baseRateOld + redeemedLUSD / (2 * totalLUSD)
File: facets/ShortOrdersFacet.sol 47 // @dev create "empty" SR. Fail early. 55 // @dev minShortErc needs to be modified (bigger) when cr < 1 74 // @dev if match and match price is gt .5% to saved oracle in either direction, update startingShortId 84 // @dev reading spot oracle price
File: libraries/LibBridgeRouter.sol 74 // @dev Prevents abusing bridge for arbitrage 104 // @dev Prevents abusing bridge for arbitrage 112 // @dev Only applicable to VAULT.ONE which has mixed LST 143 // @dev Only relevant to NFT SR that is being transferred, used to deter workarounds to the bridge credit system
File: libraries/LibOracle.sol 29 // @dev multiply base oracle by 10**10 to give it 18 decimals of precision 82 // @dev if there is issue with chainlink, get twap price. Verify twap and compare with chainlink 155 // @dev Intentionally using creationTime for oracleTime. 161 // @dev Intentionally using ercAmount for oraclePrice. Storing as price may lead to bugs in the match algos. 167 // @dev Allows caller to save gas since reading spot price costs ~16K
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L167:167
File: libraries/LibOrders.sol 29 // @dev in seconds 43 // @dev use the diff to get more time (2159), to prevent overflow at year 2106 138 // @dev: Only need to set this when placing incomingShort onto market 172 // @dev partial addOrder 188 // @dev (ID) is exiting, [ID] is inserted 237 // @dev: TAIL can't be prevId because it will always be last item in list 267 // @dev: TAIL can't be prevId because it will always be last item in list 294 // @dev (ID) is exiting, [ID] is inserted 332 // @dev mark as cancelled instead of deleting the order itself 419 // @dev not used to change state, just which methods to call 507 // @dev Handles only matching one thing 508 // @dev If does not get fully matched, b.matchedShortId == 0 and therefore not hit this block 511 // @dev Handles moving forward only 518 // @dev Handle going backward and forward 641 // @dev match price is based on the order that was already on orderbook 724 // @dev this happens at the end so fillErc isn't affected in previous calculations 760 // @dev force hint to be within 0.5% of oraclePrice 769 // @dev prevShortPrice >= oraclePrice 782 // @dev Update on match if order matches and price diff between order price and oracle > chainlink threshold (i.e. eth .5%) 790 // @dev handle .5% deviations in either directions 802 // @dev Possible for this function to never get called if updateOracleAndStartingShortViaThreshold() gets called often enough 846 // @dev If hint was prev matched, assume that hint was close to HEAD and therefore is reasonable to use HEAD 899 // @dev creating shortOrder automatically creates a closed shortRecord which also sets a shortRecordId 900 // @dev cancelling an unmatched order needs to also handle/recycle the shortRecordId 905 // @dev prevents leaving behind a partially filled SR is under minShortErc 906 // @dev if the corresponding short is cancelled, then the partially filled SR's debt will == minShortErc 928 // @dev update the eth refund amount 931 // @dev virtually mint the increased debt 962 // @dev tithe is increased only if discount is greater than 1% 964 // @dev bound the new tithe amount between 25% and 100% 967 // @dev Vault.dethTitheMod is added onto Vault.dethTithePercent to get tithe amount 970 // @dev dethTitheMod is only set when setTithe is called. 974 // @dev change back to original tithe percent 981 // @dev exists because of ShortOrderFacet contract size
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L981:981
File: libraries/LibSRUtil.sol 89 // @dev The resulting SR will not have PartialFill status after cancel 133 // @dev shortOrderId is already validated in mintNFT
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L133:133
</details>File: libraries/UniswapOracleLibrary.sol 58 // @dev Returns the cumulative tick and liquidity as of each timestamp secondsAgo from the current block timestamp 69 // @dev Gets price using this formula: p(i) = 1.0001**i, where i is the tick
Some lines use // x and some use //x. The instances below point out the usages that don't follow the majority, within each file.
File: facets/PrimaryLiquidationFacet.sol 92 //PRIVATE FUNCTIONS
File: libraries/LibOrders.sol 514 //@handles moving backwards only.
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L514:514
Consider changing the return variable to be an unnamed one, since the variable is never assigned, nor is it returned by name
<details> <summary>Click to show 15 findings</summary>File: facets/RedemptionFacet.sol 384 returns (uint88 redemptionFee)
File: libraries/LibBridgeRouter.sol 113 function withdrawalFeePct(uint256 bridgePointer, address rethBridge, address stethBridge) internal view returns (uint256 fee) {
File: libraries/LibOracle.sol 75 ) private view returns (uint256 _protocolPrice) { 131 function twapCircuitBreaker() private view returns (uint256 twapPriceInEth) { 156 function getTime(address asset) internal view returns (uint256 creationTime) { 162 function getPrice(address asset) internal view returns (uint80 oraclePrice) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L162:162
File: libraries/LibOrders.sol 30 function getOffsetTime() internal view returns (uint32 timeInSeconds) { 234 returns (int256 direction) 266 ) private view returns (int256 direction) { 409 ) internal view returns (int256 direction) { 420 function normalizeOrderType(O o) private pure returns (O newO) { 447 ) internal view returns (uint16 _hintId) { 830 ) internal view returns (uint16 hintId) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L830:830
File: libraries/LibSRUtil.sol 51 returns (bool isCancelled) 105 returns (bool recoveryViolation)
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L105:105
</details>While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.
File: Various Files None
Large code bases, or code with lots of inline-assembly, complicated math, or complicated interactions between multiple contracts, should implement invariant fuzzing tests. Invariant fuzzers such as Echidna require the test writer to come up with invariants which should not be violated under any circumstances, and the fuzzer tests various inputs and function calls to ensure that the invariants always hold. Even code with 100% code coverage can still have bugs due to the order of the operations a user performs, and invariant fuzzers, with properly and extensively-written invariants, can close this testing gap significantly.
File: Various Files None
block.number
instead of block.timestamp
:block.timestamp
is vulnerable to miner manipulation and creates a potential front-running vulnerability. Consider using block.number
instead.
File: libraries/LibOracle.sol 60 if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice(); 76 bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 77 || block.timestamp > 2 hours + timeStamp; 125 bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 126 || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0;
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L126:126
File: libraries/LibOrders.sol 32 return uint32(block.timestamp - C.STARTING_TIME); // @dev(safe-cast)
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L32:32
</details>The functions below take in an unbounded array, and make function calls for entries in the array. While the function will revert if it eventually runs out of gas, it may be a nicer user experience to require()
that the length of the array is below some reasonable maximum, so that the user doesn't have to use up a full transaction's gas only to see that the transaction reverts.
File: facets/RedemptionFacet.sol 78 for (uint8 i = 0; i < proposalInput.length; i++) { 79 p.shorter = proposalInput[i].shorter; 80 p.shortId = proposalInput[i].shortId; 81 p.shortOrderId = proposalInput[i].shortOrderId; 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 84 85 /// Evaluate proposed shortRecord 86 87 if (!validRedemptionSR(currentSR, msg.sender, p.shorter, minShortErc)) continue; 88 89 currentSR.updateErcDebt(p.asset); 90 p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); 91 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 93 if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; 94 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 96 if (p.totalAmountProposed + currentSR.ercDebt <= redemptionAmount) { 97 p.amountProposed = currentSR.ercDebt; 98 } else { 99 p.amountProposed = redemptionAmount - p.totalAmountProposed; 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 101 if (currentSR.ercDebt - p.amountProposed < minShortErc) break; 102 } 103 104 /// At this point, the shortRecord passes all checks and will be included in the slate 105 106 p.previousCR = p.currentCR; 107 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 110 STypes.Order storage shortOrder = s.shorts[asset][p.shortOrderId]; 111 if (currentSR.status == SR.PartialFill && shortOrder.ercAmount < minShortErc) { 112 if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); 113 LibOrders.cancelShort(asset, p.shortOrderId); 114 } 115 116 p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed); 117 if (p.colRedeemed > currentSR.collateral) { 118 p.colRedeemed = currentSR.collateral; 119 } 120 121 currentSR.collateral -= p.colRedeemed; 122 currentSR.ercDebt -= p.amountProposed; 123 124 p.totalAmountProposed += p.amountProposed; 125 p.totalColRedeemed += p.colRedeemed; 126 127 // @dev directly write the properties of MTypes.ProposalData into bytes 128 // instead of usual abi.encode to save on extra zeros being written 129 slate = bytes.concat( 130 slate, 131 bytes20(p.shorter), 132 bytes1(p.shortId), 133 bytes8(uint64(p.currentCR)), 134 bytes11(p.amountProposed), 135 bytes11(p.colRedeemed) 136 ); 137 138 LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); 139 p.redemptionCounter++; 140 if (redemptionAmount - p.totalAmountProposed < minShortErc) break; 141 }
File: libraries/LibOrders.sol 743 for (uint256 i = 0; i < shortHintArray.length;) { 744 shortHintId = shortHintArray[i]; 745 unchecked { 746 ++i; 747 } 748 749 STypes.Order storage short = s.shorts[asset][shortHintId]; 750 { 751 O shortOrderType = short.orderType; 752 if (shortOrderType == O.Cancelled || shortOrderType == O.Matched || shortOrderType == O.Uninitialized) { 753 continue; 754 } 755 } 756 757 uint80 shortPrice = short.price; 758 uint16 prevId = short.prevId; 759 uint80 prevShortPrice = s.shorts[asset][prevId].price; 760 // @dev force hint to be within 0.5% of oraclePrice 761 bool startingShortWithinOracleRange = shortPrice <= oraclePrice.mul(1.005 ether) && prevShortPrice >= oraclePrice; 762 bool isExactStartingShort = shortPrice >= oraclePrice && prevShortPrice < oraclePrice; 763 bool allShortUnderOraclePrice = shortPrice < oraclePrice && short.nextId == C.TAIL; 764 765 if (isExactStartingShort) { 766 Asset.startingShortId = shortHintId; 767 return; 768 } else if (startingShortWithinOracleRange) { 769 // @dev prevShortPrice >= oraclePrice 770 Asset.startingShortId = prevId; 771 return; 772 } else if (allShortUnderOraclePrice) { 773 Asset.startingShortId = C.HEAD; 774 return; 775 } 776 }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L743:776
As the Solidity Style Guide suggests: arguments, local variables and mutable state variables should be named in mixedCase style.
<details> <summary>Click to show 46 findings</summary>File: facets/PrimaryLiquidationFacet.sol 210 STypes.VaultUser storage VaultUser = s.vaultUser[m.vault][msg.sender]; 241 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)]; 170 STypes.Asset storage Asset = s.asset[m.asset]; 211 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)]; 165 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)];
File: facets/RedemptionFacet.sol 207 STypes.VaultUser storage VaultUser = s.vaultUser[Asset.vault][msg.sender]; 248 STypes.ShortRecord storage disputeSR = s.shortRecords[d.asset][disputeShorter][disputeShortId]; 386 STypes.Asset storage Asset = s.asset[asset]; 363 STypes.Asset storage Asset = s.asset[asset]; 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 151 STypes.Asset storage Asset = s.asset[p.asset]; 266 STypes.ShortRecord storage currentSR = s.shortRecords[d.asset][currentProposal.shorter][currentProposal.shortId]; 257 uint256 disputeCR = disputeSR.getCollateralRatio(redeemerAssetUser.oraclePrice); 255 STypes.Asset storage Asset = s.asset[d.asset]; 394 uint256 redeemedDUSDFraction = ercDebtRedeemed.div(totalAssetErcDebt);
File: facets/ShortOrdersFacet.sol 41 uint16 shortOrderCR 44 STypes.Asset storage Asset = s.asset[asset];
File: libraries/LibBridgeRouter.sol 23 STypes.VaultUser storage VaultUser = s.vaultUser[vault][msg.sender]; 44 STypes.VaultUser storage VaultUser = s.vaultUser[vault][msg.sender]; 154 STypes.VaultUser storage VaultUserTo = s.vaultUser[vault][to]; 151 STypes.VaultUser storage VaultUserFrom = s.vaultUser[vault][from]; 147 STypes.Asset storage Asset = s.asset[asset]; 118 uint256 unitRethTWAP = OracleLibrary.estimateTWAP(1 ether, 30 minutes, VAULT.RETH_WETH, VAULT.RETH, C.WETH); 122 uint256 unitWstethTWAP = OracleLibrary.estimateTWAP(1 ether, 30 minutes, VAULT.WSTETH_WETH, VAULT.WSTETH, C.WETH);
File: libraries/LibBytes.sol 11 function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) { 24 uint64 CR; // bytes8
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L24:24
File: libraries/LibOracle.sol 36 uint80 roundID, 37 int256 price, 38 /*uint256 startedAt*/ 39 , 40 uint256 timeStamp, 53 uint80 roundID, 54 int256 price, 55 /*uint256 startedAt*/ 56 , 57 uint256 timeStamp,
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L53:57
File: libraries/LibOrders.sol 737 STypes.Asset storage Asset = s.asset[asset]; 187 uint16 canceledID = orders[asset][C.HEAD].prevId; 891 STypes.Asset storage Asset = s.asset[asset]; 909 STypes.Vault storage Vault = s.vault[vault]; 315 uint16 prevHEAD = orders[asset][C.HEAD].prevId; 958 STypes.Vault storage Vault = s.vault[vault]; 324 uint16 prevHEAD, 812 STypes.Asset storage Asset = s.asset[asset]; 291 uint16 prevHEAD = orders[asset][C.HEAD].prevId; 195 uint16 prevCanceledID = orders[asset][canceledID].prevId; 670 STypes.Asset storage Asset = s.asset[asset]; 672 STypes.Vault storage Vault = s.vault[vault];
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L672:672
File: libraries/LibSRUtil.sol 27 STypes.Asset storage Asset = s.asset[asset]; 109 uint256 recoveryCR = LibAsset.recoveryCR(asset); 29 STypes.Vault storage Vault = s.vault[vault]; 115 uint256 assetCR = Asset.dethCollateral.div(oraclePrice.mul(Asset.ercDebt)); 112 STypes.Asset storage Asset = s.asset[asset]; 102 function checkRecoveryModeViolation(address asset, uint256 shortRecordCR, uint256 oraclePrice)
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L102:102
</details>According to the Solidity style guide function names should be in mixedCase
(lowerCamelCase).
File: facets/RedemptionFacet.sol 31 function validRedemptionSR(STypes.ShortRecord storage shortRecord, address proposer, address shorter, uint256 minShortErc)
File: libraries/LibOrders.sol 35 function convertCR(uint16 cr) internal pure returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L35:35
File: libraries/UniswapOracleLibrary.sol 47 function estimateTWAP(uint128 amountIn, uint32 secondsAgo, address pool, address baseToken, address quoteToken)
Doing so will significantly increase centralization, but will help to prevent hackers from using stolen tokens.
<details> <summary>Click to show 4 findings</summary>File: facets/BridgeRouterFacet.sol 18 contract BridgeRouterFacet is Modifiers {
File: facets/PrimaryLiquidationFacet.sol 21 contract PrimaryLiquidationFacet is Modifiers {
File: facets/RedemptionFacet.sol 21 contract RedemptionFacet is Modifiers {
</details>File: facets/ShortOrdersFacet.sol 18 contract ShortOrdersFacet is Modifiers {
When an action is triggered based on a user's action, not being able to filter based on who triggered the action makes event processing a lot more cumbersome. Including the msg.sender the events of these types of action will make events much more useful to end users. Include msg.sender
in the event output.
File: facets/RedemptionFacet.sol 284 emit Events.DisputeRedemptionAll(d.asset, redeemer);
Consider using descriptive constants or an enum instead of passing zero directly on function calls, as that might be error-prone, to fully describe the caller's intention.
<details> <summary>Click to show 23 findings</summary>File: facets/RedemptionFacet.sol 182 m = uint256(3 ether).div(0.3 ether); 182 m = uint256(3 ether).div(0.3 ether); 185 m = uint256(1.5 ether).div(0.2 ether); 185 m = uint256(1.5 ether).div(0.2 ether); 189 m = uint256(0.75 ether).div(0.2 ether); 189 m = uint256(0.75 ether).div(0.2 ether); 193 m = uint256(0.417 ether).div(0.1 ether); 193 m = uint256(0.417 ether).div(0.1 ether); 197 m = uint256(C.ONE_THIRD.div(0.1 ether)); 289 uint256 penaltyPct = LibOrders.min( 290 LibOrders.max(LibAsset.callerFeePct(d.asset), (currentProposal.CR - disputeCR).div(currentProposal.CR)), 0.33 ether 291 ); 396 newBaseRate = LibOrders.min(newBaseRate, 1 ether); // cap baseRate at a maximum of 100% 401 uint256 redemptionRate = LibOrders.min((Asset.baseRate + 0.005 ether), 1 ether);
File: facets/ShortOrdersFacet.sol 48 incomingShort.shortRecordId = LibShortRecord.createShortRecord(asset, msg.sender, SR.Closed, 0, 0, 0, 0, 0);
File: libraries/LibBridgeRouter.sol 118 uint256 unitRethTWAP = OracleLibrary.estimateTWAP(1 ether, 30 minutes, VAULT.RETH_WETH, VAULT.RETH, C.WETH); 122 uint256 unitWstethTWAP = OracleLibrary.estimateTWAP(1 ether, 30 minutes, VAULT.WSTETH_WETH, VAULT.WSTETH, C.WETH);
File: libraries/LibOracle.sol 87 try IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes) returns (uint256 twapPrice) 133 uint256 twapPrice = IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes);
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L133:133
File: libraries/LibOrders.sol 761 bool startingShortWithinOracleRange = shortPrice <= oraclePrice.mul(1.005 ether) && prevShortPrice >= oraclePrice; 960 bool isDiscounted = savedPrice > price.mul(1.01 ether); 965 uint256 discountPct = max(0.01 ether, min(((savedPrice - price).div(savedPrice)), 0.04 ether)); 965 uint256 discountPct = max(0.01 ether, min(((savedPrice - price).div(savedPrice)), 0.04 ether)); 968 Vault.dethTitheMod = (C.MAX_TITHE - Vault.dethTithePercent).mulU16(discountPct.div(0.04 ether));
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L968:968
</details>File: libraries/UniswapOracleLibrary.sol 54 uint32[] memory secondsAgos = new uint32[](2);
This is to ensure the whole API is extracted in an interface
<details> <summary>Click to show 12 findings</summary>File: facets/BridgeRouterFacet.sol 40 function getDethTotal(uint256 vault) external view nonReentrantView returns (uint256) { 41 return vault.getDethTotal(); 42 } 51 function getBridges(uint256 vault) external view returns (address[] memory) { 52 return s.vaultBridges[vault]; 53 } 63 function deposit(address bridge, uint88 amount) external nonReentrant { 64 if (amount < C.MIN_DEPOSIT) revert Errors.UnderMinimumDeposit(); 65 66 (uint256 vault, uint256 bridgePointer) = _getVault(bridge); 67 // @dev amount after deposit might be less, if bridge takes a fee 68 uint88 dethAmount = uint88(IBridge(bridge).deposit(msg.sender, amount)); // @dev(safe-cast) 69 70 vault.addDeth(bridgePointer, dethAmount); 71 maybeUpdateYield(vault, dethAmount); 72 emit Events.Deposit(bridge, msg.sender, dethAmount); 73 } 82 function depositEth(address bridge) external payable nonReentrant { 83 if (msg.value < C.MIN_DEPOSIT) revert Errors.UnderMinimumDeposit(); 84 85 (uint256 vault, uint256 bridgePointer) = _getVault(bridge); 86 87 uint88 dethAmount = uint88(IBridge(bridge).depositEth{value: msg.value}()); // Assumes 1 ETH = 1 DETH 88 vault.addDeth(bridgePointer, dethAmount); 89 maybeUpdateYield(vault, dethAmount); 90 emit Events.DepositEth(bridge, msg.sender, dethAmount); 91 } 101 function withdraw(address bridge, uint88 dethAmount) external nonReentrant { 102 if (dethAmount == 0) revert Errors.ParameterIsZero(); 103 104 (uint256 vault, uint256 bridgePointer) = _getVault(bridge); 105 106 uint88 fee; 107 if (vault == VAULT.ONE) { 108 uint88 dethAssessable = vault.assessDeth(bridgePointer, dethAmount, rethBridge, stethBridge); 109 if (dethAssessable > 0) { 110 uint256 withdrawalFeePct = LibBridgeRouter.withdrawalFeePct(bridgePointer, rethBridge, stethBridge); 111 if (withdrawalFeePct > 0) { 112 fee = dethAssessable.mulU88(withdrawalFeePct); 113 dethAmount -= fee; 114 s.vaultUser[vault][address(this)].ethEscrowed += fee; 115 } 116 } 117 } 118 119 uint88 ethAmount = _ethConversion(vault, dethAmount); 120 vault.removeDeth(dethAmount, fee); 121 IBridge(bridge).withdraw(msg.sender, ethAmount); 122 emit Events.Withdraw(bridge, msg.sender, dethAmount, fee); 123 } 133 function withdrawTapp(address bridge, uint88 dethAmount) external onlyDAO { 134 if (dethAmount == 0) revert Errors.ParameterIsZero(); 135 136 (uint256 vault,) = _getVault(bridge); 137 uint88 ethAmount = _ethConversion(vault, dethAmount); 138 139 s.vaultUser[vault][address(this)].ethEscrowed -= dethAmount; 140 s.vault[vault].dethTotal -= dethAmount; 141 142 IBridge(bridge).withdraw(msg.sender, ethAmount); 143 emit Events.WithdrawTapp(bridge, msg.sender, dethAmount); 144 }
File: facets/PrimaryLiquidationFacet.sol 47 function liquidate(address asset, address shorter, uint8 id, uint16[] memory shortHintArray, uint16 shortOrderId) 48 external 49 isNotFrozen(asset) 50 nonReentrant 51 onlyValidShortRecord(asset, shorter, id) 52 returns (uint88, uint88) 53 { 54 if (msg.sender == shorter) revert Errors.CannotLiquidateSelf(); 55 // @dev TAPP partially reimburses gas fees, capped at 10 to limit arbitrary high cost 56 if (shortHintArray.length > 10) revert Errors.TooManyHints(); 57 58 // @dev Ensures SR has enough ercDebt/collateral to make caller fee worthwhile 59 LibSRUtil.checkCancelShortOrder({ 60 asset: asset, 61 initialStatus: s.shortRecords[asset][shorter][id].status, 62 shortOrderId: shortOrderId, 63 shortRecordId: id, 64 shorter: shorter 65 }); 66 67 // @dev liquidate requires more up-to-date oraclePrice 68 LibOrders.updateOracleAndStartingShortViaTimeBidOnly(asset, shortHintArray); 69 70 MTypes.PrimaryLiquidation memory m = _setLiquidationStruct(asset, shorter, id, shortOrderId); 71 72 // @dev Can liquidate as long as CR is low enough 73 if (m.cRatio >= LibAsset.liquidationCR(m.asset)) { 74 // If CR is too high, check for recovery mode and violation to continue liquidation 75 if (!LibSRUtil.checkRecoveryModeViolation(m.asset, m.cRatio, m.oraclePrice)) revert Errors.SufficientCollateral(); 76 } 77 78 // revert if no asks, or price too high 79 _checklowestSell(m); 80 81 _performForcedBid(m, shortHintArray); 82 83 _liquidationFeeHandler(m); 84 85 _fullorPartialLiquidation(m); 86 87 emit Events.Liquidate(asset, shorter, id, msg.sender, m.ercDebtMatched); 88 89 return (m.gasFee, m.ethFilled); 90 }
File: facets/RedemptionFacet.sol 56 function proposeRedemption( 57 address asset, 58 MTypes.ProposalInput[] calldata proposalInput, 59 uint88 redemptionAmount, 60 uint88 maxRedemptionFee 61 ) external isNotFrozen(asset) nonReentrant { 62 if (proposalInput.length > type(uint8).max) revert Errors.TooManyProposals(); 63 MTypes.ProposeRedemption memory p; 64 p.asset = asset; 65 STypes.AssetUser storage redeemerAssetUser = s.assetUser[p.asset][msg.sender]; 66 uint256 minShortErc = LibAsset.minShortErc(p.asset); 67 68 if (redemptionAmount < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 69 70 if (redeemerAssetUser.ercEscrowed < redemptionAmount) revert Errors.InsufficientERCEscrowed(); 71 72 // @dev redeemerAssetUser.SSTORE2Pointer gets reset to address(0) after actual redemption 73 if (redeemerAssetUser.SSTORE2Pointer != address(0)) revert Errors.ExistingProposedRedemptions(); 74 75 p.oraclePrice = LibOracle.getPrice(p.asset); 76 77 bytes memory slate; 78 for (uint8 i = 0; i < proposalInput.length; i++) { 79 p.shorter = proposalInput[i].shorter; 80 p.shortId = proposalInput[i].shortId; 81 p.shortOrderId = proposalInput[i].shortOrderId; 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 84 85 /// Evaluate proposed shortRecord 86 87 if (!validRedemptionSR(currentSR, msg.sender, p.shorter, minShortErc)) continue; 88 89 currentSR.updateErcDebt(p.asset); 90 p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); 91 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 93 if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; 94 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 96 if (p.totalAmountProposed + currentSR.ercDebt <= redemptionAmount) { 97 p.amountProposed = currentSR.ercDebt; 98 } else { 99 p.amountProposed = redemptionAmount - p.totalAmountProposed; 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 101 if (currentSR.ercDebt - p.amountProposed < minShortErc) break; 102 } 103 104 /// At this point, the shortRecord passes all checks and will be included in the slate 105 106 p.previousCR = p.currentCR; 107 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 110 STypes.Order storage shortOrder = s.shorts[asset][p.shortOrderId]; 111 if (currentSR.status == SR.PartialFill && shortOrder.ercAmount < minShortErc) { 112 if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); 113 LibOrders.cancelShort(asset, p.shortOrderId); 114 } 115 116 p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed); 117 if (p.colRedeemed > currentSR.collateral) { 118 p.colRedeemed = currentSR.collateral; 119 } 120 121 currentSR.collateral -= p.colRedeemed; 122 currentSR.ercDebt -= p.amountProposed; 123 124 p.totalAmountProposed += p.amountProposed; 125 p.totalColRedeemed += p.colRedeemed; 126 127 // @dev directly write the properties of MTypes.ProposalData into bytes 128 // instead of usual abi.encode to save on extra zeros being written 129 slate = bytes.concat( 130 slate, 131 bytes20(p.shorter), 132 bytes1(p.shortId), 133 bytes8(uint64(p.currentCR)), 134 bytes11(p.amountProposed), 135 bytes11(p.colRedeemed) 136 ); 137 138 LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); 139 p.redemptionCounter++; 140 if (redemptionAmount - p.totalAmountProposed < minShortErc) break; 141 } 142 143 if (p.totalAmountProposed < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 144 145 // @dev SSTORE2 the entire proposalData after validating proposalInput 146 redeemerAssetUser.SSTORE2Pointer = SSTORE2.write(slate); 147 redeemerAssetUser.slateLength = p.redemptionCounter; 148 redeemerAssetUser.oraclePrice = p.oraclePrice; 149 redeemerAssetUser.ercEscrowed -= p.totalAmountProposed; 150 151 STypes.Asset storage Asset = s.asset[p.asset]; 152 Asset.ercDebt -= p.totalAmountProposed; 153 154 uint32 protocolTime = LibOrders.getOffsetTime(); 155 redeemerAssetUser.timeProposed = protocolTime; 156 // @dev Calculate the dispute period 157 // @dev timeToDispute is immediate for shorts with CR <= 1.1x 158 159 /* 160 +-------+------------+ 161 | CR(X) | Hours(Y) | 162 +-------+------------+ 163 | 1.1 | 0 | 164 | 1.2 | .333 | 165 | 1.3 | .75 | 166 | 1.5 | 1.5 | 167 | 1.7 | 3 | 168 | 2.0 | 6 | 169 +-------+------------+ 170 171 Creating fixed points and interpolating between points on the graph without using exponentials 172 Using simple y = mx + b formula 173 174 where x = currentCR - previousCR 175 m = (y2-y1)/(x2-x1) 176 b = previous fixed point (Y) 177 */ 178 179 uint256 m; 180 181 if (p.currentCR > 1.7 ether) { 182 m = uint256(3 ether).div(0.3 ether); 183 redeemerAssetUser.timeToDispute = protocolTime + uint32((m.mul(p.currentCR - 1.7 ether) + 3 ether) * 1 hours / 1 ether); 184 } else if (p.currentCR > 1.5 ether) { 185 m = uint256(1.5 ether).div(0.2 ether); 186 redeemerAssetUser.timeToDispute = 187 protocolTime + uint32((m.mul(p.currentCR - 1.5 ether) + 1.5 ether) * 1 hours / 1 ether); 188 } else if (p.currentCR > 1.3 ether) { 189 m = uint256(0.75 ether).div(0.2 ether); 190 redeemerAssetUser.timeToDispute = 191 protocolTime + uint32((m.mul(p.currentCR - 1.3 ether) + 0.75 ether) * 1 hours / 1 ether); 192 } else if (p.currentCR > 1.2 ether) { 193 m = uint256(0.417 ether).div(0.1 ether); 194 redeemerAssetUser.timeToDispute = 195 protocolTime + uint32((m.mul(p.currentCR - 1.2 ether) + C.ONE_THIRD) * 1 hours / 1 ether); 196 } else if (p.currentCR > 1.1 ether) { 197 m = uint256(C.ONE_THIRD.div(0.1 ether)); 198 redeemerAssetUser.timeToDispute = protocolTime + uint32(m.mul(p.currentCR - 1.1 ether) * 1 hours / 1 ether); 199 } 200 201 redeemerAssetUser.oraclePrice = p.oraclePrice; 202 redeemerAssetUser.timeProposed = LibOrders.getOffsetTime(); 203 204 uint88 redemptionFee = calculateRedemptionFee(asset, p.totalColRedeemed, p.totalAmountProposed); 205 if (redemptionFee > maxRedemptionFee) revert Errors.RedemptionFeeTooHigh(); 206 207 STypes.VaultUser storage VaultUser = s.vaultUser[Asset.vault][msg.sender]; 208 if (VaultUser.ethEscrowed < redemptionFee) revert Errors.InsufficientETHEscrowed(); 209 VaultUser.ethEscrowed -= redemptionFee; 210 emit Events.ProposeRedemption(p.asset, msg.sender); 211 } 224 function disputeRedemption(address asset, address redeemer, uint8 incorrectIndex, address disputeShorter, uint8 disputeShortId) 225 external 226 isNotFrozen(asset) 227 nonReentrant 228 { 229 if (redeemer == msg.sender) revert Errors.CannotDisputeYourself(); 230 MTypes.DisputeRedemption memory d; 231 d.asset = asset; 232 d.redeemer = redeemer; 233 234 STypes.AssetUser storage redeemerAssetUser = s.assetUser[d.asset][d.redeemer]; 235 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 236 237 if (LibOrders.getOffsetTime() >= redeemerAssetUser.timeToDispute) revert Errors.TimeToDisputeHasElapsed(); 238 239 MTypes.ProposalData[] memory decodedProposalData = 240 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, redeemerAssetUser.slateLength); 241 242 for (uint256 i = 0; i < decodedProposalData.length; i++) { 243 if (decodedProposalData[i].shorter == disputeShorter && decodedProposalData[i].shortId == disputeShortId) { 244 revert Errors.CannotDisputeWithRedeemerProposal(); 245 } 246 } 247 248 STypes.ShortRecord storage disputeSR = s.shortRecords[d.asset][disputeShorter][disputeShortId]; 249 // Match continue (skip) conditions in proposeRedemption() 250 uint256 minShortErc = LibAsset.minShortErc(d.asset); 251 if (!validRedemptionSR(disputeSR, d.redeemer, disputeShorter, minShortErc)) revert Errors.InvalidRedemption(); 252 253 MTypes.ProposalData memory incorrectProposal = decodedProposalData[incorrectIndex]; 254 MTypes.ProposalData memory currentProposal; 255 STypes.Asset storage Asset = s.asset[d.asset]; 256 257 uint256 disputeCR = disputeSR.getCollateralRatio(redeemerAssetUser.oraclePrice); 258 259 if (disputeCR < incorrectProposal.CR && disputeSR.updatedAt + C.DISPUTE_REDEMPTION_BUFFER <= redeemerAssetUser.timeProposed) 260 { 261 // @dev All proposals from the incorrectIndex onward will be removed 262 // @dev Thus the proposer can only redeem a portion of their original slate 263 for (uint256 i = incorrectIndex; i < decodedProposalData.length; i++) { 264 currentProposal = decodedProposalData[i]; 265 266 STypes.ShortRecord storage currentSR = s.shortRecords[d.asset][currentProposal.shorter][currentProposal.shortId]; 267 currentSR.collateral += currentProposal.colRedeemed; 268 currentSR.ercDebt += currentProposal.ercDebtRedeemed; 269 270 d.incorrectCollateral += currentProposal.colRedeemed; 271 d.incorrectErcDebt += currentProposal.ercDebtRedeemed; 272 } 273 274 s.vault[Asset.vault].dethCollateral += d.incorrectCollateral; 275 Asset.dethCollateral += d.incorrectCollateral; 276 Asset.ercDebt += d.incorrectErcDebt; 277 278 // @dev Update the redeemer's SSTORE2Pointer 279 if (incorrectIndex > 0) { 280 redeemerAssetUser.slateLength = incorrectIndex; 281 } else { 282 // @dev this implies everything in the redeemer's proposal was incorrect 283 delete redeemerAssetUser.SSTORE2Pointer; 284 emit Events.DisputeRedemptionAll(d.asset, redeemer); 285 } 286 287 // @dev Penalty is based on the proposal with highest CR (decodedProposalData is sorted) 288 // @dev PenaltyPct is bound between CallerFeePct and 33% to prevent exploiting primaryLiquidation fees 289 uint256 penaltyPct = LibOrders.min( 290 LibOrders.max(LibAsset.callerFeePct(d.asset), (currentProposal.CR - disputeCR).div(currentProposal.CR)), 0.33 ether 291 ); 292 293 uint88 penaltyAmt = d.incorrectErcDebt.mulU88(penaltyPct); 294 295 // @dev Give redeemer back ercEscrowed that is no longer used to redeem (penalty applied) 296 redeemerAssetUser.ercEscrowed += (d.incorrectErcDebt - penaltyAmt); 297 s.assetUser[d.asset][msg.sender].ercEscrowed += penaltyAmt; 298 } else { 299 revert Errors.InvalidRedemptionDispute(); 300 } 301 } 310 function claimRedemption(address asset) external isNotFrozen(asset) nonReentrant { 311 uint256 vault = s.asset[asset].vault; 312 STypes.AssetUser storage redeemerAssetUser = s.assetUser[asset][msg.sender]; 313 STypes.VaultUser storage redeemerVaultUser = s.vaultUser[vault][msg.sender]; 314 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 315 if (LibOrders.getOffsetTime() < redeemerAssetUser.timeToDispute) revert Errors.TimeToDisputeHasNotElapsed(); 316 317 MTypes.ProposalData[] memory decodedProposalData = 318 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, redeemerAssetUser.slateLength); 319 320 uint88 totalColRedeemed; 321 for (uint256 i = 0; i < decodedProposalData.length; i++) { 322 MTypes.ProposalData memory currentProposal = decodedProposalData[i]; 323 totalColRedeemed += currentProposal.colRedeemed; 324 _claimRemainingCollateral({ 325 asset: asset, 326 vault: vault, 327 shorter: currentProposal.shorter, 328 shortId: currentProposal.shortId 329 }); 330 } 331 redeemerVaultUser.ethEscrowed += totalColRedeemed; 332 delete redeemerAssetUser.SSTORE2Pointer; 333 emit Events.ClaimRedemption(asset, msg.sender); 334 } 347 function claimRemainingCollateral(address asset, address redeemer, uint8 claimIndex, uint8 id) 348 external 349 isNotFrozen(asset) 350 nonReentrant 351 { 352 STypes.AssetUser storage redeemerAssetUser = s.assetUser[asset][redeemer]; 353 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 354 if (redeemerAssetUser.timeToDispute > LibOrders.getOffsetTime()) revert Errors.TimeToDisputeHasNotElapsed(); 355 356 // @dev Only need to read up to the position of the SR to be claimed 357 MTypes.ProposalData[] memory decodedProposalData = 358 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, claimIndex + 1); 359 MTypes.ProposalData memory claimProposal = decodedProposalData[claimIndex]; 360 361 if (claimProposal.shorter != msg.sender && claimProposal.shortId != id) revert Errors.CanOnlyClaimYourShort(); 362 363 STypes.Asset storage Asset = s.asset[asset]; 364 _claimRemainingCollateral({asset: asset, vault: Asset.vault, shorter: msg.sender, shortId: id}); 365 }
</details>File: facets/ShortOrdersFacet.sol 35 function createLimitShort( 36 address asset, 37 uint80 price, 38 uint88 ercAmount, 39 MTypes.OrderHint[] memory orderHintArray, 40 uint16[] memory shortHintArray, 41 uint16 shortOrderCR 42 ) external isNotFrozen(asset) onlyValidAsset(asset) nonReentrant { 43 MTypes.CreateLimitShortParam memory p; 44 STypes.Asset storage Asset = s.asset[asset]; 45 STypes.Order memory incomingShort; 46 47 // @dev create "empty" SR. Fail early. 48 incomingShort.shortRecordId = LibShortRecord.createShortRecord(asset, msg.sender, SR.Closed, 0, 0, 0, 0, 0); 49 50 uint256 cr = LibOrders.convertCR(shortOrderCR); 51 if ((shortOrderCR + C.BID_CR) < Asset.initialCR || cr >= C.CRATIO_MAX_INITIAL) { 52 revert Errors.InvalidCR(); 53 } 54 55 // @dev minShortErc needs to be modified (bigger) when cr < 1 56 p.minShortErc = cr < 1 ether ? LibAsset.minShortErc(asset).mul(1 ether + cr.inv()) : LibAsset.minShortErc(asset); 57 p.eth = price.mul(ercAmount); 58 p.minAskEth = LibAsset.minAskEth(asset); 59 if (ercAmount < p.minShortErc || p.eth < p.minAskEth) revert Errors.OrderUnderMinimumSize(); 60 61 // For a short, need enough collateral to cover minting ERC (calculated using initialCR) 62 if (s.vaultUser[Asset.vault][msg.sender].ethEscrowed < p.eth.mul(cr)) revert Errors.InsufficientETHEscrowed(); 63 64 incomingShort.addr = msg.sender; 65 incomingShort.price = price; 66 incomingShort.ercAmount = ercAmount; 67 incomingShort.id = Asset.orderIdCounter; 68 incomingShort.orderType = O.LimitShort; 69 incomingShort.creationTime = LibOrders.getOffsetTime(); 70 incomingShort.shortOrderCR = shortOrderCR; // 170 -> 1.70x 71 72 p.startingId = s.bids[asset][C.HEAD].nextId; 73 STypes.Order storage highestBid = s.bids[asset][p.startingId]; 74 // @dev if match and match price is gt .5% to saved oracle in either direction, update startingShortId 75 if (highestBid.price >= incomingShort.price && highestBid.orderType == O.LimitBid) { 76 LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingShort, shortHintArray); 77 } 78 79 p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(asset); 80 if (LibSRUtil.checkRecoveryModeViolation(asset, cr, p.oraclePrice)) { 81 revert Errors.BelowRecoveryModeCR(); 82 } 83 84 // @dev reading spot oracle price 85 if (incomingShort.price < p.oraclePrice) { 86 LibOrders.addShort(asset, incomingShort, orderHintArray); 87 } else { 88 LibOrders.sellMatchAlgo(asset, incomingShort, orderHintArray, p.minAskEth); 89 } 90 }
Consider using formal verification to mathematically prove that your code does what is intended, and does not have any edge cases with unexpected behavior. The solidity compiler itself has this functionality built in based off of SMTChecker.
File: Various Files None
This especially problematic when the setter also emits the same value, which may be confusing to offline parsers.
File: facets/PrimaryLiquidationFacet.sol 122 function _setLiquidationStruct(address asset, address shorter, uint8 id, uint16 shortOrderId) 123 private 124 returns (MTypes.PrimaryLiquidation memory) 125 { 126 LibShortRecord.updateErcDebt(asset, shorter, id); 127 { 128 MTypes.PrimaryLiquidation memory m; 129 m.asset = asset; 130 m.short = s.shortRecords[asset][shorter][id]; 131 m.shortOrderId = shortOrderId; 132 m.vault = s.asset[asset].vault; 133 m.shorter = shorter; 134 m.penaltyCR = LibAsset.penaltyCR(asset); 135 m.oraclePrice = LibOracle.getPrice(asset); 136 m.cRatio = m.short.getCollateralRatio(m.oraclePrice); 137 m.forcedBidPriceBuffer = LibAsset.forcedBidPriceBuffer(asset); 138 m.callerFeePct = LibAsset.callerFeePct(m.asset); 139 m.tappFeePct = LibAsset.tappFeePct(m.asset); 140 m.ethDebt = m.short.ercDebt.mul(m.oraclePrice).mul(m.forcedBidPriceBuffer).mul(1 ether + m.tappFeePct + m.callerFeePct); // ethDebt accounts for forcedBidPriceBuffer and potential fees 141 return m; 142 } 143 }
The longer a string of operations is, the harder it is to understand it. Consider splitting the full calculation into more steps, with more descriptive temporary variable names, and add extensive comments.
<details> <summary>Click to show 6 findings</summary>File: facets/RedemptionFacet.sol 186 redeemerAssetUser.timeToDispute = 187 protocolTime + uint32((m.mul(p.currentCR - 1.5 ether) + 1.5 ether) * 1 hours / 1 ether); 190 redeemerAssetUser.timeToDispute = 191 protocolTime + uint32((m.mul(p.currentCR - 1.3 ether) + 0.75 ether) * 1 hours / 1 ether); 183 redeemerAssetUser.timeToDispute = protocolTime + uint32((m.mul(p.currentCR - 1.7 ether) + 3 ether) * 1 hours / 1 ether); 194 redeemerAssetUser.timeToDispute = 195 protocolTime + uint32((m.mul(p.currentCR - 1.2 ether) + C.ONE_THIRD) * 1 hours / 1 ether);
File: libraries/LibOracle.sol 125 bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 126 || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0; 76 bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 77 || block.timestamp > 2 hours + timeStamp;
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L76:77
</details>Consider breaking down these blocks into more manageable units, by splitting things into utility functions, by reducing nesting, and by using early returns.
<details> <summary>Click to show 9 findings</summary>File: facets/RedemptionFacet.sol 224 function disputeRedemption(address asset, address redeemer, uint8 incorrectIndex, address disputeShorter, uint8 disputeShortId) 225 external 226 isNotFrozen(asset) 227 nonReentrant 228 { 229 if (redeemer == msg.sender) revert Errors.CannotDisputeYourself(); 230 MTypes.DisputeRedemption memory d; 231 d.asset = asset; 232 d.redeemer = redeemer; 233 234 STypes.AssetUser storage redeemerAssetUser = s.assetUser[d.asset][d.redeemer]; 235 if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption(); 236 237 if (LibOrders.getOffsetTime() >= redeemerAssetUser.timeToDispute) revert Errors.TimeToDisputeHasElapsed(); 238 239 MTypes.ProposalData[] memory decodedProposalData = 240 LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, redeemerAssetUser.slateLength); 241 242 for (uint256 i = 0; i < decodedProposalData.length; i++) { 243 if (decodedProposalData[i].shorter == disputeShorter && decodedProposalData[i].shortId == disputeShortId) { 244 revert Errors.CannotDisputeWithRedeemerProposal(); 245 } 246 } 247 248 STypes.ShortRecord storage disputeSR = s.shortRecords[d.asset][disputeShorter][disputeShortId]; 249 // Match continue (skip) conditions in proposeRedemption() 250 uint256 minShortErc = LibAsset.minShortErc(d.asset); 251 if (!validRedemptionSR(disputeSR, d.redeemer, disputeShorter, minShortErc)) revert Errors.InvalidRedemption(); 252 253 MTypes.ProposalData memory incorrectProposal = decodedProposalData[incorrectIndex]; 254 MTypes.ProposalData memory currentProposal; 255 STypes.Asset storage Asset = s.asset[d.asset]; 256 257 uint256 disputeCR = disputeSR.getCollateralRatio(redeemerAssetUser.oraclePrice); 258 259 if (disputeCR < incorrectProposal.CR && disputeSR.updatedAt + C.DISPUTE_REDEMPTION_BUFFER <= redeemerAssetUser.timeProposed) 260 { 261 // @dev All proposals from the incorrectIndex onward will be removed 262 // @dev Thus the proposer can only redeem a portion of their original slate 263 for (uint256 i = incorrectIndex; i < decodedProposalData.length; i++) { 264 currentProposal = decodedProposalData[i]; 265 266 STypes.ShortRecord storage currentSR = s.shortRecords[d.asset][currentProposal.shorter][currentProposal.shortId]; 267 currentSR.collateral += currentProposal.colRedeemed; 268 currentSR.ercDebt += currentProposal.ercDebtRedeemed; 269 270 d.incorrectCollateral += currentProposal.colRedeemed; 271 d.incorrectErcDebt += currentProposal.ercDebtRedeemed; 272 } 273 274 s.vault[Asset.vault].dethCollateral += d.incorrectCollateral; 275 Asset.dethCollateral += d.incorrectCollateral; 276 Asset.ercDebt += d.incorrectErcDebt; 277 278 // @dev Update the redeemer's SSTORE2Pointer 279 if (incorrectIndex > 0) { 280 redeemerAssetUser.slateLength = incorrectIndex; 281 } else { 282 // @dev this implies everything in the redeemer's proposal was incorrect 283 delete redeemerAssetUser.SSTORE2Pointer; 284 emit Events.DisputeRedemptionAll(d.asset, redeemer); 285 } 286 287 // @dev Penalty is based on the proposal with highest CR (decodedProposalData is sorted) 288 // @dev PenaltyPct is bound between CallerFeePct and 33% to prevent exploiting primaryLiquidation fees 289 uint256 penaltyPct = LibOrders.min( 290 LibOrders.max(LibAsset.callerFeePct(d.asset), (currentProposal.CR - disputeCR).div(currentProposal.CR)), 0.33 ether 291 ); 292 293 uint88 penaltyAmt = d.incorrectErcDebt.mulU88(penaltyPct); 294 295 // @dev Give redeemer back ercEscrowed that is no longer used to redeem (penalty applied) 296 redeemerAssetUser.ercEscrowed += (d.incorrectErcDebt - penaltyAmt); 297 s.assetUser[d.asset][msg.sender].ercEscrowed += penaltyAmt; 298 } else { 299 revert Errors.InvalidRedemptionDispute(); 300 } 301 } 56 function proposeRedemption( 57 address asset, 58 MTypes.ProposalInput[] calldata proposalInput, 59 uint88 redemptionAmount, 60 uint88 maxRedemptionFee 61 ) external isNotFrozen(asset) nonReentrant { 62 if (proposalInput.length > type(uint8).max) revert Errors.TooManyProposals(); 63 MTypes.ProposeRedemption memory p; 64 p.asset = asset; 65 STypes.AssetUser storage redeemerAssetUser = s.assetUser[p.asset][msg.sender]; 66 uint256 minShortErc = LibAsset.minShortErc(p.asset); 67 68 if (redemptionAmount < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 69 70 if (redeemerAssetUser.ercEscrowed < redemptionAmount) revert Errors.InsufficientERCEscrowed(); 71 72 // @dev redeemerAssetUser.SSTORE2Pointer gets reset to address(0) after actual redemption 73 if (redeemerAssetUser.SSTORE2Pointer != address(0)) revert Errors.ExistingProposedRedemptions(); 74 75 p.oraclePrice = LibOracle.getPrice(p.asset); 76 77 bytes memory slate; 78 for (uint8 i = 0; i < proposalInput.length; i++) { 79 p.shorter = proposalInput[i].shorter; 80 p.shortId = proposalInput[i].shortId; 81 p.shortOrderId = proposalInput[i].shortOrderId; 82 // @dev Setting this above _onlyValidShortRecord to allow skipping 83 STypes.ShortRecord storage currentSR = s.shortRecords[p.asset][p.shorter][p.shortId]; 84 85 /// Evaluate proposed shortRecord 86 87 if (!validRedemptionSR(currentSR, msg.sender, p.shorter, minShortErc)) continue; 88 89 currentSR.updateErcDebt(p.asset); 90 p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); 91 92 // @dev Skip if proposal is not sorted correctly or if above redemption threshold 93 if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; 94 95 // @dev totalAmountProposed tracks the actual amount that can be redeemed. totalAmountProposed <= redemptionAmount 96 if (p.totalAmountProposed + currentSR.ercDebt <= redemptionAmount) { 97 p.amountProposed = currentSR.ercDebt; 98 } else { 99 p.amountProposed = redemptionAmount - p.totalAmountProposed; 100 // @dev Exit when proposal would leave less than minShortErc, proxy for nearing end of slate 101 if (currentSR.ercDebt - p.amountProposed < minShortErc) break; 102 } 103 104 /// At this point, the shortRecord passes all checks and will be included in the slate 105 106 p.previousCR = p.currentCR; 107 108 // @dev Cancel attached shortOrder if below minShortErc, regardless of ercDebt in SR 109 // @dev All verified SR have ercDebt >= minShortErc so CR does not change in cancelShort() 110 STypes.Order storage shortOrder = s.shorts[asset][p.shortOrderId]; 111 if (currentSR.status == SR.PartialFill && shortOrder.ercAmount < minShortErc) { 112 if (shortOrder.shortRecordId != p.shortId || shortOrder.addr != p.shorter) revert Errors.InvalidShortOrder(); 113 LibOrders.cancelShort(asset, p.shortOrderId); 114 } 115 116 p.colRedeemed = p.oraclePrice.mulU88(p.amountProposed); 117 if (p.colRedeemed > currentSR.collateral) { 118 p.colRedeemed = currentSR.collateral; 119 } 120 121 currentSR.collateral -= p.colRedeemed; 122 currentSR.ercDebt -= p.amountProposed; 123 124 p.totalAmountProposed += p.amountProposed; 125 p.totalColRedeemed += p.colRedeemed; 126 127 // @dev directly write the properties of MTypes.ProposalData into bytes 128 // instead of usual abi.encode to save on extra zeros being written 129 slate = bytes.concat( 130 slate, 131 bytes20(p.shorter), 132 bytes1(p.shortId), 133 bytes8(uint64(p.currentCR)), 134 bytes11(p.amountProposed), 135 bytes11(p.colRedeemed) 136 ); 137 138 LibSRUtil.disburseCollateral(p.asset, p.shorter, p.colRedeemed, currentSR.dethYieldRate, currentSR.updatedAt); 139 p.redemptionCounter++; 140 if (redemptionAmount - p.totalAmountProposed < minShortErc) break; 141 } 142 143 if (p.totalAmountProposed < minShortErc) revert Errors.RedemptionUnderMinShortErc(); 144 145 // @dev SSTORE2 the entire proposalData after validating proposalInput 146 redeemerAssetUser.SSTORE2Pointer = SSTORE2.write(slate); 147 redeemerAssetUser.slateLength = p.redemptionCounter; 148 redeemerAssetUser.oraclePrice = p.oraclePrice; 149 redeemerAssetUser.ercEscrowed -= p.totalAmountProposed; 150 151 STypes.Asset storage Asset = s.asset[p.asset]; 152 Asset.ercDebt -= p.totalAmountProposed; 153 154 uint32 protocolTime = LibOrders.getOffsetTime(); 155 redeemerAssetUser.timeProposed = protocolTime; 156 // @dev Calculate the dispute period 157 // @dev timeToDispute is immediate for shorts with CR <= 1.1x 158 159 /* 160 +-------+------------+ 161 | CR(X) | Hours(Y) | 162 +-------+------------+ 163 | 1.1 | 0 | 164 | 1.2 | .333 | 165 | 1.3 | .75 | 166 | 1.5 | 1.5 | 167 | 1.7 | 3 | 168 | 2.0 | 6 | 169 +-------+------------+ 170 171 Creating fixed points and interpolating between points on the graph without using exponentials 172 Using simple y = mx + b formula 173 174 where x = currentCR - previousCR 175 m = (y2-y1)/(x2-x1) 176 b = previous fixed point (Y) 177 */ 178 179 uint256 m; 180 181 if (p.currentCR > 1.7 ether) { 182 m = uint256(3 ether).div(0.3 ether); 183 redeemerAssetUser.timeToDispute = protocolTime + uint32((m.mul(p.currentCR - 1.7 ether) + 3 ether) * 1 hours / 1 ether); 184 } else if (p.currentCR > 1.5 ether) { 185 m = uint256(1.5 ether).div(0.2 ether); 186 redeemerAssetUser.timeToDispute = 187 protocolTime + uint32((m.mul(p.currentCR - 1.5 ether) + 1.5 ether) * 1 hours / 1 ether); 188 } else if (p.currentCR > 1.3 ether) { 189 m = uint256(0.75 ether).div(0.2 ether); 190 redeemerAssetUser.timeToDispute = 191 protocolTime + uint32((m.mul(p.currentCR - 1.3 ether) + 0.75 ether) * 1 hours / 1 ether); 192 } else if (p.currentCR > 1.2 ether) { 193 m = uint256(0.417 ether).div(0.1 ether); 194 redeemerAssetUser.timeToDispute = 195 protocolTime + uint32((m.mul(p.currentCR - 1.2 ether) + C.ONE_THIRD) * 1 hours / 1 ether); 196 } else if (p.currentCR > 1.1 ether) { 197 m = uint256(C.ONE_THIRD.div(0.1 ether)); 198 redeemerAssetUser.timeToDispute = protocolTime + uint32(m.mul(p.currentCR - 1.1 ether) * 1 hours / 1 ether); 199 } 200 201 redeemerAssetUser.oraclePrice = p.oraclePrice; 202 redeemerAssetUser.timeProposed = LibOrders.getOffsetTime(); 203 204 uint88 redemptionFee = calculateRedemptionFee(asset, p.totalColRedeemed, p.totalAmountProposed); 205 if (redemptionFee > maxRedemptionFee) revert Errors.RedemptionFeeTooHigh(); 206 207 STypes.VaultUser storage VaultUser = s.vaultUser[Asset.vault][msg.sender]; 208 if (VaultUser.ethEscrowed < redemptionFee) revert Errors.InsufficientETHEscrowed(); 209 VaultUser.ethEscrowed -= redemptionFee; 210 emit Events.ProposeRedemption(p.asset, msg.sender); 211 }
File: facets/ShortOrdersFacet.sol 35 function createLimitShort( 36 address asset, 37 uint80 price, 38 uint88 ercAmount, 39 MTypes.OrderHint[] memory orderHintArray, 40 uint16[] memory shortHintArray, 41 uint16 shortOrderCR 42 ) external isNotFrozen(asset) onlyValidAsset(asset) nonReentrant { 43 MTypes.CreateLimitShortParam memory p; 44 STypes.Asset storage Asset = s.asset[asset]; 45 STypes.Order memory incomingShort; 46 47 // @dev create "empty" SR. Fail early. 48 incomingShort.shortRecordId = LibShortRecord.createShortRecord(asset, msg.sender, SR.Closed, 0, 0, 0, 0, 0); 49 50 uint256 cr = LibOrders.convertCR(shortOrderCR); 51 if ((shortOrderCR + C.BID_CR) < Asset.initialCR || cr >= C.CRATIO_MAX_INITIAL) { 52 revert Errors.InvalidCR(); 53 } 54 55 // @dev minShortErc needs to be modified (bigger) when cr < 1 56 p.minShortErc = cr < 1 ether ? LibAsset.minShortErc(asset).mul(1 ether + cr.inv()) : LibAsset.minShortErc(asset); 57 p.eth = price.mul(ercAmount); 58 p.minAskEth = LibAsset.minAskEth(asset); 59 if (ercAmount < p.minShortErc || p.eth < p.minAskEth) revert Errors.OrderUnderMinimumSize(); 60 61 // For a short, need enough collateral to cover minting ERC (calculated using initialCR) 62 if (s.vaultUser[Asset.vault][msg.sender].ethEscrowed < p.eth.mul(cr)) revert Errors.InsufficientETHEscrowed(); 63 64 incomingShort.addr = msg.sender; 65 incomingShort.price = price; 66 incomingShort.ercAmount = ercAmount; 67 incomingShort.id = Asset.orderIdCounter; 68 incomingShort.orderType = O.LimitShort; 69 incomingShort.creationTime = LibOrders.getOffsetTime(); 70 incomingShort.shortOrderCR = shortOrderCR; // 170 -> 1.70x 71 72 p.startingId = s.bids[asset][C.HEAD].nextId; 73 STypes.Order storage highestBid = s.bids[asset][p.startingId]; 74 // @dev if match and match price is gt .5% to saved oracle in either direction, update startingShortId 75 if (highestBid.price >= incomingShort.price && highestBid.orderType == O.LimitBid) { 76 LibOrders.updateOracleAndStartingShortViaThreshold(asset, LibOracle.getPrice(asset), incomingShort, shortHintArray); 77 } 78 79 p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(asset); 80 if (LibSRUtil.checkRecoveryModeViolation(asset, cr, p.oraclePrice)) { 81 revert Errors.BelowRecoveryModeCR(); 82 } 83 84 // @dev reading spot oracle price 85 if (incomingShort.price < p.oraclePrice) { 86 LibOrders.addShort(asset, incomingShort, orderHintArray); 87 } else { 88 LibOrders.sellMatchAlgo(asset, incomingShort, orderHintArray, p.minAskEth); 89 } 90 }
File: libraries/LibBridgeRouter.sol 39 function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) 40 internal 41 returns (uint88) 42 { 43 AppStorage storage s = appStorage(); 44 STypes.VaultUser storage VaultUser = s.vaultUser[vault][msg.sender]; 45 46 uint88 creditReth; 47 uint88 creditSteth; 48 if (bridgePointer == VAULT.BRIDGE_RETH) { 49 // Withdraw RETH 50 creditReth = VaultUser.bridgeCreditReth; 51 if (creditReth >= amount) { 52 VaultUser.bridgeCreditReth -= amount; 53 return 0; 54 } 55 56 VaultUser.bridgeCreditReth = 0; 57 amount -= creditReth; 58 creditSteth = VaultUser.bridgeCreditSteth; 59 if (creditSteth < C.ROUNDING_ZERO) { 60 // Valid withdraw when no STETH credits 61 return amount; 62 } else { 63 if (IBridge(stethBridge).getDethValue() < C.ROUNDING_ZERO) { 64 // Can withdraw RETH using STETH credit when STETH bridge is empty 65 if (creditSteth >= amount) { 66 VaultUser.bridgeCreditSteth -= amount; 67 return 0; 68 } else { 69 VaultUser.bridgeCreditSteth = 0; 70 return amount - creditSteth; 71 } 72 } else { 73 // Must use available bridge credits on withdrawal 74 // @dev Prevents abusing bridge for arbitrage 75 revert Errors.MustUseExistingBridgeCredit(); 76 } 77 } 78 } else { 79 // Withdraw STETH 80 creditSteth = VaultUser.bridgeCreditSteth; 81 if (creditSteth >= amount) { 82 VaultUser.bridgeCreditSteth -= amount; 83 return 0; 84 } 85 86 VaultUser.bridgeCreditSteth = 0; 87 amount -= creditSteth; 88 creditReth = VaultUser.bridgeCreditReth; 89 if (creditReth < C.ROUNDING_ZERO) { 90 // Valid withdraw when no RETH credits 91 return amount; 92 } else { 93 if (IBridge(rethBridge).getDethValue() < C.ROUNDING_ZERO) { 94 // Can withdraw STETH using RETH credit when RETH bridge is empty 95 if (creditReth >= amount) { 96 VaultUser.bridgeCreditReth -= amount; 97 return 0; 98 } else { 99 VaultUser.bridgeCreditReth = 0; 100 return amount - creditReth; 101 } 102 } else { 103 // Must use available bridge credits on withdrawal 104 // @dev Prevents abusing bridge for arbitrage 105 revert Errors.MustUseExistingBridgeCredit(); 106 } 107 } 108 } 109 } 144 function transferBridgeCredit(address asset, address from, address to, uint88 collateral) internal { 145 AppStorage storage s = appStorage(); 146 147 STypes.Asset storage Asset = s.asset[asset]; 148 uint256 vault = Asset.vault; 149 150 if (vault == VAULT.ONE) { 151 STypes.VaultUser storage VaultUserFrom = s.vaultUser[vault][from]; 152 uint88 creditReth = VaultUserFrom.bridgeCreditReth; 153 uint88 creditSteth = VaultUserFrom.bridgeCreditSteth; 154 STypes.VaultUser storage VaultUserTo = s.vaultUser[vault][to]; 155 156 if (creditReth < C.ROUNDING_ZERO && creditSteth < C.ROUNDING_ZERO) { 157 // No bridge credits 158 return; 159 } 160 161 if (creditReth > C.ROUNDING_ZERO && creditSteth < C.ROUNDING_ZERO) { 162 // Only creditReth 163 if (creditReth > collateral) { 164 VaultUserFrom.bridgeCreditReth -= collateral; 165 VaultUserTo.bridgeCreditReth += collateral; 166 } else { 167 VaultUserFrom.bridgeCreditReth = 0; 168 VaultUserTo.bridgeCreditReth += creditReth; 169 } 170 } else if (creditReth < C.ROUNDING_ZERO && creditSteth > C.ROUNDING_ZERO) { 171 // Only creditSteth 172 if (creditSteth > collateral) { 173 VaultUserFrom.bridgeCreditSteth -= collateral; 174 VaultUserTo.bridgeCreditSteth += collateral; 175 } else { 176 VaultUserFrom.bridgeCreditSteth = 0; 177 VaultUserTo.bridgeCreditSteth += creditSteth; 178 } 179 } else { 180 // Both creditReth and creditSteth 181 uint88 creditTotal = creditReth + creditSteth; 182 if (creditTotal > collateral) { 183 creditReth = creditReth.divU88(creditTotal).mulU88(collateral); 184 creditSteth = creditSteth.divU88(creditTotal).mulU88(collateral); 185 VaultUserFrom.bridgeCreditReth -= creditReth; 186 VaultUserFrom.bridgeCreditSteth -= creditSteth; 187 } else { 188 VaultUserFrom.bridgeCreditReth = 0; 189 VaultUserFrom.bridgeCreditSteth = 0; 190 } 191 VaultUserTo.bridgeCreditReth += creditReth; 192 VaultUserTo.bridgeCreditSteth += creditSteth; 193 } 194 } 195 }
File: libraries/LibOracle.sol 69 function baseOracleCircuitBreaker( 70 uint256 protocolPrice, 71 uint80 roundId, 72 int256 chainlinkPrice, 73 uint256 timeStamp, 74 uint256 chainlinkPriceInEth 75 ) private view returns (uint256 _protocolPrice) { 76 bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 77 || block.timestamp > 2 hours + timeStamp; 78 uint256 chainlinkDiff = 79 chainlinkPriceInEth > protocolPrice ? chainlinkPriceInEth - protocolPrice : protocolPrice - chainlinkPriceInEth; 80 bool priceDeviation = protocolPrice > 0 && chainlinkDiff.div(protocolPrice) > 0.5 ether; 81 82 // @dev if there is issue with chainlink, get twap price. Verify twap and compare with chainlink 83 if (invalidFetchData) { 84 return twapCircuitBreaker(); 85 } else if (priceDeviation) { 86 // Check valid twap price 87 try IDiamond(payable(address(this))).estimateWETHInUSDC(C.UNISWAP_WETH_BASE_AMT, 30 minutes) returns (uint256 twapPrice) 88 { 89 if (twapPrice == 0) { 90 return chainlinkPriceInEth; 91 } 92 93 uint256 twapPriceNormalized = twapPrice * (1 ether / C.DECIMAL_USDC); 94 uint256 twapPriceInEth = twapPriceNormalized.inv(); 95 uint256 twapDiff = twapPriceInEth > protocolPrice ? twapPriceInEth - protocolPrice : protocolPrice - twapPriceInEth; 96 97 // Save the price that is closest to saved oracle price 98 if (chainlinkDiff <= twapDiff) { 99 return chainlinkPriceInEth; 100 } else { 101 // Check valid twap liquidity 102 IERC20 weth = IERC20(C.WETH); 103 uint256 wethBal = weth.balanceOf(C.USDC_WETH); 104 if (wethBal < 100 ether) { 105 return chainlinkPriceInEth; 106 } 107 return twapPriceInEth; 108 } 109 } catch { 110 return chainlinkPriceInEth; 111 } 112 } else { 113 return chainlinkPriceInEth; 114 } 115 }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L69:115
File: libraries/LibOrders.sol 556 function sellMatchAlgo( 557 address asset, 558 STypes.Order memory incomingAsk, 559 MTypes.OrderHint[] memory orderHintArray, 560 uint256 minAskEth 561 ) internal { 562 AppStorage storage s = appStorage(); 563 uint16 startingId = s.bids[asset][C.HEAD].nextId; 564 if (incomingAsk.price > s.bids[asset][startingId].price) { 565 if (incomingAsk.ercAmount.mul(incomingAsk.price) >= minAskEth) { 566 addSellOrder(incomingAsk, asset, orderHintArray); 567 } 568 return; 569 } 570 // matching loop starts 571 MTypes.Match memory matchTotal; 572 while (true) { 573 STypes.Order memory highestBid = s.bids[asset][startingId]; 574 if (incomingAsk.price <= highestBid.price) { 575 // Consider ask filled if only dust amount left 576 if (incomingAsk.ercAmount.mul(highestBid.price) == 0) { 577 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, false); 578 incomingAsk.ercAmount = 0; 579 matchIncomingSell(asset, incomingAsk, matchTotal); 580 return; 581 } 582 matchHighestBid(incomingAsk, highestBid, asset, matchTotal); 583 if (incomingAsk.ercAmount > highestBid.ercAmount) { 584 incomingAsk.ercAmount -= highestBid.ercAmount; 585 highestBid.ercAmount = 0; 586 matchOrder(s.bids, asset, highestBid.id); 587 588 // loop 589 startingId = highestBid.nextId; 590 if (startingId == C.TAIL) { 591 matchIncomingSell(asset, incomingAsk, matchTotal); 592 593 if (incomingAsk.ercAmount.mul(incomingAsk.price) >= minAskEth) { 594 addSellOrder(incomingAsk, asset, orderHintArray); 595 } 596 s.bids[asset][C.HEAD].nextId = C.TAIL; 597 return; 598 } 599 } else { 600 if (incomingAsk.ercAmount == highestBid.ercAmount) { 601 matchOrder(s.bids, asset, highestBid.id); 602 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, true); 603 } else { 604 highestBid.ercAmount -= incomingAsk.ercAmount; 605 s.bids[asset][highestBid.id].ercAmount = highestBid.ercAmount; 606 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, false); 607 // Check reduced dust threshold for existing limit orders 608 if (highestBid.ercAmount.mul(highestBid.price) < LibAsset.minBidEth(asset).mul(C.DUST_FACTOR)) { 609 cancelBid(asset, highestBid.id); 610 } 611 } 612 incomingAsk.ercAmount = 0; 613 matchIncomingSell(asset, incomingAsk, matchTotal); 614 return; 615 } 616 } else { 617 updateBidOrdersOnMatch(s.bids, asset, highestBid.id, false); 618 matchIncomingSell(asset, incomingAsk, matchTotal); 619 620 if (incomingAsk.ercAmount.mul(incomingAsk.price) >= minAskEth) { 621 addSellOrder(incomingAsk, asset, orderHintArray); 622 } 623 return; 624 } 625 } 626 } 499 function updateSellOrdersOnMatch(address asset, MTypes.BidMatchAlgo memory b) internal { 500 AppStorage storage s = appStorage(); 501 if (b.matchedAskId != 0) { 502 _updateOrders(s.asks, asset, C.HEAD, b.matchedAskId); 503 } 504 505 if (b.matchedShortId != 0) { 506 if (!b.isMovingBack && !b.isMovingFwd) { 507 // @dev Handles only matching one thing 508 // @dev If does not get fully matched, b.matchedShortId == 0 and therefore not hit this block 509 _updateOrders(s.shorts, asset, b.prevShortId, b.matchedShortId); 510 } else if (!b.isMovingBack && b.isMovingFwd) { 511 // @dev Handles moving forward only 512 _updateOrders(s.shorts, asset, b.firstShortIdBelowOracle, b.matchedShortId); 513 } else if (b.isMovingBack && !b.isMovingFwd) { 514 //@handles moving backwards only. 515 _updateOrders(s.shorts, asset, b.prevShortId, b.shortHintId); 516 } else if (b.isMovingBack && b.isMovingFwd) { 517 uint16 id = b.prevShortId == b.firstShortIdBelowOracle ? b.shortHintId : b.matchedShortId; 518 // @dev Handle going backward and forward 519 _updateOrders(s.shorts, asset, b.firstShortIdBelowOracle, id); 520 } 521 } 522 } 728 function _updateOracleAndStartingShort(address asset, uint16[] memory shortHintArray) private { 729 AppStorage storage s = appStorage(); 730 uint256 oraclePrice = LibOracle.getOraclePrice(asset); 731 uint256 savedPrice = asset.getPrice(); 732 asset.setPriceAndTime(oraclePrice, getOffsetTime()); 733 if (oraclePrice == savedPrice) { 734 return; // no need to update startingShortId 735 } 736 737 STypes.Asset storage Asset = s.asset[asset]; 738 bool shortOrdersIsEmpty = s.shorts[asset][C.HEAD].nextId == C.TAIL; 739 if (shortOrdersIsEmpty) { 740 Asset.startingShortId = C.HEAD; 741 } else { 742 uint16 shortHintId; 743 for (uint256 i = 0; i < shortHintArray.length;) { 744 shortHintId = shortHintArray[i]; 745 unchecked { 746 ++i; 747 } 748 749 STypes.Order storage short = s.shorts[asset][shortHintId]; 750 { 751 O shortOrderType = short.orderType; 752 if (shortOrderType == O.Cancelled || shortOrderType == O.Matched || shortOrderType == O.Uninitialized) { 753 continue; 754 } 755 } 756 757 uint80 shortPrice = short.price; 758 uint16 prevId = short.prevId; 759 uint80 prevShortPrice = s.shorts[asset][prevId].price; 760 // @dev force hint to be within 0.5% of oraclePrice 761 bool startingShortWithinOracleRange = shortPrice <= oraclePrice.mul(1.005 ether) && prevShortPrice >= oraclePrice; 762 bool isExactStartingShort = shortPrice >= oraclePrice && prevShortPrice < oraclePrice; 763 bool allShortUnderOraclePrice = shortPrice < oraclePrice && short.nextId == C.TAIL; 764 765 if (isExactStartingShort) { 766 Asset.startingShortId = shortHintId; 767 return; 768 } else if (startingShortWithinOracleRange) { 769 // @dev prevShortPrice >= oraclePrice 770 Asset.startingShortId = prevId; 771 return; 772 } else if (allShortUnderOraclePrice) { 773 Asset.startingShortId = C.HEAD; 774 return; 775 } 776 } 777 778 revert Errors.BadShortHint(); 779 } 780 }
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L728:780
</details>The identifier is imported but never used within the file
<details> <summary>Click to show 11 findings</summary>File: facets/PrimaryLiquidationFacet.sol 10 import {STypes, MTypes, SR} from "contracts/libraries/DataTypes.sol"; 10 import {STypes, MTypes, SR} from "contracts/libraries/DataTypes.sol";
File: facets/RedemptionFacet.sol 9 import {STypes, MTypes, SR} from "contracts/libraries/DataTypes.sol"; 9 import {STypes, MTypes, SR} from "contracts/libraries/DataTypes.sol"; 19 import {console} from "contracts/libraries/console.sol";
File: facets/ShortOrdersFacet.sol 7 import {STypes, MTypes, O, SR} from "contracts/libraries/DataTypes.sol"; 7 import {STypes, MTypes, O, SR} from "contracts/libraries/DataTypes.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/facets/ShortOrdersFacet.sol#L7:7
File: libraries/LibBridgeRouter.sol 6 import {STypes} from "contracts/libraries/DataTypes.sol";
File: libraries/LibOrders.sol 9 import {STypes, MTypes, O, SR} from "contracts/libraries/DataTypes.sol"; 9 import {STypes, MTypes, O, SR} from "contracts/libraries/DataTypes.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L9:9
File: libraries/LibSRUtil.sol 6 import {STypes, SR} from "contracts/libraries/DataTypes.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L6:6
</details>Solidity follows two's complement rules for its integers, meaning that the most significant bit for signed integers is used to denote the sign, and converting between the two requires inverting all of the bits and adding one. Because of this, casting an unsigned integer to a signed one may result in a change of the sign and or magnitude of the value. For example, int8(type(uint8).max)
is not equal to type(int8).max
, but is equal to -1
. type(uint8).max
in binary is 11111111
, which if cast to a signed value, means the first binary 1
indicates a negative value, and the binary 1
s, invert to all zeroes, and when one is added, it becomes one, but negative, and therefore the decimal value of binary 11111111
is -1
.
File: libraries/UniswapOracleLibrary.sol 65 if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int32(secondsAgo) != 0)) { 62 int24 tick = int24(tickCumulativesDelta / int32(secondsAgo));
The Solidity style guide says to use mixedCase for local and state variable names. Note that while OpenZeppelin may not follow this advice, it still is the recommended way of naming variables.
<details> <summary>Click to show 82 findings</summary>File: facets/PrimaryLiquidationFacet.sol 165 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)]; 170 STypes.Asset storage Asset = s.asset[m.asset]; 210 STypes.VaultUser storage VaultUser = s.vaultUser[m.vault][msg.sender]; 211 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)]; 241 STypes.VaultUser storage TAPP = s.vaultUser[m.vault][address(this)];
File: facets/RedemptionFacet.sol 151 STypes.Asset storage Asset = s.asset[p.asset]; 207 STypes.VaultUser storage VaultUser = s.vaultUser[Asset.vault][msg.sender]; 255 STypes.Asset storage Asset = s.asset[d.asset]; 363 STypes.Asset storage Asset = s.asset[asset]; 386 STypes.Asset storage Asset = s.asset[asset];
File: facets/ShortOrdersFacet.sol 44 STypes.Asset storage Asset = s.asset[asset];
File: libraries/LibBridgeRouter.sol 23 STypes.VaultUser storage VaultUser = s.vaultUser[vault][msg.sender]; 44 STypes.VaultUser storage VaultUser = s.vaultUser[vault][msg.sender]; 147 STypes.Asset storage Asset = s.asset[asset]; 151 STypes.VaultUser storage VaultUserFrom = s.vaultUser[vault][from]; 154 STypes.VaultUser storage VaultUserTo = s.vaultUser[vault][to];
File: libraries/LibBytes.sol 24 uint64 CR; // bytes8 11 function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L11:11
File: libraries/LibOracle.sol 11 import {LibOrders} from "contracts/libraries/LibOrders.sol"; 11 import {LibOrders} from "contracts/libraries/LibOrders.sol"; 11 import {LibOrders} from "contracts/libraries/LibOrders.sol"; 11 import {LibOrders} from "contracts/libraries/LibOrders.sol"; 11 import {LibOrders} from "contracts/libraries/LibOrders.sol"; 11 import {LibOrders} from "contracts/libraries/LibOrders.sol"; 11 import {LibOrders} from "contracts/libraries/LibOrders.sol"; 11 import {LibOrders} from "contracts/libraries/LibOrders.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L11:11
File: libraries/LibOrders.sol 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 670 STypes.Asset storage Asset = s.asset[asset]; 672 STypes.Vault storage Vault = s.vault[vault]; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 737 STypes.Asset storage Asset = s.asset[asset]; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 812 STypes.Asset storage Asset = s.asset[asset]; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 891 STypes.Asset storage Asset = s.asset[asset]; 909 STypes.Vault storage Vault = s.vault[vault]; 11 import {Events} from "contracts/libraries/Events.sol"; 958 STypes.Vault storage Vault = s.vault[vault]; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol"; 11 import {Events} from "contracts/libraries/Events.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L11:11
File: libraries/LibSRUtil.sol 27 STypes.Asset storage Asset = s.asset[asset]; 29 STypes.Vault storage Vault = s.vault[vault]; 11 import {LibAsset} from "contracts/libraries/LibAsset.sol"; 11 import {LibAsset} from "contracts/libraries/LibAsset.sol"; 11 import {LibAsset} from "contracts/libraries/LibAsset.sol"; 112 STypes.Asset storage Asset = s.asset[asset]; 11 import {LibAsset} from "contracts/libraries/LibAsset.sol"; 11 import {LibAsset} from "contracts/libraries/LibAsset.sol"; 11 import {LibAsset} from "contracts/libraries/LibAsset.sol";
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L11:11
</details>File: libraries/UniswapOracleLibrary.sol 11 function observe(uint32[] calldata secondsAgos) 11 function observe(uint32[] calldata secondsAgos)
Explain to a developer any extra details
<details> <summary>Click to show 57 findings</summary>File: facets/BridgeRouterFacet.sol 29 constructor(address _rethBridge, address _stethBridge) { 40 function getDethTotal(uint256 vault) external view nonReentrantView returns (uint256) { 156 function _getVault(address bridge) private view returns (uint256 vault, uint256 bridgePointer) { 169 function _ethConversion(uint256 vault, uint88 amount) private view returns (uint88) {
File: facets/PrimaryLiquidationFacet.sol 30 constructor(address _dusd) { 122 function _setLiquidationStruct(address asset, address shorter, uint8 id, uint16 shortOrderId) 229 function min88(uint256 a, uint88 b) private pure returns (uint88) { 240 function _fullorPartialLiquidation(MTypes.PrimaryLiquidation memory m) private {
File: facets/RedemptionFacet.sol 31 function validRedemptionSR(STypes.ShortRecord storage shortRecord, address proposer, address shorter, uint256 minShortErc) 347 function claimRemainingCollateral(address asset, address redeemer, uint8 claimIndex, uint8 id) 368 function _claimRemainingCollateral(address asset, uint256 vault, address shorter, uint8 shortId) private {
File: libraries/LibBridgeRouter.sol 21 function addDeth(uint256 vault, uint256 bridgePointer, uint88 amount) internal { 39 function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) 198 function removeDeth(uint256 vault, uint88 amount, uint88 fee) internal {
File: libraries/LibBytes.sol 11 function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L11:11
File: libraries/LibOracle.sol 19 function getOraclePrice(address asset) internal view returns (uint256) { 69 function baseOracleCircuitBreaker( 117 function oracleCircuitBreaker( 131 function twapCircuitBreaker() private view returns (uint256 twapPriceInEth) { 149 function setPriceAndTime(address asset, uint256 oraclePrice, uint32 oracleTime) internal {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L149:149
File: libraries/LibOrders.sol 35 function convertCR(uint16 cr) internal pure returns (uint256) { 40 function increaseSharesOnMatch(address asset, STypes.Order memory order, MTypes.Match memory matchTotal, uint88 eth) internal { 55 function currentOrders(mapping(address => mapping(uint16 => STypes.Order)) storage orders, address asset) 78 function isShort(STypes.Order memory order) internal pure returns (bool) { 82 function addBid(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 103 function addAsk(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 128 function addShort(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 153 function addSellOrder(STypes.Order memory incomingOrder, address asset, MTypes.OrderHint[] memory orderHintArray) private { 231 function verifyBidId(address asset, uint16 _prevId, uint256 _newPrice, uint16 _nextId) 260 function verifySellId( 320 function _reuseOrderIds( 362 function findPrevOfIncomingId( 402 function verifyId( 440 function getOrderId( 532 function _updateOrders( 556 function sellMatchAlgo( 628 function matchIncomingSell(address asset, STypes.Order memory incomingOrder, MTypes.Match memory matchTotal) private { 652 function matchIncomingAsk(address asset, STypes.Order memory incomingAsk, MTypes.Match memory matchTotal) private { 668 function matchIncomingShort(address asset, STypes.Order memory incomingShort, MTypes.Match memory matchTotal) private { 728 function _updateOracleAndStartingShort(address asset, uint16[] memory shortHintArray) private { 810 function updateStartingShortIdViaShort(address asset, STypes.Order memory incomingShort) internal { 826 function findOrderHintId( 854 function cancelBid(address asset, uint16 id) internal { 868 function cancelAsk(address asset, uint16 id) internal { 882 function cancelShort(address asset, uint16 id) internal { 955 function handlePriceDiscount(address asset, uint80 price) internal { 985 function min(uint256 a, uint256 b) internal pure returns (uint256) { 989 function max(uint256 a, uint256 b) internal pure returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L989:989
File: libraries/LibSRUtil.sol 22 function disburseCollateral(address asset, address shorter, uint88 collateral, uint256 dethYieldRate, uint32 updatedAt) 49 function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 72 function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 102 function checkRecoveryModeViolation(address asset, uint256 shortRecordCR, uint256 oraclePrice) 124 function transferShortRecord(address from, address to, uint40 tokenId) internal { 151 function updateErcDebt(STypes.ShortRecord storage short, address asset) internal {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L151:151
</details>File: libraries/UniswapOracleLibrary.sol 11 function observe(uint32[] calldata secondsAgos) 28 function getQuoteAtTick(int24 tick, uint128 baseAmount, address baseToken, address quoteToken) 47 function estimateTWAP(uint128 amountIn, uint32 secondsAgo, address pool, address baseToken, address quoteToken)
Explain to an end user what this does
<details> <summary>Click to show 55 findings</summary>File: facets/BridgeRouterFacet.sol 29 constructor(address _rethBridge, address _stethBridge) { 148 function maybeUpdateYield(uint256 vault, uint88 amount) private { 156 function _getVault(address bridge) private view returns (uint256 vault, uint256 bridgePointer) { 169 function _ethConversion(uint256 vault, uint88 amount) private view returns (uint88) {
File: facets/PrimaryLiquidationFacet.sol 30 constructor(address _dusd) { 96 function _checklowestSell(MTypes.PrimaryLiquidation memory m) private view { 229 function min88(uint256 a, uint88 b) private pure returns (uint88) {
File: facets/RedemptionFacet.sol 31 function validRedemptionSR(STypes.ShortRecord storage shortRecord, address proposer, address shorter, uint256 minShortErc) 347 function claimRemainingCollateral(address asset, address redeemer, uint8 claimIndex, uint8 id) 368 function _claimRemainingCollateral(address asset, uint256 vault, address shorter, uint8 shortId) private { 382 function calculateRedemptionFee(address asset, uint88 colRedeemed, uint88 ercDebtRedeemed)
File: libraries/LibBridgeRouter.sol 21 function addDeth(uint256 vault, uint256 bridgePointer, uint88 amount) internal { 39 function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) 113 function withdrawalFeePct(uint256 bridgePointer, address rethBridge, address stethBridge) internal view returns (uint256 fee) { 144 function transferBridgeCredit(address asset, address from, address to, uint88 collateral) internal { 198 function removeDeth(uint256 vault, uint88 amount, uint88 fee) internal {
File: libraries/LibBytes.sol 11 function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L11:11
File: libraries/LibOracle.sol 19 function getOraclePrice(address asset) internal view returns (uint256) { 69 function baseOracleCircuitBreaker( 117 function oracleCircuitBreaker( 131 function twapCircuitBreaker() private view returns (uint256 twapPriceInEth) { 149 function setPriceAndTime(address asset, uint256 oraclePrice, uint32 oracleTime) internal { 156 function getTime(address asset) internal view returns (uint256 creationTime) { 162 function getPrice(address asset) internal view returns (uint80 oraclePrice) { 168 function getSavedOrSpotOraclePrice(address asset) internal view returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L168:168
File: libraries/LibOrders.sol 30 function getOffsetTime() internal view returns (uint32 timeInSeconds) { 35 function convertCR(uint16 cr) internal pure returns (uint256) { 40 function increaseSharesOnMatch(address asset, STypes.Order memory order, MTypes.Match memory matchTotal, uint88 eth) internal { 55 function currentOrders(mapping(address => mapping(uint16 => STypes.Order)) storage orders, address asset) 78 function isShort(STypes.Order memory order) internal pure returns (bool) { 82 function addBid(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 103 function addAsk(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 173 function addOrder( 320 function _reuseOrderIds( 420 function normalizeOrderType(O o) private pure returns (O newO) { 628 function matchIncomingSell(address asset, STypes.Order memory incomingOrder, MTypes.Match memory matchTotal) private { 728 function _updateOracleAndStartingShort(address asset, uint16[] memory shortHintArray) private { 783 function updateOracleAndStartingShortViaThreshold( 803 function updateOracleAndStartingShortViaTimeBidOnly(address asset, uint16[] memory shortHintArray) internal { 810 function updateStartingShortIdViaShort(address asset, STypes.Order memory incomingShort) internal { 826 function findOrderHintId( 854 function cancelBid(address asset, uint16 id) internal { 868 function cancelAsk(address asset, uint16 id) internal { 882 function cancelShort(address asset, uint16 id) internal { 955 function handlePriceDiscount(address asset, uint80 price) internal { 985 function min(uint256 a, uint256 b) internal pure returns (uint256) { 989 function max(uint256 a, uint256 b) internal pure returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L989:989
File: libraries/LibSRUtil.sol 22 function disburseCollateral(address asset, address shorter, uint88 collateral, uint256 dethYieldRate, uint32 updatedAt) 49 function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 72 function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 102 function checkRecoveryModeViolation(address asset, uint256 shortRecordCR, uint256 oraclePrice) 124 function transferShortRecord(address from, address to, uint40 tokenId) internal { 151 function updateErcDebt(STypes.ShortRecord storage short, address asset) internal {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L151:151
</details>File: libraries/UniswapOracleLibrary.sol 11 function observe(uint32[] calldata secondsAgos) 47 function estimateTWAP(uint128 amountIn, uint32 secondsAgo, address pool, address baseToken, address quoteToken)
A title that should describe the contract/interface
File: libraries/UniswapOracleLibrary.sol 10 interface IUniswapV3Pool {
The name of the author
File: libraries/UniswapOracleLibrary.sol 10 interface IUniswapV3Pool {
Explain to an end user what this does
File: libraries/UniswapOracleLibrary.sol 10 interface IUniswapV3Pool {
Explain to a developer any extra details
File: libraries/UniswapOracleLibrary.sol 10 interface IUniswapV3Pool {
A title that should describe the contract/interface
<details> <summary>Click to show 5 findings</summary>File: libraries/LibBridgeRouter.sol 16 library LibBridgeRouter {
File: libraries/LibBytes.sol 9 library LibBytes {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L9:9
File: libraries/LibOracle.sol 16 library LibOracle {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L16:16
File: libraries/LibOrders.sol 20 library LibOrders {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L20:20
File: libraries/LibSRUtil.sol 18 library LibSRUtil {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L18:18
</details>The name of the author
<details> <summary>Click to show 6 findings</summary>File: libraries/LibBridgeRouter.sol 16 library LibBridgeRouter {
File: libraries/LibBytes.sol 9 library LibBytes {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L9:9
File: libraries/LibOracle.sol 16 library LibOracle {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L16:16
File: libraries/LibOrders.sol 20 library LibOrders {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L20:20
File: libraries/LibSRUtil.sol 18 library LibSRUtil {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L18:18
</details>File: libraries/UniswapOracleLibrary.sol 21 library OracleLibrary {
Explain to an end user what this does
<details> <summary>Click to show 5 findings</summary>File: libraries/LibBridgeRouter.sol 16 library LibBridgeRouter {
File: libraries/LibBytes.sol 9 library LibBytes {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L9:9
File: libraries/LibOracle.sol 16 library LibOracle {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L16:16
File: libraries/LibOrders.sol 20 library LibOrders {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L20:20
File: libraries/LibSRUtil.sol 18 library LibSRUtil {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L18:18
</details>Explain to a developer any extra details
<details> <summary>Click to show 6 findings</summary>File: libraries/LibBridgeRouter.sol 16 library LibBridgeRouter {
File: libraries/LibBytes.sol 9 library LibBytes {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L9:9
File: libraries/LibOracle.sol 16 library LibOracle {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L16:16
File: libraries/LibOrders.sol 20 library LibOrders {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L20:20
File: libraries/LibSRUtil.sol 18 library LibSRUtil {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L18:18
</details>File: libraries/UniswapOracleLibrary.sol 21 library OracleLibrary {
title that should describe the contract
<details> <summary>Click to show 4 findings</summary>File: facets/BridgeRouterFacet.sol 18 contract BridgeRouterFacet is Modifiers {
File: facets/PrimaryLiquidationFacet.sol 21 contract PrimaryLiquidationFacet is Modifiers {
File: facets/RedemptionFacet.sol 21 contract RedemptionFacet is Modifiers {
</details>File: facets/ShortOrdersFacet.sol 18 contract ShortOrdersFacet is Modifiers {
The name of the author
<details> <summary>Click to show 4 findings</summary>File: facets/BridgeRouterFacet.sol 18 contract BridgeRouterFacet is Modifiers {
File: facets/PrimaryLiquidationFacet.sol 21 contract PrimaryLiquidationFacet is Modifiers {
File: facets/RedemptionFacet.sol 21 contract RedemptionFacet is Modifiers {
</details>File: facets/ShortOrdersFacet.sol 18 contract ShortOrdersFacet is Modifiers {
Explain to an end user what this does
<details> <summary>Click to show 4 findings</summary>File: facets/BridgeRouterFacet.sol 18 contract BridgeRouterFacet is Modifiers {
File: facets/PrimaryLiquidationFacet.sol 21 contract PrimaryLiquidationFacet is Modifiers {
File: facets/RedemptionFacet.sol 21 contract RedemptionFacet is Modifiers {
</details>File: facets/ShortOrdersFacet.sol 18 contract ShortOrdersFacet is Modifiers {
Explain to a developer any extra details
<details> <summary>Click to show 4 findings</summary>File: facets/BridgeRouterFacet.sol 18 contract BridgeRouterFacet is Modifiers {
File: facets/PrimaryLiquidationFacet.sol 21 contract PrimaryLiquidationFacet is Modifiers {
File: facets/RedemptionFacet.sol 21 contract RedemptionFacet is Modifiers {
</details>File: facets/ShortOrdersFacet.sol 18 contract ShortOrdersFacet is Modifiers {
Explain to an end user what this does
File: facets/BridgeRouterFacet.sol 26 address private immutable rethBridge; 27 address private immutable stethBridge;
File: facets/PrimaryLiquidationFacet.sol 28 address private immutable dusd;
Explain to a developer any extra details
File: facets/BridgeRouterFacet.sol 26 address private immutable rethBridge; 27 address private immutable stethBridge;
File: facets/PrimaryLiquidationFacet.sol 28 address private immutable dusd;
Documents the return variables of a contract’s function
<details> <summary>Click to show 31 findings</summary>File: facets/BridgeRouterFacet.sol 40 function getDethTotal(uint256 vault) external view nonReentrantView returns (uint256) { 51 function getBridges(uint256 vault) external view returns (address[] memory) { 156 function _getVault(address bridge) private view returns (uint256 vault, uint256 bridgePointer) { 169 function _ethConversion(uint256 vault, uint88 amount) private view returns (uint88) {
File: facets/PrimaryLiquidationFacet.sol 229 function min88(uint256 a, uint88 b) private pure returns (uint88) {
File: facets/RedemptionFacet.sol 31 function validRedemptionSR(STypes.ShortRecord storage shortRecord, address proposer, address shorter, uint256 minShortErc) 382 function calculateRedemptionFee(address asset, uint88 colRedeemed, uint88 ercDebtRedeemed)
File: libraries/LibBridgeRouter.sol 39 function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) 113 function withdrawalFeePct(uint256 bridgePointer, address rethBridge, address stethBridge) internal view returns (uint256 fee) {
File: libraries/LibBytes.sol 11 function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L11:11
File: libraries/LibOracle.sol 19 function getOraclePrice(address asset) internal view returns (uint256) { 69 function baseOracleCircuitBreaker( 131 function twapCircuitBreaker() private view returns (uint256 twapPriceInEth) { 156 function getTime(address asset) internal view returns (uint256 creationTime) { 162 function getPrice(address asset) internal view returns (uint80 oraclePrice) { 168 function getSavedOrSpotOraclePrice(address asset) internal view returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L168:168
File: libraries/LibOrders.sol 30 function getOffsetTime() internal view returns (uint32 timeInSeconds) { 35 function convertCR(uint16 cr) internal pure returns (uint256) { 55 function currentOrders(mapping(address => mapping(uint16 => STypes.Order)) storage orders, address asset) 78 function isShort(STypes.Order memory order) internal pure returns (bool) { 362 function findPrevOfIncomingId( 420 function normalizeOrderType(O o) private pure returns (O newO) { 440 function getOrderId( 826 function findOrderHintId( 985 function min(uint256 a, uint256 b) internal pure returns (uint256) { 989 function max(uint256 a, uint256 b) internal pure returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L989:989
File: libraries/LibSRUtil.sol 49 function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 72 function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 102 function checkRecoveryModeViolation(address asset, uint256 shortRecordCR, uint256 oraclePrice)
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L102:102
</details>File: libraries/UniswapOracleLibrary.sol 11 function observe(uint32[] calldata secondsAgos) 47 function estimateTWAP(uint128 amountIn, uint32 secondsAgo, address pool, address baseToken, address quoteToken)
Documents a parameter just like in Doxygen (must be followed by parameter name)
<details> <summary>Click to show 54 findings</summary>File: facets/BridgeRouterFacet.sol 29 constructor(address _rethBridge, address _stethBridge) { 148 function maybeUpdateYield(uint256 vault, uint88 amount) private { 156 function _getVault(address bridge) private view returns (uint256 vault, uint256 bridgePointer) { 169 function _ethConversion(uint256 vault, uint88 amount) private view returns (uint88) {
File: facets/PrimaryLiquidationFacet.sol 30 constructor(address _dusd) { 96 function _checklowestSell(MTypes.PrimaryLiquidation memory m) private view { 229 function min88(uint256 a, uint88 b) private pure returns (uint88) {
File: facets/RedemptionFacet.sol 31 function validRedemptionSR(STypes.ShortRecord storage shortRecord, address proposer, address shorter, uint256 minShortErc) 347 function claimRemainingCollateral(address asset, address redeemer, uint8 claimIndex, uint8 id) 368 function _claimRemainingCollateral(address asset, uint256 vault, address shorter, uint8 shortId) private { 382 function calculateRedemptionFee(address asset, uint88 colRedeemed, uint88 ercDebtRedeemed)
File: libraries/LibBridgeRouter.sol 21 function addDeth(uint256 vault, uint256 bridgePointer, uint88 amount) internal { 39 function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) 113 function withdrawalFeePct(uint256 bridgePointer, address rethBridge, address stethBridge) internal view returns (uint256 fee) { 144 function transferBridgeCredit(address asset, address from, address to, uint88 collateral) internal { 198 function removeDeth(uint256 vault, uint88 amount, uint88 fee) internal {
File: libraries/LibBytes.sol 11 function readProposalData(address SSTORE2Pointer, uint8 slateLength) internal view returns (MTypes.ProposalData[] memory) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L11:11
File: libraries/LibOracle.sol 19 function getOraclePrice(address asset) internal view returns (uint256) { 69 function baseOracleCircuitBreaker( 117 function oracleCircuitBreaker( 131 function twapCircuitBreaker() private view returns (uint256 twapPriceInEth) { 149 function setPriceAndTime(address asset, uint256 oraclePrice, uint32 oracleTime) internal { 156 function getTime(address asset) internal view returns (uint256 creationTime) { 162 function getPrice(address asset) internal view returns (uint80 oraclePrice) { 168 function getSavedOrSpotOraclePrice(address asset) internal view returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L168:168
File: libraries/LibOrders.sol 30 function getOffsetTime() internal view returns (uint32 timeInSeconds) { 35 function convertCR(uint16 cr) internal pure returns (uint256) { 40 function increaseSharesOnMatch(address asset, STypes.Order memory order, MTypes.Match memory matchTotal, uint88 eth) internal { 55 function currentOrders(mapping(address => mapping(uint16 => STypes.Order)) storage orders, address asset) 78 function isShort(STypes.Order memory order) internal pure returns (bool) { 82 function addBid(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 103 function addAsk(address asset, STypes.Order memory order, MTypes.OrderHint[] memory orderHintArray) internal { 173 function addOrder( 320 function _reuseOrderIds( 420 function normalizeOrderType(O o) private pure returns (O newO) { 628 function matchIncomingSell(address asset, STypes.Order memory incomingOrder, MTypes.Match memory matchTotal) private { 728 function _updateOracleAndStartingShort(address asset, uint16[] memory shortHintArray) private { 783 function updateOracleAndStartingShortViaThreshold( 803 function updateOracleAndStartingShortViaTimeBidOnly(address asset, uint16[] memory shortHintArray) internal { 810 function updateStartingShortIdViaShort(address asset, STypes.Order memory incomingShort) internal { 826 function findOrderHintId( 854 function cancelBid(address asset, uint16 id) internal { 868 function cancelAsk(address asset, uint16 id) internal { 882 function cancelShort(address asset, uint16 id) internal { 955 function handlePriceDiscount(address asset, uint80 price) internal { 985 function min(uint256 a, uint256 b) internal pure returns (uint256) { 989 function max(uint256 a, uint256 b) internal pure returns (uint256) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L989:989
File: libraries/LibSRUtil.sol 22 function disburseCollateral(address asset, address shorter, uint88 collateral, uint256 dethYieldRate, uint32 updatedAt) 49 function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 72 function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 102 function checkRecoveryModeViolation(address asset, uint256 shortRecordCR, uint256 oraclePrice) 124 function transferShortRecord(address from, address to, uint40 tokenId) internal { 151 function updateErcDebt(STypes.ShortRecord storage short, address asset) internal {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L151:151
</details>File: libraries/UniswapOracleLibrary.sol 47 function estimateTWAP(uint128 amountIn, uint32 secondsAgo, address pool, address baseToken, address quoteToken)
Sensitive setter functions in smart contracts often alter critical state variables. Without events emitted in these functions, external observers or dApps cannot easily track or react to these state changes. Missing events can obscure contract activity, hampering transparency and making integration more challenging. To resolve this, incorporate appropriate event emissions within these functions. Events offer an efficient way to log crucial changes, aiding in real-time tracking and post-transaction verification..
File: facets/PrimaryLiquidationFacet.sol 122 function _setLiquidationStruct(address asset, address shorter, uint8 id, uint16 shortOrderId) 123 private 124 returns (MTypes.PrimaryLiquidation memory) 125 { 126 LibShortRecord.updateErcDebt(asset, shorter, id); 127 { 128 MTypes.PrimaryLiquidation memory m; 129 m.asset = asset; 130 m.short = s.shortRecords[asset][shorter][id]; 131 m.shortOrderId = shortOrderId; 132 m.vault = s.asset[asset].vault; 133 m.shorter = shorter; 134 m.penaltyCR = LibAsset.penaltyCR(asset); 135 m.oraclePrice = LibOracle.getPrice(asset); 136 m.cRatio = m.short.getCollateralRatio(m.oraclePrice); 137 m.forcedBidPriceBuffer = LibAsset.forcedBidPriceBuffer(asset); 138 m.callerFeePct = LibAsset.callerFeePct(m.asset); 139 m.tappFeePct = LibAsset.tappFeePct(m.asset); 140 m.ethDebt = m.short.ercDebt.mul(m.oraclePrice).mul(m.forcedBidPriceBuffer).mul(1 ether + m.tappFeePct + m.callerFeePct); // ethDebt accounts for forcedBidPriceBuffer and potential fees 141 return m; 142 } 143 }
The file is never imported by any other source file. If the file is needed for tests, it should be moved to a test directory
<details> <summary>Click to show 6 findings</summary>File: libraries/LibBridgeRouter.sol /// @auditbase `gitio/libraries/LibBridgeRouter.sol` not used in any contracts
File: libraries/LibBytes.sol /// @auditbase `gitio/libraries/LibBytes.sol` not used in any contracts
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibBytes.sol#L1:1
File: libraries/LibOracle.sol /// @auditbase `gitio/libraries/LibOracle.sol` not used in any contracts
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L1:1
File: libraries/LibOrders.sol /// @auditbase `gitio/libraries/LibOrders.sol` not used in any contracts
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L1:1
File: libraries/LibSRUtil.sol /// @auditbase `gitio/libraries/LibSRUtil.sol` not used in any contracts
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L1:1
</details>File: libraries/UniswapOracleLibrary.sol /// @auditbase `gitio/libraries/UniswapOracleLibrary.sol` not used in any contracts
Combining multiple libraries, interfaces, or contracts in a single file can lead to clutter, reduced readability, and versioning issues. Resolution: Adopt the best practice of defining only one library, interface, or contract per Solidity file. This modular approach enhances clarity, simplifies unit testing, and streamlines code review. Furthermore, segregating components makes version management easier, as updates to one component won't necessitate changes to a file housing multiple unrelated components. Structured file management can further assist in avoiding naming collisions and ensure smoother integration into larger systems or DApps.
File: libraries/UniswapOracleLibrary.sol 10 interface IUniswapV3Pool { 11 function observe(uint32[] calldata secondsAgos) 12 external 13 view 14 returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); 15 } 16 17 /* solhint-disable */ 18 19 /// @title Oracle library 20 /// @notice Provides functions to integrate with V3 pool oracle 21 library OracleLibrary {
Using a struct to encapsulate multiple parameters in Solidity functions can significantly enhance code readability and maintainability. Instead of passing a long list of arguments, which can be error-prone and hard to manage, a struct allows grouping related data into a single, coherent entity. This approach simplifies function signatures and makes the code more organized. It also enhances code clarity, as developers can easily understand the relationship between the parameters. Moreover, it aids in future code modifications and expansions, as adding or modifying a parameter only requires changes in the struct definition, rather than in every function that uses these parameters.
<details> <summary>Click to show 14 findings</summary>File: facets/PrimaryLiquidationFacet.sol 47 function liquidate(address asset, address shorter, uint8 id, uint16[] memory shortHintArray, uint16 shortOrderId) 48 external 49 isNotFrozen(asset) 50 nonReentrant 51 onlyValidShortRecord(asset, shorter, id) 52 returns (uint88, uint88) 53 {
File: facets/RedemptionFacet.sol 224 function disputeRedemption(address asset, address redeemer, uint8 incorrectIndex, address disputeShorter, uint8 disputeShortId) 225 external 226 isNotFrozen(asset) 227 nonReentrant 228 {
File: facets/ShortOrdersFacet.sol 35 function createLimitShort( 36 address asset, 37 uint80 price, 38 uint88 ercAmount, 39 MTypes.OrderHint[] memory orderHintArray, 40 uint16[] memory shortHintArray, 41 uint16 shortOrderCR 42 ) external isNotFrozen(asset) onlyValidAsset(asset) nonReentrant {
File: libraries/LibBridgeRouter.sol 39 function assessDeth(uint256 vault, uint256 bridgePointer, uint88 amount, address rethBridge, address stethBridge) 40 internal 41 returns (uint88) 42 {
File: libraries/LibOracle.sol 69 function baseOracleCircuitBreaker( 70 uint256 protocolPrice, 71 uint80 roundId, 72 int256 chainlinkPrice, 73 uint256 timeStamp, 74 uint256 chainlinkPriceInEth 75 ) private view returns (uint256 _protocolPrice) { 117 function oracleCircuitBreaker( 118 uint80 roundId, 119 uint80 baseRoundId, 120 int256 chainlinkPrice, 121 int256 baseChainlinkPrice, 122 uint256 timeStamp, 123 uint256 baseTimeStamp 124 ) private view {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOracle.sol#L117:124
File: libraries/LibOrders.sol 260 function verifySellId( 261 mapping(address => mapping(uint16 => STypes.Order)) storage orders, 262 address asset, 263 uint16 _prevId, 264 uint256 _newPrice, 265 uint16 _nextId 266 ) private view returns (int256 direction) { 320 function _reuseOrderIds( 321 mapping(address => mapping(uint16 => STypes.Order)) storage orders, 322 address asset, 323 uint16 id, 324 uint16 prevHEAD, 325 O cancelledOrMatched 326 ) private { 402 function verifyId( 403 mapping(address => mapping(uint16 => STypes.Order)) storage orders, 404 address asset, 405 uint16 prevId, 406 uint256 newPrice, 407 uint16 nextId, 408 O orderType 409 ) internal view returns (int256 direction) { 440 function getOrderId( 441 mapping(address => mapping(uint16 => STypes.Order)) storage orders, 442 address asset, 443 int256 direction, 444 uint16 hintId, 445 uint256 _newPrice, 446 O orderType 447 ) internal view returns (uint16 _hintId) {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibOrders.sol#L440:447
File: libraries/LibSRUtil.sol 22 function disburseCollateral(address asset, address shorter, uint88 collateral, uint256 dethYieldRate, uint32 updatedAt) 23 internal 24 { 49 function checkCancelShortOrder(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 50 internal 51 returns (bool isCancelled) 52 { 72 function checkShortMinErc(address asset, SR initialStatus, uint16 shortOrderId, uint8 shortRecordId, address shorter) 73 internal 74 returns (bool isCancelled) 75 {
https://github.com/code-423n4/2024-03-dittoeth//blob/main/contracts/libraries/LibSRUtil.sol#L72:75
</details>File: libraries/UniswapOracleLibrary.sol 47 function estimateTWAP(uint128 amountIn, uint32 secondsAgo, address pool, address baseToken, address quoteToken) 48 internal 49 view 50 returns (uint256 amountOut) 51 {
#0 - c4-pre-sort
2024-04-08T02:42:59Z
raymondfam marked the issue as sufficient quality report
#1 - raymondfam
2024-04-08T02:49:30Z
This bot report should be deemed high quality but there could only be a winner for sponsor's review.
#2 - c4-judge
2024-04-17T07:56:32Z
hansfriese marked the issue as grade-a
🌟 Selected for report: popeye
Also found by: 0xbrett8571, JcFichtner, LinKenji, Rhaydden, SAQ, Sathish9098, albahaca, clara, emerald7017, fouzantanveer, foxb868, hunter_w3b, kaveyjoe, roguereggiant
236.6726 USDC - $236.67
Overview: The Ditto protocol emerges as a pioneering solution within the Ethereum ecosystem, introducing a decentralized stable asset protocol poised to redefine the landscape of decentralized finance (DeFi). By leveraging overcollateralized liquid staked ETH (rETH, stETH), Ditto facilitates the creation of stablecoins through a meticulously crafted gas-optimized orderbook. At its core, Ditto aims to provide users with a seamless experience in generating and managing stable assets, starting with the introduction of a USD stablecoin, dUSD.
Foundations: Built upon the foundational principles of cryptocurrencies, DittoETH prioritizes core values such as censorship resistance, neutrality, and permissionless transactions. Drawing inspiration from Bitshares' innovative polymorphic digital assets (PMDA) concept, DittoETH employs an orderbook model, ensuring transparent and efficient asset issuance and trading mechanisms.
Pegged Asset Issuance: Central to DittoETH's functionality is the issuance of pegged assets, ERC-20 tokens designed to track the price of targeted assets. Users can mint these DittoAssets by collateralizing staked ETH, providing them with exposure to various financial instruments while preserving the benefits of decentralization.
Market Collateralization: DittoETH's market collateralization framework relies on staked ETH derivatives and ETH itself to ensure the stability and solvency of the protocol. Shorters play a pivotal role in minting pegged assets, earning rewards and additional collateral from matched long traders, thereby enhancing liquidity and market resilience.
Refinements and Innovations: Drawing from the experiences of predecessors like MakerDAO, Synthetix, and Liquity, DittoETH introduces refinements and innovations to address existing limitations. By exclusively using staked ETH as collateral and implementing an orderbook model, DittoETH offers a more resilient and scalable solution for stable asset creation and management.
Key Features and Mechanisms: DittoETH incorporates several key features and mechanisms to ensure stability and sustainability within the protocol. This includes an incentive structure for shorters, robust liquidation parameters, and the establishment of the Treasury Asset Protection Pool (TAPP) to safeguard against market volatility.
Integration of Oracles: To maintain the stability and peg of DittoAssets, the protocol integrates oracles from Chainlink, providing reliable price feeds essential for accurate asset valuation and management.
Conclusion: In summary, the Ditto protocol represents a significant leap forward in the realm of decentralized finance, offering a robust and efficient solution for creating and managing stable assets. With its innovative approach, commitment to decentralization, and emphasis on security, DittoETH sets a new standard for stability and reliability in the DeFi landscape, promising a future of decentralized financial empowerment.
my approach to evaluating the Ditto Protocol's codebase involved a thorough analysis of its contracts and facets. assessing its functionality and key features for efficient trading. We also analyzed the architecture, identified components, and evaluated code quality, highlighting security concerns and centralization risks. Additionally, we reviewed the bid matching algorithm and oracle integration, conducting a risk analysis to identify potential vulnerabilities. This comprehensive approach provided stakeholders with valuable insights to make informed decisions about deploying and operating the protocol.
Explanation of the contract:
BidOrdersFacet
. It encompasses functionality related to the creation and processing of bid orders within a market. The contract contains methods for creating bid orders, matching bids with existing sell orders (asks or shorts), and settling matched bids. The contract is structured to handle various scenarios related to bid order creation, matching, and settlement within the context of a market environment.
Functionality of the contract:
Bid Order Creation: The contract facilitates the creation of bid orders in a market. These bid orders can be either limit orders or market orders. The parameters required for creating a bid order include the market impacted, unit price in Ether for the ERC token being traded, the amount of ERC token to buy, and additional parameters related to gas-optimized sorted placement and matching.
Bid Matching Algorithm: The contract implements a bid matching algorithm (bidMatchAlgo
) that determines how incoming bid orders are matched with existing sell orders on the market. It handles scenarios where bid orders match with existing sell orders, considering factors such as price, order type, and availability of matching sell orders.
Settlement of Matched Bids: Once a bid order is matched with existing sell orders, the contract settles the matched bids by transferring assets (Ether and ERC tokens) between the involved parties. It also updates relevant data structures and variables to reflect the execution of the matched bids.
Key Features:
Bid Order Creation: The contract provides functions (createBid
and createForcedBid
) for creating bid orders in the market. These functions ensure that bid orders are created with necessary parameters and adhere to certain conditions specified within the contract.
Bid Matching Algorithm: The contract implements a sophisticated bid matching algorithm (bidMatchAlgo
) that determines how bid orders are matched with existing sell orders on the market. This algorithm considers factors such as price, order type, and availability of matching sell orders, ensuring efficient and fair matching of bids.
Settlement of Matched Bids: Once bid orders are successfully matched with existing sell orders, the contract settles the matched bids by transferring assets between the involved parties. It also updates relevant data structures and variables to reflect the execution of the matched bids, ensuring accurate tracking of market activity.
Overall, the contract provides comprehensive functionality for managing bid orders within a market environment, including creation, matching, and settlement of bids, thereby facilitating efficient trading activities on the platform.
Explanation of the contract:
ShortOrdersFacet
. It is part of a larger system, likely a decentralized finance (DeFi) protocol, and specifically focuses on managing short orders within a market system. The contract facilitates the creation of limit short orders, allowing users to sell ERC tokens short in the market.
Functionality of the contract:
Creation of Limit Short Orders: The primary functionality of the contract is to create limit short orders within the market system. Users can specify the asset, unit price in ETH for the ERC tokens sold short, the amount of ERC tokens to be sold short, and additional parameters such as order hints and the initial collateral ratio for the short order.
Validation and Processing: Upon receiving the parameters for creating a limit short order, the contract validates various conditions such as the collateral ratio, minimum order size, and user escrowed ETH balance to ensure the integrity of the system. It then processes the order by interacting with other components of the system, such as the asset storage, order book, and oracle.
Oracle Interaction: The contract interacts with an oracle to obtain the current price of the asset being traded. This price is used for validation and decision-making during the creation of limit short orders.
Matching Algorithms: The contract includes algorithms for matching short orders with existing bids in the order book. It considers factors such as price thresholds and recovery mode violations to determine the appropriate action for processing the short order.
Key Features:
Limit Short Order Creation: Users can create limit short orders, specifying the price and quantity of ERC tokens to be sold short. This feature allows traders to profit from price declines in the asset's value.
Validation Checks: The contract performs various validation checks to ensure that limit short orders meet specific criteria, such as minimum order size and collateral requirements. This helps maintain the integrity and stability of the market system.
Oracle Integration: By interacting with an oracle, the contract obtains real-time price information, which is crucial for making informed decisions during the creation and processing of short orders. This enhances the accuracy and efficiency of the trading system.
Dynamic Matching Algorithms: The contract includes dynamic matching algorithms that consider factors such as current market conditions and recovery mode violations when processing short orders. This adaptive approach ensures optimal execution of orders and mitigates risks associated with market volatility.
Overall, the contract plays a vital role in facilitating short selling activities within the market system, offering users a mechanism to profit from downward price movements while maintaining the stability and integrity of the overall protocol.
Explanation of the contract:
PrimaryLiquidationFacet
. It is part of a larger system, likely a decentralized finance (DeFi) protocol, and specifically focuses on the primary method of liquidating short positions within this system. The contract facilitates the liquidation of short positions by forcing the shorter to place a bid on the market, effectively closing their position.
Functionality of the contract:
Liquidation of Short Positions: The primary functionality of the contract is to liquidate short positions within the market system. When triggered, this contract forces the shorter to place a bid on the market to cover their short position. This process involves interacting with various components of the system, such as short records, asset storage, and the order book.
Validation and Processing: Upon receiving a request for liquidation, the contract performs various validation checks to ensure the integrity and validity of the process. It checks factors such as the collateral ratio of the short position, the availability of eligible sell orders in the market, and the ability of the shorter to cover the bid cost.
Forced Bid Execution: Once the necessary validations are complete, the contract executes the forced bid process. This involves setting up and executing a bid on behalf of the shorter to cover their short position. The shorter bears the cost of this forced bid on the market.
Fee Handling: The contract handles the distribution of fees associated with the liquidation process. These fees include caller fees, TAPP (Teller Auction Pricing Protocol) fees, and gas fees. The contract ensures that fees are distributed appropriately between the TAPP and the caller, considering factors such as available collateral and the outcome of the liquidation.
Accounting for Liquidation Results: Depending on whether the liquidation is successful or partial, the contract adjusts the relevant accounting entries accordingly. It updates the short records, disburse collateral, and handles the transfer of assets between parties involved in the liquidation process.
Key Features:
Forced Bid Liquidation: The contract implements a forced bid mechanism to liquidate short positions in the market system. This mechanism ensures that shorters fulfill their obligations by placing bids to cover their short positions, even if they are unwilling or unable to do so voluntarily.
Validation Checks: The contract performs thorough validation checks to ensure the integrity and validity of the liquidation process. It verifies factors such as collateral ratios, market conditions, and the availability of sell orders before proceeding with the liquidation.
Fee Distribution: The contract handles the distribution of fees associated with the liquidation process, ensuring fair and transparent allocation of fees between the TAPP and the caller. This feature enhances the efficiency and reliability of the liquidation mechanism.
Accounting for Results: Depending on the outcome of the liquidation process, the contract adjusts relevant accounting entries to reflect the results accurately. It handles full and partial liquidations differently, updating short records and collateral disbursements accordingly.
Overall, the contract plays a crucial role in maintaining the stability and integrity of the market system by facilitating the efficient and effective liquidation of short positions. It provides a robust mechanism for enforcing obligations and managing risk within the decentralized finance ecosystem.
Explanation of the contract:
BridgeRouterFacet
. It serves as a router for interacting with bridges in a decentralized finance (DeFi) protocol. The contract facilitates depositing and withdrawing assets (referred to as LST - Liquidity Staking Tokens) into and out of the protocol via various bridges. Bridges are smart contracts responsible for bridging assets between different blockchain networks.
Functionality of the contract:
Bridge Management: The contract manages interactions with different bridges by facilitating depositing and withdrawing assets through these bridges. Each bridge corresponds to a specific asset or token, allowing users to interact with different assets supported by the protocol.
Deposit Functionality: Users can deposit LST tokens into the protocol via specific bridges. The contract handles the deposit process, ensuring that the deposited tokens are converted into an equivalent amount of a protocol-specific asset called dETH (deposited ETH). The deposited assets are credited to the user's account within the protocol.
Withdrawal Functionality: Users can withdraw dETH from the protocol, converting it back to LST tokens via the specified bridge. The contract manages the withdrawal process, ensuring that users receive the correct amount of LST tokens equivalent to their dETH holdings. Withdrawal credits may be applicable in certain scenarios, particularly for vaults holding multiple unique LST tokens.
Bridge Management Functions: The contract includes functions for retrieving information about bridges associated with a specific vault, such as getting the present value of all bridges within a vault and fetching an array of bridge addresses for a given vault.
Yield Rate Updates: The contract automatically updates the yield rate for a vault when bridge deposits are sufficiently large. This feature aims to deter attempts to take advantage of long delays between updates to the yield rate by creating large temporary positions.
Key Features:
Bridge Interaction: The contract enables seamless interaction with bridges, allowing users to deposit and withdraw assets across different blockchain networks. This feature enhances interoperability and liquidity within the protocol by supporting various assets.
Deposit and Withdrawal Management: The contract manages the deposit and withdrawal processes, ensuring accurate conversion between LST tokens and dETH assets. It also handles withdrawal credits for vaults holding multiple unique LST tokens, enhancing user experience and security.
Automatic Yield Rate Updates: The contract includes a mechanism for automatically updating the yield rate for a vault based on the size of bridge deposits. This feature enhances the protocol's efficiency and responsiveness to market dynamics, ensuring optimal yield generation for users.
Overall, the contract serves as a critical component of the protocol's infrastructure, facilitating efficient asset bridging, deposit, and withdrawal operations while optimizing yield generation for users.
Explanation of the contract:
ExitShortFacet
, which is likely part of a larger DeFi protocol or system. Its primary purpose is to facilitate the exit of short positions within the system. The contract provides several methods for shorters to exit their positions based on their preferences and available assets.
Functionality of the contract:
Exiting Short Positions:
exitShortWallet
: Allows shorters to exit using ERC tokens from their wallet (e.g., MetaMask).exitShortErcEscrowed
: Enables shorters to exit using ERC tokens from their balance (ErcEscrowed).exitShort
: Facilitates exiting shorts by placing bids on the market.Partial Exit Support:
Validation and Processing:
Bid Placement for Exit:
exitShort
function, shorters can choose to exit their positions by placing bids on the market. This involves specifying a price at which they want to place the bid and providing hints for matching against shorts.Collateral Refund:
Key Features:
Multiple Exit Methods:
Partial Exit Support:
Validation Checks:
Collateral Refund Mechanism:
Bid Placement for Market Exit:
Explanation of the contract: It appears to be a part of a decentralized application (DApp) or protocol related to financial markets or assets. The contract is specifically named "RedemptionFacet" suggesting its functionality is related to redemption processes within the protocol.
Functionality of the contract: The contract contains functions related to proposing, disputing, and claiming redemptions within the protocol. Users can propose redemptions for certain assets, which involves submitting a list of Short Records (SRs) as candidates for redemption along with other parameters. These proposals are subject to a dispute period. During the dispute period, users can challenge proposed redemptions if they believe there are inaccuracies or inconsistencies. After the dispute period, the redeemer can claim the redemption, and any leftover collateral from fully redeemed SRs can be claimed by the shorter.
Key Features:
Overall, the contract facilitates a redemption process within the protocol, allowing users to propose, dispute, and claim redemptions for assets, with mechanisms in place to address inaccuracies and ensure fairness in the redemption process.
Explanation of the library:
The LibBridgeRouter
library facilitates interactions related to the bridge mechanism within a decentralized finance (DeFi) protocol. It includes functions to handle adding and removing dETH (deposited ETH) from user accounts, assessing the amount of dETH not covered by bridge credits during withdrawal, calculating withdrawal fees, and transferring bridge credits between users.
Functionality of the library:
Adding dETH (addDeth
): This function credits a user's account with dETH and adjusts the bridge credit if applicable. It updates the total dETH held within the specified vault.
Assessing dETH (assessDeth
): This function determines how much dETH is not covered by bridge credits during withdrawal. It handles cases where users withdraw either RETH (redeemable ETH) or stETH (staked ETH) and calculates the remaining dETH that needs to be withdrawn from the vault.
Calculating withdrawal fee percentage (withdrawalFeePct
): This function calculates the withdrawal fee percentage based on the premium or discount differential between the market prices of RETH and stETH. The fee is charged to prevent free arbitrage and is applicable only to VAULT.ONE, which has mixed LST (liquidity staking tokens).
Transferring bridge credit (transferBridgeCredit
): This function transfers bridge credits from one user to another when an NFT (non-fungible token) short record (SR) is being transferred. It ensures that bridge credits are not abused and prevents workarounds to the bridge credit system.
Removing dETH (removeDeth
): This function updates the user's account upon dETH withdrawal, deducting the withdrawn amount along with any associated withdrawal fee from the user's ethEscrowed balance. It also updates the total dETH held within the vault.
Key Features:
Explanation of the library:
The LibBytes
library contains a custom decoding function readProposalData
designed to decode proposal data stored in a specific format within a byte array. This library is utilized to extract proposal data, which includes various parameters such as addresses, IDs, and numerical values, from a given byte array.
Functionality of the library:
readProposalData
Function: This function decodes proposal data stored in a byte array. It takes two parameters: the pointer to the SSTORE2
contract and the length of the slate (number of proposals). The function iterates over the byte array, extracting proposal data for each proposal and storing it in an array of MTypes.ProposalData
structs.
Inside the loop, it performs bitwise operations and assembly code to extract specific fields from the byte array and store them in the ProposalData
struct. Each ProposalData
struct contains fields such as shorter
(an address), shortId
(a single byte), CR
(an unsigned 64-bit integer), ercDebtRedeemed
(an unsigned 88-bit integer), and colRedeemed
(an unsigned 88-bit integer).
Key Features:
Explanation of the library:
The LibOracle
library provides functionalities related to fetching and managing oracle prices for assets in a decentralized finance (DeFi) system. It integrates with Chainlink oracles and calculates prices based on various conditions, including circuit breakers to handle potential issues with oracle data.
Functionality of the library:
getOraclePrice
Function: This function retrieves the price of an asset from a specified oracle. It calculates the price based on the oracle's latest round data and adjusts it according to specific conditions. The function includes circuit breaker mechanisms to handle scenarios where oracle data might be unavailable or unreliable.
baseOracleCircuitBreaker
Function: This function serves as a circuit breaker for the base oracle. It evaluates whether the fetched data from the base oracle is valid and adjusts the price calculation accordingly. If there are issues with the base oracle, it falls back to using a time-weighted average price (TWAP) or another reliable source.
oracleCircuitBreaker
Function: Similar to baseOracleCircuitBreaker
, this function acts as a circuit breaker for non-base oracles. It ensures that the fetched data from the oracle is valid and rejects it if there are any issues.
twapCircuitBreaker
Function: This function calculates the TWAP of an asset's price if there are issues with the oracle data. It verifies the TWAP price and liquidity before returning it as the final price.
setPriceAndTime
Function: This function sets the oracle price and time for an asset. It updates the oracle price and the time of the last update in the storage.
getTime
Function: Retrieves the time of the last oracle price update for a specific asset.
getPrice
Function: Retrieves the last recorded oracle price for a specific asset.
getSavedOrSpotOraclePrice
Function: This function allows callers to retrieve the saved oracle price if the last update occurred within a certain timeframe. If the last update is too old, it fetches the oracle price using the getOraclePrice
function.
Key Features:
getSavedOrSpotOraclePrice
function allows callers to save gas by retrieving the saved oracle price if it was recently updated, reducing the need for frequent oracle queries.Explanation of the Library: LibOrders is a Solidity library designed to manage various aspects of trading orders within a decentralized trading system. It encompasses a range of functionalities crucial for maintaining an efficient and reliable trading environment on the blockchain. Let's break down its key features and functionalities:
Order Management: LibOrders provides comprehensive support for managing different types of orders, including limit bids, asks, and shorts. It offers functions for adding new orders, canceling existing ones, and executing matches between compatible orders.
Price and Time Handling: The library interacts with external oracles, such as LibOracle, to fetch real-time asset prices. It also utilizes timestamps for managing order durations and enforcing time-based conditions for order execution.
Shares and Rewards Calculation: LibOrders calculates shares and rewards associated with order matches, considering various factors such as order duration, executed price, and market conditions. This ensures fair distribution of rewards among participants.
Order Matching Logic: It contains sophisticated logic for matching incoming orders with existing ones in the order book. This logic ensures that trades are executed according to predefined market rules and order preferences, maintaining market integrity.
Order Book Maintenance: LibOrders is responsible for maintaining the order book, ensuring that orders are correctly added, updated, or removed based on trading activities. This includes handling complexities such as order matching, price adjustments, and order lifecycle events.
Key Features:
Advanced Order Types: Supports advanced order types beyond basic buy and sell orders, including short selling. It incorporates specific logic tailored to handle the complexities of different order types within the trading system.
Decentralized Price Feeds: Utilizes decentralized oracles to fetch accurate and tamper-proof price information from external sources. This ensures that the trading system operates with reliable market data, crucial for fair and transparent trading.
Efficient State Management: Employs efficient state management techniques, such as the use of storage mappings and structs, to handle the substantial amount of data associated with orders, trades, and assets. This helps optimize gas usage and reduce transaction costs.
Event Emission: Emits events for significant actions and state changes within the library, providing transparency and enabling off-chain applications to react to on-chain activities effectively.
Gas Optimization: Implements strategies for gas optimization, such as reusing order IDs and optimizing storage patterns. These optimizations are essential for minimizing transaction costs and improving the overall efficiency of the trading system.
Overall, LibOrders plays a pivotal role in facilitating smooth and secure trading activities on the blockchain by providing essential functionalities for order management, price handling, and order book maintenance. Its comprehensive feature set ensures that the trading system operates efficiently and transparently, enhancing the overall trading experience for participants.
Explanation of the library:
The provided Solidity library, named LibSRUtil
, serves as an extension to the existing functionality related to managing short positions within a decentralized trading system. It introduces additional helper functions to facilitate various operations involved in managing short positions and associated records.
Functionality of the library:
Disbursement of Collateral: The disburseCollateral
function facilitates the disbursement of collateral associated with short positions. It adjusts the collateral balances of both the vault and the asset while distributing any yield earned from the collateral to the appropriate parties.
Cancellation and Verification of Short Orders: The checkCancelShortOrder
function checks whether a short order can be canceled based on specific conditions. It verifies the status of the short order and the associated short record, allowing for cancellation under certain circumstances.
Verification of Minimum ERC Debt: The checkShortMinErc
function ensures that the ERC debt associated with a short position meets the minimum threshold required for the asset. It validates the remaining ERC debt against the minimum ERC threshold and cancels the short order if necessary.
Recovery Mode Violation Check: The checkRecoveryModeViolation
function checks for violations of recovery mode conditions. It examines the collateralization ratio of a short record against the recovery collateralization ratio for the asset, determining whether a violation has occurred.
Transfer of Short Records: The transferShortRecord
function facilitates the transfer of short records between different addresses. It handles the cancellation of associated short orders, updates record statuses, and transfers collateral balances accordingly.
Update of ERC Debt: The updateErcDebt
function updates the ERC debt associated with a short record based on the current ERC debt rate for the asset.
Key Features:
Enhanced Short Position Management: The library enhances the management of short positions within the trading system by providing additional functionalities for collateral disbursement, order cancellation, verification of minimum ERC debt, and recovery mode violation checks.
Improved Security and Reliability: By introducing verification mechanisms and checks for various conditions, the library helps ensure the integrity and security of short position management operations, reducing the risk of errors or malicious activities.
Streamlined Short Record Transfer: The transferShortRecord
function streamlines the process of transferring short records between addresses, handling all necessary steps such as order cancellation and collateral transfer, thereby simplifying asset management operations for users.
Compatibility and Integration: The library is designed to seamlessly integrate with existing components and libraries within the trading system, providing an extension of functionality while maintaining compatibility with the broader ecosystem.
Efficient Resource Utilization: Through the use of optimized data structures and algorithms, the library aims to optimize gas usage and minimize transaction costs, ensuring efficient resource utilization within the decentralized trading platform.
Explanation of the library:
The provided Solidity library, named OracleLibrary
, serves as a utility for interacting with Uniswap V3 pool oracles. It enables smart contracts to fetch price quotes and estimate Time-Weighted Average Prices (TWAPs) for token pairs traded on Uniswap V3 pools.
Functionality of the library:
Quote Calculation: The getQuoteAtTick
function calculates the amount of quote token received in exchange for a given amount of base token at a specified tick value. It leverages Uniswap V3 pool tick math to compute the quote amount with high precision, considering the square root ratio of the tick.
TWAP Estimation: The estimateTWAP
function estimates the Time-Weighted Average Price (TWAP) for a token pair within a specified time window. It retrieves tick cumulatives from the Uniswap V3 pool oracle, calculates the tick change over the time window, and derives the corresponding tick value. Then, it uses the tick value to compute the price quote for the given input amount of base token.
Key Features:
Integration with Uniswap V3 Pool Oracles: The library seamlessly integrates with Uniswap V3 pool oracles, allowing smart contracts to interact with these oracles to fetch price data and TWAP estimates.
Accurate Price Calculation: By utilizing tick math and tick cumulatives from Uniswap V3 pools, the library ensures accurate and precise calculation of token exchange rates and TWAPs, enhancing the reliability of price data for decentralized applications.
Support for Various Token Pairs: The library supports arbitrary token pairs traded on Uniswap V3 pools, enabling developers to fetch price quotes and estimate TWAPs for diverse sets of tokens within the ecosystem.
Robust Error Handling: The library includes error handling mechanisms to ensure that inputs are valid and operations are performed securely. It reverts transactions with invalid inputs or unexpected conditions, promoting robustness and reliability in smart contract interactions.
Efficiency and Gas Optimization: Through optimized algorithms and precise mathematical calculations, the library aims to minimize gas consumption and optimize resource utilization, making it suitable for use in gas-sensitive environments such as Ethereum smart contracts.
Architecture Description:
The Ditto Protocol's advanced architecture is intricately woven through its contract system, which forms the backbone of its decentralized stable asset ecosystem within Ethereum. These contracts are meticulously designed to handle various aspects of asset creation, trading, collateralization, liquidation, redemption, and bridge interactions. Each contract plays a vital role in ensuring the stability, efficiency, and security of the protocol.
Key Components of the Contract System:
BidOrdersFacet:
ShortOrdersFacet:
PrimaryLiquidationFacet:
BridgeRouterFacet:
ExitShortFacet:
RedemptionFacet:
Architecture Recommendation:
Given the critical role of contracts in the Ditto Protocol, it's essential to adhere to the following architectural principles:
Optimized Gas-Efficiency Enhancements:
Enhanced Cryptographic Security Measures:
Intelligent Machine Learning Integration:
Dynamic Scalability Solutions:
Advanced User Experience Optimization:
Real-Time Market Surveillance Mechanisms:
In this table:
Contract | Code Maintainability | Code Comments | Documentation | Error Handling | Imports |
---|---|---|---|---|---|
BidOrdersFacet | High | Moderate | Low | High | Moderate |
BridgeRouterFacet | High | High | Moderate | High | Moderate |
ExitShortFacet | High | Moderate | Moderate | High | Moderate |
PrimaryLiquidationFacet | High | Moderate | Moderate | High | Moderate |
RedemptionFacet | Moderate | High | Low | High | Moderate |
ShortOrdersFacet | High | High | Low | High | High |
Contract | Code Maintainability | Code Comments | Documentation | Error Handling | Imports |
---|---|---|---|---|---|
BidOrdersFacet | The code appears well-structured and modular, with separate functions for different tasks. Comments exist to describe the purpose of functions, parameters, and important logic. However, some functions lack detailed comments, such as _shortDirectionHandler , which could benefit from more descriptive explanations. Overall, the codebase seems maintainable. | Comments aid in understanding the code, providing insights into the purpose of functions, parameters, and key logic. While most functions have sufficient comments, some areas could enhance clarity, especially regarding complex algorithms or decision-making processes. | The code lacks formal documentation such as a README file or inline documentation using tools like NatSpec. Formal documentation could provide an overview of the contract, its functions, and usage instructions, making it easier for developers to understand and integrate the contract into their projects. | Error handling mechanisms are implemented using Solidity's revert statement to revert transactions in case of errors. Error messages are thrown using custom error codes defined in the Errors library, enhancing reliability by ensuring transactions fail gracefully when encountering exceptional conditions. | The contract imports various libraries and interfaces from other contracts/modules, essential for accessing shared functionality and ensuring modularity. However, it's important to verify the integrity and security of the imported code to mitigate potential risks such as dependency vulnerabilities. |
BridgeRouterFacet | The codebase appears well-maintained and reliable, following Solidity best practices, such as using libraries for common functionality and employing modifiers for access control. The use of using directives indicates a thoughtful approach to code organization and readability. | Code comments provide explanations for various functions, parameters, and implementation details, enhancing code comprehension and maintainability. Comments follow Solidity's documentation style, improving readability and developer experience. | Documentation within the code is sufficient for understanding the purpose and functionality of different functions and modifiers. However, additional high-level documentation describing the overall architecture and design decisions would be beneficial for onboarding new developers and maintaining the codebase in the long term. | Error handling is implemented effectively throughout the codebase, with functions reverting with specific error messages when encountering invalid inputs or exceptional conditions, ensuring robustness and user experience. | The code imports external Solidity files using relative paths, maintaining clarity and ensuring modularity. This approach simplifies dependency management and facilitates code reuse. However, ensuring that imported contracts are up-to-date and secure is essential for mitigating potential vulnerabilities. |
ExitShortFacet | The codebase demonstrates a good level of maintainability and reliability, adhering to Solidity best practices by using libraries for common functionality, employing modifiers for access control, and utilizing descriptive function names. However, some functions could benefit from further modularization and abstraction to reduce complexity and enhance readability. | Comments exist to explain the purpose, behavior, and parameters of various functions, aiding code comprehension and maintenance. However, some complex or critical sections could benefit from more detailed explanations to clarify intricate logic or edge cases. | The codebase provides documentation within comments to describe the intent and usage of different functions. While these comments enhance code comprehension, additional high-level documentation outlining the overall architecture, design decisions, and interaction flow would improve developers' understanding of the system's structure and operation. | Error handling mechanisms are implemented effectively throughout the codebase, with functions reverting with specific error messages when encountering invalid inputs or exceptional conditions, ensuring robustness and user-friendly error messages. Further validation checks and defensive programming could be applied to anticipate and prevent potential edge cases or vulnerabilities. | The code imports external Solidity files using relative paths, promoting clarity and modularity. However, ensuring that imported contracts are up-to-date and secure is crucial for mitigating potential vulnerabilities and maintaining compatibility with future Solidity versions. Additionally, organizing imports alphabetically can enhance readability and maintainability. |
PrimaryLiquidationFacet | The codebase demonstrates a good level of maintainability and reliability, adhering to Solidity best practices by utilizing libraries for common functionality, employing modifiers for access control, and utilizing descriptive function names. However, some functions could benefit from further modularization and abstraction to reduce complexity and enhance readability. | Code comments explain the purpose, behavior, and parameters of various functions, enhancing code comprehension and facilitating maintenance tasks. Comments clarify the logic behind liquidation procedures, error handling, and interaction with external contracts. However, additional comments could be provided to elucidate complex algorithms or critical sections further. | The codebase provides documentation within comments to describe the intent and usage of different functions. High-level comments outline the purpose of primary liquidation and its key components, such as forced bids, fee handling, and collateral accounting. However, supplementing these comments with detailed explanations of data structures and algorithmic processes would further aid developers' understanding. | Error handling mechanisms are implemented effectively throughout the codebase, with functions reverting with specific error messages (e.g., Errors.CannotLiquidateSelf() , Errors.NoSells() ) when encountering invalid inputs, exceptional conditions, or contract violations. This ensures robustness and provides users with clear feedback on transaction failures or unauthorized operations. | The code imports external Solidity files using relative paths, promoting clarity and modularity. However, ensuring that imported contracts are up-to-date and secure is crucial for mitigating potential vulnerabilities and maintaining compatibility with future Solidity versions. |
RedemptionFacet | The codebase demonstrates a moderate level of maintainability and reliability, with a consistent coding style and structure. Usage of libraries for common functionalities enhances reliability. However, some areas may benefit from additional comments or code documentation to improve maintainability, especially for complex logic or algorithm implementations. Overall, the code appears stable and reliable, with no evident issues or bugs. | Comments are present throughout the codebase, providing explanations for functions, variables, and complex logic. They help improve code readability and understanding, aiding future maintenance and collaboration efforts. The comments follow best practices, offering insights into the purpose, behavior, and usage of various components. Enhancing comments with additional details or examples could further improve clarity. | While the code includes inline comments, comprehensive external documentation describing the architecture, design decisions, and usage guidelines is not provided. External documentation, such as README files or developer guides, can enhance understanding and facilitate onboarding for new contributors or users. Including information on dependencies, deployment procedures, and API usage would be beneficial for users and developers alike. | Error handling mechanisms are implemented effectively throughout the codebase, with various functions including error checks and revert statements to ensure proper execution and prevent unexpected behavior. Error messages are informative, helping users identify and address issues effectively. The codebase handles common error scenarios gracefully, promoting robustness and reliability. | The codebase imports necessary external dependencies and libraries efficiently, utilizing import statements to include external contracts and libraries, ensuring modularity and reusability. Grouping related imports together enhances readability and organization. However, organizing imports alphabetically or categorically could further improve clarity and maintainability. |
ShortOrdersFacet | The codebase exhibits a high level of maintainability and reliability, following best practices for code organization and readability. The use of modular design patterns and descriptive function names enhances maintainability by facilitating code navigation and comprehension. Furthermore, the codebase employs error handling mechanisms to handle unexpected scenarios gracefully, contributing to its overall reliability. Overall, the code is well-structured and easy to maintain. | Comprehensive inline comments are provided throughout the codebase, offering insights into the purpose, behavior, and usage of various components. These comments enhance code readability and understanding, aiding developers in navigating and modifying the code. Additionally, the comments adhere to best practices, providing clarity and context for complex logic and algorithm implementations. Overall, the codebase is well-documented, facilitating future maintenance and collaboration efforts. | While the code includes detailed inline comments, comprehensive external documentation describing the architecture, design decisions, and usage guidelines is not provided. External documentation, such as README files or developer guides, can enhance understanding and facilitate onboarding for new contributors or users. Including information on dependencies, deployment procedures, and API usage would be beneficial for users and developers alike. | The codebase employs robust error handling mechanisms to handle various scenarios effectively. Revert statements are used strategically to revert transactions in case of invalid inputs or unexpected conditions, preventing erroneous state changes. Error messages are informative and descriptive, helping users identify and address issues efficiently. Additionally, error checks are performed diligently, reducing the likelihood of runtime errors and enhancing overall reliability. | The codebase efficiently imports necessary external dependencies and libraries. Import statements are organized logically, enhancing readability and maintainability. Grouping related imports together promotes clarity and reduces cognitive overhead. However, organizing imports alphabetically or categorically could further improve consistency and ease of navigation. |
This table provides a detailed breakdown of the quality analysis for each contract, covering code maintainability, code comments, documentation, error handling, and imports.
</details>Security Concerns:
Input Validation:
createBid
function should validate parameters like price
and ercAmount
to ensure they are within acceptable ranges and adhere to the contract's requirements.Reentrancy:
nonReentrant
modifier or similar patterns to prevent reentrancy attacks. Pay special attention to functions like matchIncomingBid
, matchlowestSell
, and _createBid
, as they involve state modifications and external calls.Oracle Manipulation:
LibOracle.getPrice
, should implement measures to mitigate oracle manipulation attacks. Ensure that the oracle data is securely sourced and validated to prevent malicious actors from influencing market prices or exploiting inaccuracies in the oracle's data feed.Access Control:
createForcedBid
should be restricted to specific contracts or addresses to prevent unauthorized access and potential abuse of privileged functionalities.Gas Limitations:
bidMatchAlgo
. Ensure that the contract remains usable within the Ethereum gas limits to prevent out-of-gas errors and ensure smooth execution under various network conditions.Library and Dependency Security:
LibOrders
, LibOracle
, and LibAsset
, to mitigate the risk of potential vulnerabilities inherited from external code. Conduct thorough code reviews and consider auditing the dependencies for security vulnerabilities and best practices adherence.Front-Running:
bidMatchAlgo
. Consider techniques such as using commit-reveal schemes or adjusting the order of operations to minimize the impact of front-running strategies and ensure fair execution of orders.Consistency and Atomicity:
matchIncomingBid
and matchlowestSell
should handle state updates and interactions with external contracts in a manner that guarantees consistency and atomicity.By addressing these security concerns and conducting thorough testing and auditing, developers can enhance the robustness and security posture of the BidOrdersFacet
contract, mitigating potential risks and vulnerabilities in its implementation.
Security Concerns:
The provided Solidity code exhibits several security considerations:
Input Validation: The contract validates inputs such as deposit amounts to prevent underflows or malicious behavior. For instance, the deposit
and depositEth
functions verify that the deposited amount meets a minimum threshold (C.MIN_DEPOSIT
) before proceeding.
Reentrancy Protection: The nonReentrant
modifier is used to prevent reentrancy attacks, ensuring that functions cannot be called recursively and protecting against potential exploits involving repeated interactions with the contract's state.
Access Control: Certain functions, such as withdrawTapp
, are restricted to specific roles (onlyDAO
), limiting their execution to authorized entities and preventing unauthorized access to critical functionalities.
Safe Math Operations: The code employs safe arithmetic operations (mul
, div
) to prevent overflows and underflows, reducing the risk of integer vulnerabilities.
External Contract Interaction: Interactions with external contracts (IBridge
) are conducted cautiously, with appropriate error handling and checks to ensure the integrity of cross-contract calls.
Constant Gas Usage: Functions are designed to consume predictable and reasonable amounts of gas, mitigating the risk of DoS attacks by ensuring that contract execution costs remain within acceptable bounds.
Despite these measures, security is an ongoing concern in decentralized applications. Regular audits, peer reviews, and adherence to best practices can further strengthen the security posture of the contract. Additionally, staying informed about emerging vulnerabilities and promptly addressing any identified issues are crucial for maintaining a secure and robust smart contract ecosystem.
Security Concerns:
The provided Solidity code exhibits several security considerations:
Input Validation: The contract validates inputs, such as buybackAmount
, to prevent underflows, overflows, and unexpected behavior. Functions revert with specific error messages (Errors.InvalidBuyback()
, Errors.InsufficientCollateral()
, etc.) when encountering invalid or unauthorized operations.
Reentrancy Protection: The nonReentrant
modifier is utilized to prevent reentrancy attacks, ensuring that functions cannot be called recursively and protecting against potential exploits involving repeated interactions with the contract's state.
Access Control: Certain functions are restricted to authorized entities, such as valid short record owners or non-frozen assets, using modifiers (onlyValidShortRecord
, isNotFrozen
, etc.), enhancing security by limiting access to critical functionalities.
Safe Math Operations: The code employs safe arithmetic operations (mul
, div
) to prevent overflows and underflows, reducing the risk of integer vulnerabilities and ensuring the correctness of financial calculations.
External Contract Interaction: Interactions with external contracts (IDiamond
) are conducted with caution, ensuring proper error handling and validation checks to maintain the integrity of cross-contract calls and prevent potential exploits or unauthorized operations.
Collateral Ratio Checks: The contract includes checks to ensure that collateral ratios remain within acceptable bounds (getCollateralRatioNonPrice
), mitigating the risk of under-collateralization and protecting users' assets from liquidation or loss.
Atomic Operations: To prevent race conditions and ensure atomicity, certain operations are executed atomically within the same function call, reducing the risk of concurrency-related vulnerabilities.
Despite these measures, security is an ongoing concern in decentralized applications. Regular audits, thorough testing, and adherence to best practices are essential for maintaining a secure and robust smart contract ecosystem. Additionally, staying informed about emerging vulnerabilities and promptly addressing any identified issues are crucial for safeguarding users' funds and maintaining trust in the protocol.
Security Concerns:
Input Validation: The contract validates inputs, such as shortHintArray
length, to prevent potential exploits or unexpected behavior. It ensures that parameters meet specified criteria and reverts transactions with specific error messages (Errors.TooManyHints()
) if validation fails, mitigating the risk of malicious inputs.
Reentrancy Protection: The nonReentrant
modifier is utilized to prevent reentrancy attacks, ensuring that functions cannot be called recursively and protecting against potential exploits involving repeated interactions with the contract's state. This helps safeguard against reentrancy vulnerabilities and unauthorized reentrant calls.
Access Control: Certain functions are restricted to authorized entities, such as valid short record owners or non-frozen assets, using modifiers (onlyValidShortRecord
, isNotFrozen
), enhancing security by limiting access to critical functionalities. Access control mechanisms prevent unauthorized users from executing privileged operations, reducing the risk of unauthorized asset manipulation.
Safe Math Operations: The code employs safe arithmetic operations (mulU80
, mulU88
) to prevent overflows and underflows, reducing the risk of integer vulnerabilities and ensuring the correctness of financial calculations. Safe math operations mitigate the risk of arithmetic exceptions and ensure accurate computation of liquidation fees, bid amounts, and collateral adjustments.
External Contract Interaction: Interactions with external contracts (IDiamond
) are conducted with caution, ensuring proper error handling and validation checks to maintain the integrity of cross-contract calls and prevent potential exploits or unauthorized operations. Contract interactions are carefully managed to prevent unauthorized access or manipulation of external contract state, enhancing security and reliability.
Centralization Risks:
Diamond Standard Dependency: The contract's reliance on the IDiamond
interface introduces centralization risks if the referenced Diamond Standard implementation holds significant control over critical functions or resources. This dependency could lead to centralization of control, making the system vulnerable to manipulation by the owner or administrator of the Diamond Standard contract.
Admin Functions: The presence of functions like createForcedBid
accessible only to specific contracts hints at potential centralization of authority or privileges. If these admin functions grant excessive control to specific entities, it may centralize power, leading to potential misuse or exploitation.
Centralization Risks:
Bridge Centralization: The contract's interaction with bridge contracts (IBridge
) for asset deposits and withdrawals introduces centralization risks if these bridges are controlled by a single entity. Dependency on centralized bridges could lead to manipulation or disruptions in asset transfers.
Admin Privileges: Privileged functions such as withdrawTapp
, limited to specific entities like a DAO, pose centralization risks if governance becomes centralized over time. Misuse of these privileges could lead to unfair advantages or protocol manipulation.
Centralization Risks:
Market Control: Control over markets and short positions by a few entities may lead to manipulation or exploitation of vulnerabilities. Concentrated influence could disrupt market dynamics or unfairly advantage certain participants.
Admin Privileges: Functions like exitShortWallet
and exitShortErcEscrowed
, accessible under specific conditions, pose centralization risks if abused. Misuse of these privileges could result in protocol manipulation or disruptions in short position exits.
Centralization Risks:
Market Control: The contract's role in facilitating short position liquidations may pose risks if control over liquidations is centralized. This concentration of control could enable market manipulation or unfair treatment of participants.
Admin Privileges: Certain functions restricted to specific entities may centralize authority. If not decentralized, these privileges could lead to biased outcomes or disruptions in the liquidation process.
Centralization Risks:
Admin Controls: Privileged admin functions could centralize control over critical functionalities, potentially leading to biased outcomes or exploitation.
Oracle Dependency: Reliance on a single oracle or a small set of oracles introduces centralization risks. Manipulation or failure of oracles could disrupt the system's operations or compromise data integrity.
Vault Management: Centralized management of vaults or collateral assets may pose risks to system stability and security. Decentralized control mechanisms should be implemented to mitigate these risks.
Governance Structure: Lack of decentralized governance mechanisms or concentration of governance power may lead to biased decision-making or resistance to beneficial changes.
Centralization Risks:
This report outlines the centralization risks inherent in each contract, emphasizing potential vulnerabilities arising from dependencies, privileged functions, and governance structures. Addressing these risks is crucial to ensuring the resilience and fairness of the system.
Order Matching Algorithm:
The implemented bid matching algorithm (bidMatchAlgo
) requires meticulous scrutiny to ensure it effectively matches bid orders with appropriate sell orders. It is imperative to verify that the algorithm operates efficiently and accurately while safeguarding against vulnerabilities such as front-running or order manipulation.
Oracle Integration: The contract's interaction with an oracle for price information necessitates a comprehensive evaluation of the integration's reliability, accuracy, and security. Additionally, measures should be in place to gracefully handle oracle failures or manipulation to maintain the integrity of price data.
Bridge Interaction Mechanism:
Functions facilitating asset deposits and withdrawals via bridge contracts (IBridge
) demand a robust review to ascertain seamless asset transfers while mitigating risks such as front-running, reentrancy attacks, or unexpected behavior during deposit or withdrawal operations.
Yield Rate Update Mechanism:
The inclusion of a mechanism (maybeUpdateYield
) for automatically updating the vault yield rate based on bridge deposits necessitates assessment regarding its effectiveness and fairness in maintaining a balanced yield rate. It is crucial to deter attempts to manipulate the yield through large temporary positions.
Short Position Exit Mechanisms: Various mechanisms for exiting short positions, including the use of ERC tokens from wallets or held in escrow, require evaluation to ensure effectiveness, security, and fairness. It is imperative to prevent unauthorized access and minimize the risk of asset loss while facilitating partial or full exits.
Bid Placement Mechanism: The mechanism for placing bids on markets to exit short positions warrants scrutiny to ensure accurate reflection of market conditions, prevention of front-running or manipulation, and facilitation of fair and efficient short position exits.
Liquidation Mechanism: The mechanism for liquidating short positions by compelling the shorter to place bids on the market necessitates assessment regarding its effectiveness, fairness, and security. It is crucial to facilitate timely and efficient short position exits while minimizing the risk of market manipulation or abuse.
Fee Handling Mechanism: Mechanisms for handling liquidation fees and distributing them among different parties involved in the process require scrutiny to ensure transparency, fairness, and efficiency in fee distribution. Preventing abuse or disruptions in the fee allocation process is essential.
Redemption Proposal Submission: The mechanism allowing users to submit proposals for redeeming assets necessitates validation to ensure accurate proposal evaluation and fee calculations.
Proposal Evaluation: Evaluation of proposed redemption candidates based on criteria such as collateral ratio and thresholds requires scrutiny to ensure accurate evaluation and inclusion criteria.
Dispute Resolution: Handling of disputes triggered by users challenging proposed redemptions warrants assessment to ensure proper dispute resolution and penalties for incorrect proposals.
Redemption Claim: Verification of proper processing of claims for redeemed assets along with any remaining collateral is necessary to ensure system integrity.
Redemption Fee Calculation: The calculation of redemption fees based on various factors requires thorough review to ensure accurate fee calculation considering all relevant factors.
Operational Flow and Logic:
Functions such as createLimitShort
necessitate review to ensure correct handling of edge cases and prevention of vulnerabilities such as reentrancy attacks or unexpected state changes. Thorough scrutiny of the operational flow and logic is essential to identify potential inefficiencies or vulnerabilities.
This report provides a structured analysis of each contract's mechanisms, highlighting areas that require careful review to ensure the system operates as intended while mitigating potential risks.
This report provides an in-depth analysis of various risks associated with the contracts and facets within the provided codebase. The risks are categorized into four main categories: Systemic Risks, Admin Abuse Risks, Technical Risks, and Integration Risks.
Order Book Management: The management of the order book could pose systemic risks if vulnerabilities exist in order prioritization, matching algorithms, or order manipulation, potentially affecting the entire trading system.
Vault Operations: Flaws in vault operations, such as inadequate collateralization or inefficient liquidation mechanisms, could introduce systemic risks impacting the stability and integrity of the protocol.
Forced Bid Creation: The presence of admin functions like createForcedBid
may lead to admin abuse if misused to manipulate market conditions or exploit users unfairly.
Escrowed Ether Handling: Improper management of escrowed ether balances could result in admin abuse if unauthorized parties gain access to or manipulate these balances.
Reentrancy Vulnerabilities: Complex order matching and settlement logic may introduce reentrancy vulnerabilities, potentially leading to exploits or unexpected behavior during order execution.
Gas Optimization: Inefficient gas usage or storage operations could increase transaction costs or lead to contract execution failures.
External Contract Dependencies: Integration with external contracts could introduce compatibility issues or vulnerabilities that impact protocol functionality or security.
Oracle Dependency: Reliance on external oracles for price data may introduce risks related to oracle reliability, accuracy, and security.
Vault Management System: Flaws in vault management or bridge interactions could pose systemic risks impacting asset transfers and yield rate adjustments.
Bridge Dependency: Dependency on external bridges could introduce systemic risks related to bridge failures, network congestion, or vulnerabilities in bridge contracts.
Privileged Functions: Admin functions like withdrawTapp
may lead to admin abuse if misused by DAO governance to unfairly benefit specific entities or groups.
Eth Escrow Handling: Mishandling of eth escrow balances may result in admin abuse if unauthorized parties access or manipulate these balances.
Reentrancy Vulnerabilities: Complex deposit and withdrawal logic may introduce reentrancy vulnerabilities, leading to asset loss or protocol exploits.
Gas Optimization: Inefficient gas usage could increase transaction costs or lead to contract execution failures.
Market Dynamics: Inadequacies in market mechanisms or short position management could introduce systemic risks impacting market stability and protocol integrity.
Protocol Integrity: Flaws in protocol design or governance may lead to systemic failures affecting all participants and assets within the system.
Privileged Functions: Admin functions restricted to specific entities may result in admin abuse if misused to manipulate markets or exert excessive control over protocol operations.
Asset Management: Mismanagement of assets or short positions by entities with administrative privileges may lead to asset loss or unfair treatment of users.
Smart Contract Vulnerabilities: Complex logic for exiting short positions may introduce vulnerabilities such as reentrancy attacks or unexpected state changes.
External Contract Dependencies: Reliance on external contracts for market interactions may introduce vulnerabilities impacting protocol functionality or security.
External Contract Interactions: Interactions with external contracts may introduce compatibility issues or vulnerabilities impacting protocol operations or security.
Protocol Upgrades: Upgrades or modifications to external protocols may introduce compatibility issues or disruptions in existing functionalities.
Market Stability: Flaws in the liquidation process or collateral management could introduce systemic risks impacting market stability and protocol integrity.
Protocol Integrity: Design flaws or governance issues may lead to systemic failures affecting all users and assets relying on the protocol.
Privileged Functions: Admin functions restricted to specific entities may result in admin abuse if misused to manipulate markets or exert control over the liquidation process.
Asset Management: Mismanagement of assets or collateral by entities with administrative privileges may lead to asset loss or unfair treatment of users.
Smart Contract Vulnerabilities: Vulnerabilities in the liquidation process may introduce exploits or disruptions impacting asset values or protocol integrity.
External Contract Dependencies: Integration with external contracts may introduce vulnerabilities impacting protocol functionality or security.
External Contract Interactions: Interactions with external contracts may introduce compatibility issues or vulnerabilities impacting protocol operations or security.
Protocol Upgrades: Upgrades to external protocols may introduce compatibility issues or disruptions in existing functionalities.
leading to potential misuse or exploitation.
This comprehensive risk analysis provides valuable insights into the potential vulnerabilities and threats associated with each contract and facet in the provided codebase. Mitigating these risks requires a holistic approach encompassing rigorous testing, adherence to best practices, and continuous monitoring and improvement of the system's security, reliability, and performance.
25 hours
#0 - c4-pre-sort
2024-04-07T20:27:37Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-04-17T07:16:36Z
hansfriese marked the issue as grade-b
#2 - albahaca0000
2024-04-17T13:29:42Z
Hi @hansfriese thanks for judging
I wish to express my gratitude for your consideration of my analysis. I firmly believe that it adheres closely to the code4rena judging criteria, offering a comprehensive examination of the risks associated with each contract. These encompass systemic risks, technical risks, integration risks, and concerns regarding centralization or admin abuse.
Highlighted areas of focus: - Comprehensive depiction of the project’s risk framework: - Risks related to admin abuse - Systemic risks - Technical risks - Integration risks
Within this assessment, I've strived to provide a robust security analysis of each contract, addressing systemic risk, integration risk, technical risk, and centralization risk or admin abuse risk. This evaluation is grounded in thorough data analysis.
I kindly request that you review other sections of the report, such as the Codebase Quality Analysis and Security Concerns, as well as the 'How the Contract Works' section. These sections provide further insights into the meticulous examination conducted and contribute to the holistic understanding of the project's intricacies, Please take another look at my report to see the considerable effort put into its completion.
Thank you for your judging.
#3 - hansfriese
2024-04-21T07:57:13Z
After checking again, I agree it deserves a grade-a.
#4 - c4-judge
2024-04-21T07:57:22Z
hansfriese marked the issue as grade-a