Platform: Code4rena
Start Date: 29/03/2024
Pot Size: $36,500 USDC
Total HM: 5
Participants: 72
Period: 5 days
Judge: 3docSec
Total Solo HM: 1
Id: 357
League: ETH
Rank: 18/72
Findings: 2
Award: $72.43
🌟 Selected for report: 0
🚀 Solo Findings: 0
64.1515 USDC - $64.15
Description:
The nat spec of burn
functions says that admin can burn rOUSG tokens from any account. But it is not true, cause the admin can't burn rOUSG token of user which is remove from KYC list or sanctioned.
Proof of Concept:
burn
function: It is calling _burnShares
function to burn the shares of particular address.function burn( address _account, uint256 _amount ) external onlyRole(BURNER_ROLE) { uint256 ousgSharesAmount = getSharesByROUSG(_amount); if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER) revert UnwrapTooSmall(); @> _burnShares(_account, ousgSharesAmount); ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER ); emit Transfer(address(0), msg.sender, getROUSGByShares(ousgSharesAmount)); emit TransferShares(_account, address(0), ousgSharesAmount); }
_burnShares
function: Than it is calling _beforeTokenTransfer
function to get the KYC and sanctioned status of _account.function _burnShares( address _account, uint256 _sharesAmount ) internal whenNotPaused returns (uint256) { require(_account != address(0), "BURN_FROM_THE_ZERO_ADDRESS"); @> _beforeTokenTransfer(_account, address(0), _sharesAmount); //checking KYC for account uint256 accountShares = shares[_account]; require(_sharesAmount <= accountShares, "BURN_AMOUNT_EXCEEDS_BALANCE"); totalShares -= _sharesAmount; //decreasing totalShares with shares amount shares[_account] = accountShares - _sharesAmount; return totalShares; //return total shares }
_beforeTokenTransfer
function: And if now the _account is sanctioned or not in KYC list than the admin can't burn the rOUSG token of _account.function _beforeTokenTransfer( address from, address to, uint256 ) internal view { if (from != msg.sender && to != msg.sender) { require(_getKYCStatus(msg.sender), "rOUSG: 'sender' address not KYC'd"); } @> if (from != address(0)) { // If not minting require(_getKYCStatus(from), "rOUSG: 'from' address not KYC'd"); } if (to != address(0)) { // If not burning require(_getKYCStatus(to), "rOUSG: 'to' address not KYC'd"); } }
Impact:
Admin can't burn rOUSG tokens from any account as nat spec of burn
function states.
Recommended Mitigation:
The recommended mitigation is to burn the rOUSG token directly in burn
function rather in _burnShares
function.
Context
#0 - c4-pre-sort
2024-04-04T05:11:35Z
0xRobocop marked the issue as duplicate of #237
#1 - c4-judge
2024-04-09T08:32:31Z
3docSec marked the issue as satisfactory
🌟 Selected for report: immeas
Also found by: 0xAkira, 0xCiphky, 0xGreyWolf, 0xJaeger, 0xMosh, 0xabhay, 0xlemon, 0xmystery, 0xweb3boy, Aamir, Abdessamed, Aymen0909, Breeje, DanielArmstrong, DarkTower, Dots, EaglesSecurity, FastChecker, HChang26, Honour, IceBear, JC, K42, Krace, MaslarovK, Omik, OxTenma, SAQ, Shubham, Stormreckson, Tigerfrake, Tychai0s, VAD37, ZanyBonzy, albahaca, arnie, ast3ros, asui, b0g0, bareli, baz1ka, btk, caglankaan, carrotsmuggler, cheatc0d3, dd0x7e8, grearlake, igbinosuneric, jaydhales, kaden, kartik_giri_47538, m4ttm, ni8mare, niser93, nonn_ac, oualidpro, pfapostol, pkqs90, popeye, radev_sw, samuraii77, slvDev, zabihullahazadzoi
8.2807 USDC - $8.28
_approve
Function for spender.Description:
The _approve
function is used to setting the allowance of a spender over the owner's tokens. Call by the public functions like approve
and increaseAllowance
functions to set the allowance of spender. But there is no check in _approve
function if the spender address is in KYC whitelist and not sanctioned too cause if the spender address is not KYC'd and in sanction list than the spender can not make any transaction even though the allowances[_owner][_spender]
mapping for the spender show's the allowance to be non zero.
Impact: The non KYC'd or sanctioned address will have the allowance for rOUSG token.
Proof of Concept:
Using the public approve
and increaseAllowance
function the rOUSG token holder can give the allowance of rOUSG to the non KYC'd or sanctioned address.
Even though if spender try to execute the transferFrom
function it will failed cause of the check in transfer
function.
So it is better to not give the allowance of rOUSG token to non KYC'd or sanctioned address at first place.
For that _approve
function should check the _getKYCStatus
for the spender.
approve() function:
function approve(address _spender, uint256 _amount) public returns (bool) { _approve(msg.sender, _spender, _amount); //approving spender on behalf of owner return true; }
function increaseAllowance( address _spender, //spender address uint256 _addedValue //value to increase allowance ) public returns (bool) { _approve( msg.sender, _spender, allowances[msg.sender][_spender] + _addedValue ); return true; }
function _approve( address _owner, address _spender, uint256 _amount ) internal whenNotPaused { require(_owner != address(0), "APPROVE_FROM_ZERO_ADDRESS"); require(_spender != address(0), "APPROVE_TO_ZERO_ADDRESS"); allowances[_owner][_spender] = _amount; emit Approval(_owner, _spender, _amount); }
Recommended Mitigation:
The recommended mitigation is to add _getKYCStatus
check for the spender.
Coded mitigation:
function _approve( address _owner, address _spender, uint256 _amount ) internal whenNotPaused { require(_owner != address(0), "APPROVE_FROM_ZERO_ADDRESS"); require(_spender != address(0), "APPROVE_TO_ZERO_ADDRESS"); + require(_getKYCStatus(_spender), "rOUSG: '_spender' address not KYC'd"); allowances[_owner][_spender] = _amount; emit Approval(_owner, _spender, _amount); }
Description:
The ousgInstantManager
constructor performs a check for decimal compatibility between OUSG and rOUSG tokens but incorrectly refers to USDC instead of rOUSG in the checks.
Impact:
Can lead to confusion for the developer who will deploy and initialize the ousgInstantManager
contract.
Proof of Concept:
constructor( address defaultAdmin, address _usdc, address _usdcReciever, address _feeReceiver, address _ousgOracle, address _ousg, address _rousg, address _buidl, address _buidlRedeemer, RateLimiterConfig memory rateLimiterConfig ) InstantMintTimeBasedRateLimiter( rateLimiterConfig.mintLimitDuration, rateLimiterConfig.redeemLimitDuration, rateLimiterConfig.mintLimit, rateLimiterConfig.redeemLimit ) { require( address(_usdc) != address(0), "OUSGInstantManager: USDC cannot be 0x0" ); require( address(_usdcReciever) != address(0), "OUSGInstantManager: USDC Receiver cannot be 0x0" ); require( address(_feeReceiver) != address(0), "OUSGInstantManager: feeReceiver cannot be 0x0" ); require( address(_ousgOracle) != address(0), "OUSGInstantManager: OUSG Oracle cannot be 0x0" ); require(_ousg != address(0), "OUSGInstantManager: OUSG cannot be 0x0"); require(_rousg != address(0), "OUSGInstantManager: rOUSG cannot be 0x0"); require(_buidl != address(0), "OUSGInstantManager: BUIDL cannot be 0x0"); require( address(_buidlRedeemer) != address(0), "OUSGInstantManager: BUIDL Redeemer cannot be 0x0" ); require( IERC20Metadata(_ousg).decimals() == IERC20Metadata(_rousg).decimals(), @> "OUSGInstantManager: OUSG decimals must be equal to USDC decimals" //@audit-info ); require( IERC20Metadata(_usdc).decimals() == IERC20Metadata(_buidl).decimals(), "OUSGInstantManager: USDC decimals must be equal to BUIDL decimals" ); usdc = IERC20(_usdc); usdcReceiver = _usdcReciever; feeReceiver = _feeReceiver; oracle = IRWAOracle(_ousgOracle); ousg = IRWALike(_ousg); rousg = ROUSG(_rousg); buidl = IERC20(_buidl); buidlRedeemer = IBUIDLRedeemer(_buidlRedeemer); decimalsMultiplier = 10 ** (IERC20Metadata(_ousg).decimals() - IERC20Metadata(_usdc).decimals()); //10*1e12 require( OUSG_TO_ROUSG_SHARES_MULTIPLIER == rousg.OUSG_TO_ROUSG_SHARES_MULTIPLIER(), "OUSGInstantManager: OUSG to rOUSG shares multiplier must be equal to rOUSG's" ); _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin); _grantRole(CONFIGURER_ROLE, defaultAdmin); _grantRole(PAUSER_ROLE, defaultAdmin); }
Recommended Mitigation: The recommended mitigation is to rewrite the error comment and change the USDC with rOUSG. Add this: "OUSGInstantManager: OUSG decimals must be equal to rOUSG decimals"
ousgInstantManager::_redeemBUIDL
function.Description:
The ousgInstantManager::_redeemBUIDL
function is having a strict equivalence check when verifying the balance of USDC tokens in the contract after redeeming BUIDL tokens. The equality check (==) ensures that the USDC balance is exactly equal to the previous balance plus the redeemed BUIDL amount. However, a less strict check, such as greater than or equal to (>=), would be more appropriate if there any potential rounding or minor difference between the expected and actual balance occur.
Impact: The minor difference in token balances could lead to transaction revert, potentially stopping the intended functionality of the contract, If the USDC balance of contract is more than expected.
Proof of Concept:
function _redeemBUIDL(uint256 buidlAmountToRedeem) internal { require( buidl.balanceOf(address(this)) >= minBUIDLRedeemAmount, "OUSGInstantManager::_redeemBUIDL: Insufficient BUIDL balance" ); uint256 usdcBalanceBefore = usdc.balanceOf(address(this)); buidl.approve(address(buidlRedeemer), buidlAmountToRedeem); buidlRedeemer.redeem(buidlAmountToRedeem); require( @> usdc.balanceOf(address(this)) == usdcBalanceBefore + buidlAmountToRedeem, //@audit-info strick == check should be >= "OUSGInstantManager::_redeemBUIDL: BUIDL:USDC not 1:1" ); }
Recommended Mitigation: The recommended mitigation is to use greater than or equal (>=) instead of (==) equality check which will also work as intended and it will even execute the tx even if the USDC balance of contract is more than expected. Coded recommendation:
function _redeemBUIDL(uint256 buidlAmountToRedeem) internal { require( buidl.balanceOf(address(this)) >= minBUIDLRedeemAmount, "OUSGInstantManager::_redeemBUIDL: Insufficient BUIDL balance" ); uint256 usdcBalanceBefore = usdc.balanceOf(address(this)); buidl.approve(address(buidlRedeemer), buidlAmountToRedeem); buidlRedeemer.redeem(buidlAmountToRedeem); require( - usdc.balanceOf(address(this)) == usdcBalanceBefore + buidlAmountToRedeem, + usdc.balanceOf(address(this)) >= usdcBalanceBefore + buidlAmountToRedeem, "OUSGInstantManager::_redeemBUIDL: BUIDL:USDC not 1:1" ); }
multiexcall
function can revert if contract can't accept ETH.Description:
As the readme of contest sates "The permissioned multicall function on OUSGInstantManager will only be used to transfer non-ERC20 assets out of the contract if the assets are accidentally deposited." If any one of the arbitrary contract does not accepts the ether than it will revert the whole multiexcall
function transaction.
Proof of Concept:
multiexcall
function is used to transfer non-ERC20 assets out of the contract is the asset are accidentally deposited.multiexcall
function is transferring eth to the multiple addresses.Impact: The transferring of eth to multiple addresses will revert.
Recommended Mitigation: The recommended mitigation is that the admin needs to remove th address from the array which is causing revert.
rOUSG
contractDescription:
The rOUSG
contract is using OpenZeppelin's upgradeable contracts, such as ContextUpgradeable, PausableUpgradeable, and AccessControlEnumerableUpgradeable, but it not calling the init functions for these contracts in its own initializer __rOUSG_init
function.
Proof of Concept:
The PausableUpgradeable
contract have __Pausable_init()
function which is used to Initializes the contract in unpaused state. but the rOUSG::__rOUSG_init
is not calling it.
/** * @dev Initializes the contract in unpaused state. */ function __Pausable_init() internal onlyInitializing { __Pausable_init_unchained(); } function __Pausable_init_unchained() internal onlyInitializing { _paused = false; }
Recommended Mitigation:
Ensure that the initializer __rOUSG_init
function of the contract rOUSG
calls the init functions of all inherited upgradeable contracts.
transfer
and transferFrom
functions.Description:
The rOUSG.sol
and ousgInstantManager.sol
contract does not handle the boolean return from the transfer and transferFrom functions as recommended by the ERC-20 standard. The ERC-20 standard specifies that Callers MUST handle false from returns (bool success). Callers MUST NOT assume that false is never returned!
As EIP20 Quotes:
NOTES:
Impact: Not handling the bool return value from transfer and transferFrom functions lead to a lack of proper error handling,
Proof of Concept:
The unwrap function of rOUSG
contract calling the transfer
function to return the OUSG token back to msg.sender but missing handling the return bool.
function unwrap(uint256 _rOUSGAmount) external whenNotPaused { require(_rOUSGAmount > 0, "rOUSG: can't unwrap zero rOUSG tokens"); uint256 ousgSharesAmount = getSharesByROUSG(_rOUSGAmount); if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER) revert UnwrapTooSmall(); _burnShares(msg.sender, ousgSharesAmount); @> ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER ); emit Transfer(msg.sender, address(0), _rOUSGAmount); emit TransferShares(msg.sender, address(0), ousgSharesAmount); }
Recommended Mitigation:
The recommended mitigation is to handle the return bool value where ever the transfer
and transferFrom
function is called in the rOUSG
and ousgInstantManager
contract because it is better engineering/error handling practices.
Coded mitigation:
function unwrap(uint256 _rOUSGAmount) external whenNotPaused { require(_rOUSGAmount > 0, "rOUSG: can't unwrap zero rOUSG tokens"); uint256 ousgSharesAmount = getSharesByROUSG(_rOUSGAmount); if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER) revert UnwrapTooSmall(); _burnShares(msg.sender, ousgSharesAmount); + bool success = ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER //KYC check is implicit ); + require(success, "rOUSG: Failed to transfer!"); emit Transfer(msg.sender, address(0), _rOUSGAmount); emit TransferShares(msg.sender, address(0), ousgSharesAmount); }
getOUSGPrice()
check.Description:
The getOUSGPrice
function checks a requirement to ensure that the price of OUSG in USDC is above a MINIMUM_OUSG_PRICE which is 105e18. But it does not include the MINIMUM_OUSG_PRICE in check, It will be better to include the MINIMUM_OUSG_PRICE in check cause for majority of time the price of OUSG was below 105e18 it is only 16 feb when the OUSG price cross the MINIMUM_OUSG_PRICE mark.
Impact: Overly strict check do not understand the market price of OUSG.
Proof of Concept:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); //the pirce of OUSG in USDC require( @> price > MINIMUM_OUSG_PRICE, //MINIMUM_OUSG_PRICE = 105e18 "OUSGInstantManager::getOUSGPrice: Price unexpectedly low" ); }
Recommended Mitigation:
The recommended mitigation is to include MINIMUM_OUSG_PRICE in getOUSGPrice()
function check.
Coded recommendation:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); //the pirce of OUSG in USDC require( - price > MINIMUM_OUSG_PRICE, + price >= MINIMUM_OUSG_PRICE, "OUSGInstantManager::getOUSGPrice: Price unexpectedly low" ); }
ousgInstantManager
contract.Description:
The setMintFee
and setRedeemFee
functions are used for configuration of the minting fee and redeem fee. But these both function use 200 as the max fee and it is magic number, cause it is hard-coded numeric constants and lack explanation. So it is better to initialize a constant variable MAX_FEE with 200 and than use it in functions rather than direct 200.
Impact: The magic numbers in the code reduces code readability, so it is better to not uses them.
Proof of Concept:
setMintfee
function:function setMintFee( uint256 _mintFee ) external override onlyRole(CONFIGURER_ROLE) { //@audit-info 200 is magic number @> require(mintFee < 200, "OUSGInstantManager::setMintFee: Fee too high"); emit MintFeeSet(mintFee, _mintFee); mintFee = _mintFee; }
setRedeemFee
function:function setRedeemFee( uint256 _redeemFee ) external override onlyRole(CONFIGURER_ROLE) {//@audit-info 200 is magic number @> require(redeemFee < 200, "OUSGInstantManager::setRedeemFee: Fee too high"); emit RedeemFeeSet(redeemFee, _redeemFee); redeemFee = _redeemFee; }
Recommended Mitigation: The recommended mitigation is to initialize a constant variable MAX_FEE with 200 and than use it. Coded Recommendation:
+ uint256 public constant MAX_FEE = 200;
#0 - c4-pre-sort
2024-04-05T05:33:11Z
0xRobocop marked the issue as sufficient quality report
#1 - 3docSec
2024-04-10T07:25:43Z
I-01 is inconsequential I-04 is invalid
#2 - c4-judge
2024-04-10T07:25:46Z
3docSec marked the issue as grade-b