Platform: Code4rena
Start Date: 01/09/2023
Pot Size: $36,500 USDC
Total HM: 4
Participants: 70
Period: 6 days
Judge: kirk-baird
Id: 281
League: ETH
Rank: 64/70
Findings: 1
Award: $7.08
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: adriro
Also found by: 0x6980, 0xStalin, 0xanmol, 0xmystery, 0xpanicError, Arz, Aymen0909, BenRai, Breeje, Lalanda, MohammedRizwan, Raihan, SovaSlava, Stormreckson, Udsen, ast3ros, bin2chen, castle_chain, catellatech, codegpt, dev0cloo, gkrastenov, hals, klau5, kutugu, ladboy233, matrix_0wl, nirlin, ohm, peanuts, pipidu83, sandy, wahedtalash77
7.08 USDC - $7.08
preRebaseTokenAmount
and postRebaseTokenAmount
calculations will always return the same value in the _burnShares
function as they take the same input parameter _sharesAmount
and they share an identical timestamp (used in oracle.getPrice()
), while the range was not changed.
function _burnShares( address _account, uint256 _sharesAmount ) internal whenNotPaused returns (uint256) { require(_account != address(0), "BURN_FROM_THE_ZERO_ADDRESS"); _beforeTokenTransfer(_account, address(0), _sharesAmount); uint256 accountShares = shares[_account]; require(_sharesAmount <= accountShares, "BURN_AMOUNT_EXCEEDS_BALANCE"); uint256 preRebaseTokenAmount = getRUSDYByShares(_sharesAmount); totalShares -= _sharesAmount; shares[_account] = accountShares - _sharesAmount; uint256 postRebaseTokenAmount = getRUSDYByShares(_sharesAmount); emit SharesBurnt( _account, preRebaseTokenAmount, postRebaseTokenAmount, _sharesAmount ); return totalShares; // Notice: we're not emitting a Transfer event to the zero address here since shares burn // works by redistributing the amount of tokens corresponding to the burned shares between // all other token holders. The total supply of the token doesn't change as the result. // This is equivalent to performing a send from `address` to each other token holder address, // but we cannot reflect this as it would require sending an unbounded number of events. // We're emitting `SharesBurnt` event to provide an explicit rebase log record nonetheless. }
Given getRUSDYByShares definition below:
function getRUSDYByShares(uint256 _shares) public view returns (uint256) { return (_shares * oracle.getPrice()) / (1e18 * BPS_DENOMINATOR); }
And the getPrice() function in RWADynamicOracle.sol
function getPrice() public view whenNotPaused returns (uint256 price) { uint256 length = ranges.length; for (uint256 i = 0; i < length; ++i) { Range storage range = ranges[(length - 1) - i]; if (range.start <= block.timestamp) { if (range.end <= block.timestamp) { return derivePrice(range, range.end - 1); } else { return derivePrice(range, block.timestamp); } } } }
This is good practice for several reasons:
There is 32 occurrences of unnamed imports in this scope:
import "contracts/interfaces/IAxelarGateway.sol"; import "contracts/interfaces/IAxelarGasService.sol"; import "contracts/external/axelar/AxelarExecutable.sol"; import "contracts/interfaces/IRWALike.sol"; import "contracts/interfaces/IAllowlist.sol"; import "contracts/external/openzeppelin/contracts/access/Ownable.sol"; import "contracts/external/openzeppelin/contracts/security/Pausable.sol"; import "contracts/bridge/MintRateLimiter.sol";
import "contracts/interfaces/IAxelarGateway.sol"; import "contracts/interfaces/IAxelarGasService.sol"; import "contracts/interfaces/IMulticall.sol"; import "contracts/interfaces/IRWALike.sol";
and
import "contracts/external/openzeppelin/contracts/access/Ownable.sol"; import "contracts/external/openzeppelin/contracts/security/Pausable.sol";
import "contracts/rwaOracles/IRWAOracle.sol"; import "contracts/external/openzeppelin/contracts/access/AccessControlEnumerable.sol"; import "contracts/external/openzeppelin/contracts/security/Pausable.sol";
import "contracts/external/openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/token/ERC20/IERC20MetadataUpgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import "contracts/usdy/blocklist/BlocklistClientUpgradeable.sol"; import "contracts/usdy/allowlist/AllowlistClientUpgradeable.sol"; import "contracts/sanctions/SanctionsListClientUpgradeable.sol"; import "contracts/interfaces/IUSDY.sol"; import "contracts/rwaOracles/IRWADynamicOracle.sol";
import "contracts/external/openzeppelin/contracts/proxy/ProxyAdmin.sol"; import "contracts/Proxy.sol"; import "contracts/usdy/rUSDY.sol"; import "contracts/interfaces/IMulticall.sol";
Explicitly name all imports actually used in the contract.
This enables for more control on which aspect of the data developers want to expose.
There are 31 occurrences of this in scope.
Switch public variables to private visibility and define getter functions for the ones that are meant to be exposed. e.g. in https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L97
bytes32 public constant USDY_MANAGER_ROLE = keccak256("ADMIN_ROLE");
Should become
bytes32 private constant USDY_MANAGER_ROLE = keccak256("ADMIN_ROLE"); function getManagerRole() external view returns(bytes32){ return USDY_MANAGER_ROLE; }
In rUSDY.sol, the UnwrapTooSmall()
error is defined in between two public variable declarations.
// Used to scale up usdy amount -> shares uint256 public constant BPS_DENOMINATOR = 10_000; // Error when redeeming shares < `BPS_DENOMINATOR` error UnwrapTooSmall(); /// @dev Role based access control roles bytes32 public constant USDY_MANAGER_ROLE = keccak256("ADMIN_ROLE");
Group all error declarations in the same part of the contract to increase readability.
This is good practice to avoid confusion between constant variables and immutable variables, and to thus improve readability of the contract.
There are 6 occurrences of this in scope.
/// @notice Token contract bridged by this contract IRWALike public immutable TOKEN; /// @notice Pointer to AxelarGateway contract IAxelarGateway public immutable AXELAR_GATEWAY; /// @notice Pointer to USDY allowlist IAllowlist public immutable ALLOWLIST;
/// @notice Token contract bridged by this contract IRWALike public immutable TOKEN; /// @notice Pointer to AxelarGateway contract IAxelarGateway public immutable AXELAR_GATEWAY; /// @notice Pointer to AxelarGasService contract IAxelarGasService public immutable GAS_RECEIVER;
Prefix all immutable variables with “i_” E.g. in https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L40
IAllowlist public immutable ALLOWLIST;
Should be
IAllowlist public immutable i_allowList;
Note that these immutable variables should also be defined as private with associated getter functions as mentioned in 3.
function _mintIfThresholdMet(bytes32 txnHash) internal {
(DestinationBridge.sol#L337) is defined under a section labeled as
/*////////////////////////////////////////////////////////////// Public Functions //////////////////////////////////////////////////////////////*/
If categorization of function visibility is made explicit in comment, visibility of functions should match the section they are in (i.e. for example _mintIfThresholdMet
should be declared under the “internal functions” section)
There are 2 occurrences of unused errors in the scope.
error InvalidPrice();
error PriceNotSet();
Are defined but not used
Use errors where relevant or delete unused error declarations.
There are 4 occurrences in rUSDY.sol: name()
, symbol()
, decimals()
, totalSupply()
are all unused within the contract.
Mark all public functions not used in the contract as external to improve clarity of the codebase.
This makes it less straightforward for external reviewers to eyeball where these objects are defined.
E.g. errors and events are defined at the end of the contract in DestinationBridge.sol and RWADynamicOracle.sol (before helper functions for the latter) but at the start / in the middle for rUSDY.sol
Unless there is a clear readability improvement from not doing so, define objects in the same order across files.
We found a few typos in comments, namely 2 occurrences: In comments: 2 occurrences: rUSDY.sol#L631 “facliitate” rUSDY.sol#L658-659
@notice Sets the Oracle address * @dev The new oracle must comply with the `IPricerReader` interface
“Oracle” or “oracle” inconsistency
#0 - c4-pre-sort
2023-09-08T08:02:53Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-09-24T05:41:42Z
kirk-baird marked the issue as grade-b