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: 24/73
Findings: 3
Award: $434.74
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: immeas
Also found by: 0xComfyCat, Vagner, ast3ros, kutugu, markus_ether, volodya
303.1226 USDC - $303.12
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L266 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L352-L389
The Validated Action Attestation (VAA) includes an intendedRecipient
field that is employed to ascertain the designated receiver of a message. Considering that the Wormhole message is multicasted, it is advisable to authenticate the intendedRecipient
at the receiving end.
In the context of the TemporalGovernor, the validation is performed during _queueProposal
, thus ensuring that any queued proposal is specifically intended for the TemporalGovernor. It is therefore deemed acceptable to forgo this verification during execution in _executeProposal
. However, if the intendedRecipient
is not verified, the Guardian could potentially enforce the execution of a VAA that is not meant for the TemporalGovernor contract, by means of fastTrackProposalExecution
.
The following Forge test demonstrates that TemporalGovernor's fastTrackProposalExecution
does not reject a VAA that incorporates an invalid intendedRecipient
.
function testTemporalGovernorIntendedRecipientCheck() public { // Setup address mockWormholeCore = address(1234); ITemporalGovernor.TrustedSender[] memory trustedSenders = new ITemporalGovernor.TrustedSender[](1); trustedSenders[0] = ITemporalGovernor.TrustedSender(1, address(this)); TemporalGovernor gov = new TemporalGovernor(mockWormholeCore, 1 days, 0, trustedSenders); IWormhole.VM memory mockVM; mockVM.emitterChainId = 1; mockVM.emitterAddress = bytes32(bytes20(address(this))) >> 96; address[] memory targets = new address[](1); uint256[] memory values = new uint256[](1); bytes[] memory datas = new bytes[](1); // intendedRecipient is not TemporalGovernor mockVM.payload = abi.encode(address(123), targets, values, datas); vm.mockCall(mockWormholeCore, abi.encodeWithSignature("parseAndVerifyVM(bytes)"), abi.encode(mockVM, true, "")); // queueProposal rejects vm.expectRevert("TemporalGovernor: Incorrect destination"); gov.queueProposal(""); // fastTrackProposalExecution accept and execute it vm.expectCall(address(0), "", 1); gov.fastTrackProposalExecution(""); }
Incorporate an intendedRecipient
validation into the internal function _executeProposal
.
// Ensure the emitterAddress of this VAA is a trusted address require( trustedSenders[vm.emitterChainId].contains(vm.emitterAddress), /// allow multiple per chainid "TemporalGovernor: Invalid Emitter Address" ); require( !queuedTransactions[vm.hash].executed, "TemporalGovernor: tx already executed" ); queuedTransactions[vm.hash].executed = true; address intendedRecipient; address[] memory targets; /// contracts to call uint256[] memory values; /// native token amount to send bytes[] memory calldatas; /// calldata to send (intendedRecipient , targets, values, calldatas) = abi.decode( vm.payload, (address, address[], uint256[], bytes[]) ); // @audit - To prevent fastracking wrong intendedRecipient, we check it again here require(intendedRecipient == address(this), "TemporalGovernor: Incorrect destination");
Invalid Validation
#0 - c4-pre-sort
2023-08-03T13:24:15Z
0xSorryNotSorry marked the issue as duplicate of #308
#1 - c4-judge
2023-08-12T20:27:39Z
alcueca marked the issue as satisfactory
86.7417 USDC - $86.74
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L27-L424 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L382-L388
It is conceivable that a proposal might incorporate a call that transmits ether. During _executeProposal
, the values
array from the payload is decoded and an attempt to send it is made accordingly.
address[] memory targets; /// contracts to call uint256[] memory values; /// native token amount to send bytes[] memory calldatas; /// calldata to send (, targets, values, calldatas) = abi.decode( vm.payload, (address, address[], uint256[], bytes[]) );
While the call's implementation is correct, executing such a proposal is not feasible. The contract lacks any means to receive ether, as there are no payable functions or receive
functions. Hence, it is not possible for anyone to fund the TemporalGovernor with ether, except via selfdestruct
, which is not advisable.
The following Forge tests demonstrate that:
function testTemporalGovernorCannotExecuteSendEther() public { // Setup address mockWormholeCore = address(1234); ITemporalGovernor.TrustedSender[] memory trustedSenders = new ITemporalGovernor.TrustedSender[](1); trustedSenders[0] = ITemporalGovernor.TrustedSender(1, address(this)); TemporalGovernor gov = new TemporalGovernor(mockWormholeCore, 1 days, 0, trustedSenders); // Can't send ether to TemporalGovernor deal(address(this), 1 ether); vm.expectRevert(); payable(address(gov)).transfer(1 ether); // Mock proposal that attempt to sends ether IWormhole.VM memory mockVM; mockVM.emitterChainId = 1; mockVM.emitterAddress = bytes32(bytes20(address(this))) >> 96; address[] memory targets = new address[](1); uint256[] memory values = new uint256[](1); values[0] = 1 ether; bytes[] memory datas = new bytes[](1); mockVM.payload = abi.encode(address(gov), targets, values, datas); vm.mockCall(mockWormholeCore, abi.encodeWithSignature("parseAndVerifyVM(bytes)"), abi.encode(mockVM, true, "")); // Can queue but can't execute gov.queueProposal(""); vm.expectRevert(); gov.executeProposal(""); }
There are two possible mitigation strategies, contingent upon whether or not the TemporalGovernor needs to send ether:
receive
fallback function or a payable function to accept ether.values
field in the payload.Payable
#0 - c4-pre-sort
2023-08-03T13:21:12Z
0xSorryNotSorry marked the issue as duplicate of #268
#1 - c4-judge
2023-08-12T20:36:48Z
alcueca marked the issue as satisfactory
🌟 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
_reduceReserve
callmint
and redeem
would revert when transfer is pausedborrowerIndex
in repayBorrowFresh
_reduceReserve
callThe _reduceReserve
function, which initiates the transfer of tokens out of the mToken contract, inherently necessitates a sufficient balance within the mToken contract to facilitate the transfer.
// Fail gracefully if protocol has insufficient underlying cash if (getCashPrior() < reduceAmount) { return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDUCE_RESERVES_CASH_NOT_AVAILABLE); }
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L1356-L1359
Potential exists for an ill-intentioned actor to manipulate the cash reserve through borrowing, consequently preventing the execution of the proposal. It is prudent, therefore, to avoid combining the _reduceReserve
function with other high-stakes calls such as lowering the collateral factor in the same proposal.
mint
and redeem
would revert when transfer is pausedThe mint and redeem functions of the WETHRouter involve the transfer of mTokens as part of the wrapping process. Consequently, these functions may cease to function if the transfer operation is paused. It is, therefore, recommended that the DApp frontend be designed to handle this particular case seamlessly, enabling the user to mint/redeem as WETH via mToken even in the event of a transfer pause.
function mint(address recipient) external payable { weth.deposit{value: msg.value}(); require(mToken.mint(msg.value) == 0, "WETHRouter: mint failed"); // @audit - Revert if transfer is paused IERC20(address(mToken)).safeTransfer( recipient, mToken.balanceOf(address(this)) ); } function redeem(uint256 mTokenRedeemAmount, address recipient) external { // @audit - Revert if transfer is paused IERC20(address(mToken)).safeTransferFrom( msg.sender, address(this), mTokenRedeemAmount ); ... }
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/router/WETHRouter.sol#L36-L39 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/router/WETHRouter.sol#L46-L50
The design of ChainlinkOracle is such that it does not accommodate the support of underlying tokens that have more than 18 decimals. This necessary verification appears to be absent in the initialization process of mToken.
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L26
With uint32, contract would last until 2106
borrowerIndex
in repayBorrowFresh
/* We remember the original borrowerIndex for verification purposes */ vars.borrowerIndex = accountBorrows[borrower].interestIndex;
https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/MToken.sol#L879-L880
#0 - c4-judge
2023-08-11T22:18:18Z
alcueca marked the issue as grade-a
#1 - c4-sponsor
2023-08-12T00:49:56Z
ElliotFriedman marked the issue as sponsor acknowledged