Platform: Code4rena
Start Date: 25/08/2022
Pot Size: $75,000 USDC
Total HM: 35
Participants: 147
Period: 7 days
Judge: 0xean
Total Solo HM: 15
Id: 156
League: ETH
Rank: 24/147
Findings: 4
Award: $955.75
π Selected for report: 0
π Solo Findings: 0
https://github.com/code-423n4/2022-08-olympus/blob/main/src/libraries/TransferHelper.sol#L10-L33
TransferHelper.sol
and solmate
won't check if the token is a contract or not.
A hacker could set traps for non existing tokens to steal future funds from users.
The safeTransfer()
functions used in the contract are wrappers around the solmate
library. Solmate will not check for contract existance.
File: src/libraries/TransferHelper.sol function safeTransferFrom( ERC20 token, address from, address to, uint256 amount ) internal { (bool success, bytes memory data) = address(token).call( abi.encodeWithSelector(ERC20.transferFrom.selector, from, to, amount) ); require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FROM_FAILED"); } function safeTransfer( ERC20 token, address to, uint256 amount ) internal { (bool success, bytes memory data) = address(token).call( abi.encodeWithSelector(ERC20.transfer.selector, to, amount) ); require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED"); }
Manual review.
Looking at OpenZeppelin's implementation, a check is made on the code for the token address.
address(token).code.length > 0
Consider using OpenZeppelin or checking for contract existance manually inside TransferHelper.sol
.
#0 - 0xean
2022-09-19T23:47:52Z
dupe of #117
11.0311 DAI - $11.03
https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/PRICE.sol#L161 https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/PRICE.sol#L170
Not checking additional fields returned by Chainlink might cause incorrect prices being processed.
The only values being check from latestRoundData
are price
and updatedAt
.
File: src/modules/PRICE.sol 161: (, int256 ohmEthPriceInt, , uint256 updatedAt, ) = _ohmEthPriceFeed.latestRoundData(); 170: (, reserveEthPriceInt, , updatedAt, ) = _reserveEthPriceFeed.latestRoundData();
Add additional checks for roundId
and answeredInRound
. Also, add a check to ensure updatedAt
is not zero.
$ git diff --no-index PRICE.sol.orig PRICE.sol diff --git a/PRICE.sol.orig b/PRICE.sol index 55d85d3..b0792ea 100644 --- a/PRICE.sol.orig +++ b/PRICE.sol @@ -158,18 +158,30 @@ contract OlympusPrice is Module { uint256 ohmEthPrice; uint256 reserveEthPrice; { - (, int256 ohmEthPriceInt, , uint256 updatedAt, ) = _ohmEthPriceFeed.latestRoundData(); + (uint256 roundId, int256 ohmEthPriceInt, , uint256 updatedAt, uint256 answeredInRound) = _ohmEthPriceFeed.latestRoundData(); // Use a multiple of observation frequency to determine what is too old to use. // Price feeds will not provide an updated answer if the data doesn't change much. // This would be similar to if the feed just stopped updating; therefore, we need a cutoff. if (updatedAt < block.timestamp - 3 * uint256(observationFrequency)) revert Price_BadFeed(address(_ohmEthPriceFeed)); + // Example of a new custom error to handle stale prices. + if (answeredInRound < roundId) + revert Price_Stale(); + // Example of a new custom error to handle incomplete rounds. + if (updatedAt == 0) + revert Price_IncompleteRound(); ohmEthPrice = uint256(ohmEthPriceInt); int256 reserveEthPriceInt; - (, reserveEthPriceInt, , updatedAt, ) = _reserveEthPriceFeed.latestRoundData(); + (uint256 roundId, reserveEthPriceInt, , updatedAt, uint256 answeredInRound) = _reserveEthPriceFeed.latestRoundData(); if (updatedAt < block.timestamp - uint256(observationFrequency)) revert Price_BadFeed(address(_reserveEthPriceFeed)); + // Example of a new custom error to handle stale prices. + if (answeredInRound < roundId) + revert Price_Stale(); + // Example of a new custom error to handle incomplete rounds. + if (updatedAt == 0) + revert Price_IncompleteRound(); reserveEthPrice = uint256(reserveEthPriceInt); }
#0 - Oighty
2022-09-06T19:23:01Z
Duplicate. See comment on #441.
π Selected for report: zzzitron
Also found by: 0x040, 0x1f8b, 0x52, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSky, 0xSmartContract, 0xkatana, 8olidity, Aymen0909, Bahurum, BipinSah, Bnke0x0, CRYP70, CertoraInc, Ch_301, Chandr, Chom, CodingNameKiki, Deivitto, DimSon, Diraco, ElKu, EthLedger, Funen, GalloDaSballo, Guardian, IllIllI, JansenC, Jeiwan, Lambda, LeoS, Margaret, MasterCookie, PPrieditis, PaludoX0, Picodes, PwnPatrol, RaymondFam, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, StevenL, The_GUILD, TomJ, Tomo, Trust, Waze, __141345__, ajtra, ak1, apostle0x01, aviggiano, bin2chen, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch13fd357r0y3r, cloudjunky, cryptphi, csanuragjain, d3e4, datapunk, delfin454000, devtooligan, dipp, djxploit, durianSausage, eierina, enckrish, erictee, fatherOfBlocks, gogo, grGred, hansfriese, hyh, ignacio, indijanc, itsmeSTYJ, ladboy233, lukris02, martin, medikko, mics, natzuu, ne0n, nxrblsrpr, okkothejawa, oyc_109, p_crypt0, pfapostol, prasantgupta52, rajatbeladiya, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, shenwilly, sikorico, sorrynotsorry, tnevler, tonisives, w0Lfrum, yixxas
54.3147 DAI - $54.31
The batchToTresury
function has access control, but it's updating the state after external calls.
Consider adding a nonReetrancy
modifier.
File: src/policies/BondCallback.sol function batchToTreasury(ERC20[] memory tokens_) external onlyRole("callback_admin") { ERC20 token; uint256 balance; uint256 len = tokens_.length; for (uint256 i; i < len; ) { token = tokens_[i]; balance = token.balanceOf(address(this)); token.safeTransfer(address(TRSRY), balance); priorBalances[token] = token.balanceOf(address(this)); unchecked { ++i; } } }
Consider adding checks against zero address when a function is receiving an input address.
File: main/src/modules/TRSRY.sol 64: function setApprovalFor( 122: function setDebt(
Locking the pragma helps to ensure that contracts do not accidentally get deployed using an outdated compiler version. Note that pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case with contracts in a library or a package.
There are 3 instances of this issue.
File: src/interfaces/IBondCallback.sol 2: pragma solidity >=0.8.0;
File: src/policies/interfaces/IHeart.sol 2: pragma solidity >=0.8.0;
File: src/policies/interfaces/IOperator.sol 2: pragma solidity >=0.8.0;
There are 2 instances of this issue
File: src/policies/Governance.sol 151: function getActiveProposal() public view returns (ActivatedProposal memory) { 145: function getMetadata(uint256 proposalId_) public view returns (ProposalMetadata memory) {
Consider adding NATSPEC on all functions to enhance the project documentation.
File: src/policies/Governance.sol 61: function configureDependencies() external override returns (Keycode[] memory dependencies) { 70: function requestPermissions()
Consider emitting an event when setActiveStatus
is called to facilitate monitoring of the system.
File: main/src/modules/TRSRY.sol 126: function setActiveStatus(bool activate_) external onlyKernel {
There are 4 instances of this issue.
File: src/policies/Operator.sol 657: /// TODO determine if this should use the last price from the MA or recalculate the current price, ideally last price is ok since it should have been just updated and should include check against secondary?
File: src/policies/TreasuryCustodian.sol 51: // TODO Currently allows anyone to revoke any approval EXCEPT activated policies. 52: // TODO must reorg policy storage to be able to check for deactivated policies. 56: // TODO Make sure `policy_` is an actual policy and not a random address.
#0 - 0xLienid
2022-09-09T02:00:52Z
Adding zero address checks adds a bunch of overhead and doesn't make much sense in a heavily permissioned system. safeTransfer should be fine since it is only operating on tokens we've specified. The rest looks good and will aim to implement.
π Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xkatana, Amithuddar, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Deivitto, Dionysus, Diraco, ElKu, Fitraldys, Funen, GalloDaSballo, Guardian, IllIllI, JC, JansenC, Jeiwan, LeoS, Metatron, Noah3o6, RaymondFam, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Saintcode_, Shishigami, Sm4rty, SooYa, StevenL, Tagir2003, The_GUILD, TomJ, Tomo, Waze, __141345__, ajtra, apostle0x01, aviggiano, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch0bu, chrisdior4, d3e4, delfin454000, djxploit, durianSausage, erictee, exolorkistis, fatherOfBlocks, gogo, grGred, hyh, ignacio, jag, karanctf, kris, ladboy233, lukris02, m_Rassska, martin, medikko, natzuu, ne0n, newfork01, oyc_109, peiw, rbserver, ret2basic, robee, rokinot, rvierdiiev, sikorico, simon135, tnevler, zishansami
32.9707 DAI - $32.97
There are 7 instances of this issue.
File: src/policies/Operator.sol 488: decimals++; 670: _status.low.count++; 675: _status.low.count--; 686: _status.high.count++; 691: _status.high.count--;
File: src/utils/KernelUtils.sol 49: i++; 64: i++;
There is 1 instance of this issue.
File: src/policies/Governance.sol 278: for (uint256 step; step < instructions.length; ) {
There are 3 instances of this issue.
File: src/Kernel.sol 397: for (uint256 i = 0; i < reqLength; ) {
File: src/utils/KernelUtils.sol 43: for (uint256 i = 0; i < 5; ) { 58: for (uint256 i = 0; i < 32; ) {
Replace > 0
with != 0
for unsigned integers.
On the instance bellow userVotesForProposal
is a nested mapping of uints.
File: src/policies/Governance.sol 247: if (userVotesForProposal[activeProposal.proposalId][msg.sender] > 0) {
There are 5 instances of this issue.
File: src/policies/Operator.sol 372: int8 scaleAdjustment = int8(ohmDecimals) - int8(reserveDecimals) + (priceDecimals / 2); 419: uint256 invCushionPrice = 10**(oracleDecimals * 2) / range.cushion.low.price; 420: uint256 invWallPrice = 10**(oracleDecimals * 2) / range.wall.low.price; 427: int8 scaleAdjustment = int8(reserveDecimals) - int8(ohmDecimals) + (priceDecimals / 2); 786: ) * (FACTOR_SCALE + RANGE.spread(true) * 2)) /
There are 2 instances of this issue.
File: src/policies/Governance.sol 223: if (proposalHasBeenActivated[proposalId_] == true) { 306: if (tokenClaimsForProposal[proposalId_][msg.sender] == true) {
There is 1 instance of this issue in Price.sol
and 2 instances in TRSRY.sol
File: src/modules/PRICE.sol 136: _movingAverage += (currentPrice - earliestPrice) / numObs;
File: main/src/modules/TRSRY.sol 96: reserveDebt[token_][msg.sender] += amount_; 97: totalDebt[token_] += amount_;
The values can still be inspected on the source code if necessary.
There are 9 instances of this issue.
File: src/modules/PRICE.sol 59: uint8 public constant decimals = 18;
File: src/modules/RANGE.sol 65: uint256 public constant FACTOR_SCALE = 1e4;
File: src/policies/Governance.sol 121: uint256 public constant SUBMISSION_REQUIREMENT = 100; 124: uint256 public constant ACTIVATION_DEADLINE = 2 weeks; 127: uint256 public constant GRACE_PERIOD = 1 weeks; 130: uint256 public constant ENDORSEMENT_THRESHOLD = 20; 133: uint256 public constant EXECUTION_THRESHOLD = 33; 137: uint256 public constant EXECUTION_TIMELOCK = 3 days;
File: src/policies/Operator.sol 89: uint32 public constant FACTOR_SCALE = 1e4;