Platform: Code4rena
Start Date: 24/07/2023
Pot Size: $100,000 USDC
Total HM: 18
Participants: 73
Period: 7 days
Judge: alcueca
Total Solo HM: 8
Id: 267
League: ETH
Rank: 41/73
Findings: 2
Award: $88.25
🌟 Selected for report: 0
🚀 Solo Findings: 0
43.3709 USDC - $43.37
The function _executeProposal in TemporalGovernor contract will fail, if there is a value to send with the call to the targets
_executeProposal function could send native token out along with a call to the targets encoded in vm.payload, but the current implementation has no recieve function in place to receive any native tokens
This will make the function unusable when a value is to be sent along with the call, as all subsequent calls with not enough native token balance in the contract will revert this calls
function _executeProposal(bytes memory VAA, bool overrideDelay) private { ############# for (uint256 i = 0; i < targets.length; i++) { address target = targets[i]; uint256 value = values[i]; bytes memory data = calldatas[i]; // Go make our call, and if it is not successful revert with the error bubbling up (bool success, bytes memory returnData) = target.call{value: value}( data ); /// revert on failure with error message if any require(success, string(returnData)); emit ExecutedTransaction(target, value, data); } }
Manual Review
I Recommend adding a receive function to receive native tokens, the receive function can then be restricted to accept tokens from only trusted addresses
call/delegatecall
#0 - c4-pre-sort
2023-08-03T13:20:55Z
0xSorryNotSorry marked the issue as duplicate of #268
#1 - c4-judge
2023-08-12T20:36:11Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-12T20:36:15Z
alcueca marked the issue as partial-50
#3 - c4-judge
2023-08-21T21:35:25Z
alcueca changed the severity to 2 (Med Risk)
🌟 Selected for report: immeas
Also found by: 0x70C9, 0xAnah, 0xArcturus, 0xComfyCat, 0xWaitress, 0xackermann, 0xkazim, 2997ms, 33audits, Arz, Aymen0909, ChrisTina, JP_Courses, John_Femi, Jorgect, Kaysoft, LosPollosHermanos, MohammedRizwan, Nyx, Rolezn, Sathish9098, Stormreckson, T1MOH, Tendency, Topmark, Udsen, Vagner, albertwh1te, ast3ros, banpaleo5, berlin-101, catellatech, cats, codetilda, cryptonue, eeshenggoh, fatherOfBlocks, hals, jamshed, jaraxxus, josephdara, kankodu, kodyvim, kutugu, lanrebayode77, mert_eren, nadin, naman1778, niki, petrichor, ravikiranweb3, said, solsaver, souilos, twcctop, wahedtalash77
44.8793 USDC - $44.88
Comptroller::allMarkets, is an array where there are just pushes. No upper bound, no pop.
As this array can grow quite large, the transaction’s gas cost could exceed the block gas limit and make it impossible to call this function at all here:
function _addMarketInternal(address mToken) internal { for (uint i = 0; i < allMarkets.length; i ++) { require(allMarkets[i] != MToken(mToken), "market already added"); } allMarkets.push(MToken(mToken)); }
Consider introducing a reasonable upper limit based on block gas limits
Here the condition should be ’<=’, not ’<’ to allow filling the cap fully
In Comptroller::borrowAllowed :
######### if (borrowCap != 0) { uint totalBorrows = MToken(mToken).totalBorrows(); uint nextTotalBorrows = add_(totalBorrows, borrowAmount); require(nextTotalBorrows < borrowCap, "market borrow cap reached"); // <--@audit Here } #########
should be:
require(nextTotalBorrows <= borrowCap, "market borrow cap reached");
It is possible to set close factor mantissa less than closeFactorMinMantissa and greater than closeFactorMaxMantissa as the method Comptroller::setCloseFactor does not limit the input parameters.
I recommend adding a check for the newCloseFactorMantissa parameter, restrict the input to be within the set limits
Due to MToken's inheritance of ERC20’s approve function, it is vulnerable to the ERC20 approve and double spend front running attack. Briefly, an authorized spender could spend both allowances by front running an allowance-changing transaction.
Consider implementing OpenZeppelin’s decreaseAllowance and increaseAllowance functions to help mitigate this.
The mToken administrator can call the _reduceReserves function to withdraw some of the reserves. However, the function enforces the receiver to be the admin
// doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. doTransferOut(admin, reduceAmount);
This merges roles that should probably be distinct, particularly when the administrator is replaced with a decentralized governance process.
Consider allowing the administrator to choose the recipient in the function call.
TemporalGovernor::setTrustedSenders emits a TrustedSenderUpdated event even when the sender wasn't set
In the for loop, the add method used checks if the trusted sender has previously been set at the chain id, if it has, it returns false. In such scenarios, the function still emits a TrustedSenderUpdated event
Since the add method returns true when the value is added and false when it is not, I will recommend checking the returns value, and emitting an event only when the returned value is true
The remove method used here from OZ EnumerableSet::remove function, checks the index, the value is stored at, if the index equals 0, meaning the value hasn't been previously added to the array, the boolean false is returned.
In such a scenario the function still emits a TrustedSenderUpdated removal event
function unSetTrustedSenders( TrustedSender[] calldata _trustedSenders ) external { require( msg.sender == address(this), "TemporalGovernor: Only this contract can update trusted senders" ); unchecked { for (uint256 i = 0; i < _trustedSenders.length; i++) { trustedSenders[_trustedSenders[i].chainId].remove( addressToBytes(_trustedSenders[i].addr) ); emit TrustedSenderUpdated( _trustedSenders[i].chainId, _trustedSenders[i].addr, false /// removed from list ); } } }
I will recommend checking the return value the removal operation and then emitting the event only if the return value equals true
/// @notice Allow the guardian to pause the contract /// removes the guardians ability to call pause again until governance reaaproves them /// starts the timer for the permissionless unpause /// cannot call this function if guardian is revoked function togglePause() external onlyOwner { if (paused()) { _unpause(); } else { require( guardianPauseAllowed, "TemporalGovernor: guardian pause not allowed" ); guardianPauseAllowed = false; lastPauseTime = block.timestamp.toUint248(); _pause(); } /// statement for SMT solver assert(!guardianPauseAllowed); /// this should be an unreachable state }
The comments says the function allows the guardian to pause the contract, but this isn't just the case. When the owner interacts with the togglePause function, if the contract is paused, the function unpauses the contract, it only pauses the contract if the above check is false
#0 - c4-judge
2023-08-12T17:41:12Z
alcueca marked the issue as grade-b
#1 - c4-judge
2023-08-12T17:41:23Z
alcueca marked the issue as grade-a
#2 - c4-judge
2023-08-12T17:41:27Z
alcueca marked the issue as grade-b