When the `returnNative` parameter is set to true in the `_params` provided to `MoneyMarketHook.execute`, it is not handled properly and could disrupt user expectations #29
When param.returnNative is set to true while calling MoneyMarketHook.execute, users expect the returned token from the withdraw operation to be in native form and sent to the caller. However, in the current implementation, this is not considered and could disrupt user expectations.
Proof of Concept
The withdraw functionality inside MoneyMarketHook will process the WithdrawParams provided by users and construct the operations using _handleWithdraw, which consist of calling decollateralize and burnTo in InitCore, providing the parameters accordingly.
function _handleWithdraw(uint _offset, bytes[] memory _data, uint _initPosId, WithdrawParams[] calldata _params)
internal
view
returns (uint, bytes[] memory)
{
for (uint i; i < _params.length; i = i.uinc()) {
// decollateralize to pool
_data[_offset] = abi.encodeWithSelector(
IInitCore.decollateralize.selector, _initPosId, _params[i].pool, _params[i].shares, _params[i].pool
);
_offset = _offset.uinc();
// burn collateral to underlying token
address helper = _params[i].rebaseHelperParams.helper;
address uTokenReceiver = _params[i].to;
// if need to unwrap to rebase token
if (helper != address(0)) {
address uToken = ILendingPool(_params[i].pool).underlyingToken();
_require(
_params[i].rebaseHelperParams.tokenIn == uToken
&& IRebaseHelper(_params[i].rebaseHelperParams.helper).YIELD_BEARING_TOKEN() == uToken,
Errors.INVALID_TOKEN_IN
);
uTokenReceiver = helper;
}
_data[_offset] = abi.encodeWithSelector(IInitCore.burnTo.selector, _params[i].pool, uTokenReceiver);
_offset = _offset.uinc();
}
return (_offset, _data);
}
As it can be observed, _handleWithdraw doesn't check param.returnNative and not adjust the uTokenReceiver token receiver to address(this) when param.returnNative is set to true.
Now, when execute finish perform the multicall and check that _params.returnNative is set to true, it will not work properly as the token is not send to the Hook.
function execute(OperationParams calldata _params)
external
payable
returns (uint posId, uint initPosId, bytes[] memory results)
{
// create position if not exist
if (_params.posId == 0) {
(posId, initPosId) = createPos(_params.mode, _params.viewer);
} else {
// for existing position, only owner can execute
posId = _params.posId;
initPosId = initPosIds[msg.sender][posId];
_require(IERC721(POS_MANAGER).ownerOf(initPosId) == address(this), Errors.NOT_OWNER);
}
results = _handleMulticall(initPosId, _params);
// check slippage
_require(_params.minHealth_e18 <= IInitCore(CORE).getPosHealthCurrent_e18(initPosId), Errors.SLIPPAGE_CONTROL);
// unwrap token if needed
for (uint i; i < _params.withdrawParams.length; i = i.uinc()) {
address helper = _params.withdrawParams[i].rebaseHelperParams.helper;
if (helper != address(0)) IRebaseHelper(helper).unwrap(_params.withdrawParams[i].to);
}
// return native token
if (_params.returnNative) {
>>> IWNative(WNATIVE).withdraw(IERC20(WNATIVE).balanceOf(address(this)));
>>> (bool success,) = payable(msg.sender).call{value: address(this).balance}('');
_require(success, Errors.CALL_FAILED);
}
}
This could disrupt user expectations. Consider a third-party contract integrated with this hook that can only operate using native balance, doesn't expect, and cannot handle tokens/ERC20. This can cause issues for the integrator.
Tools Used
Manual review.
Recommended Mitigation Steps
When constructing withdraw operation, check if _params.returnNative is set to true, and change uTokenReceiver to address(this.
- function _handleWithdraw(uint _offset, bytes[] memory _data, uint _initPosId, WithdrawParams[] calldata _params)
+ function _handleWithdraw(uint _offset, bytes[] memory _data, uint _initPosId, WithdrawParams[] calldata _params, bool _returnNative)
internal
view
returns (uint, bytes[] memory)
{
for (uint i; i < _params.length; i = i.uinc()) {
// decollateralize to pool
_data[_offset] = abi.encodeWithSelector(
IInitCore.decollateralize.selector, _initPosId, _params[i].pool, _params[i].shares, _params[i].pool
);
_offset = _offset.uinc();
// burn collateral to underlying token
address helper = _params[i].rebaseHelperParams.helper;
address uTokenReceiver = _params[i].to;
// if need to unwrap to rebase token
if (helper != address(0)) {
address uToken = ILendingPool(_params[i].pool).underlyingToken();
_require(
_params[i].rebaseHelperParams.tokenIn == uToken
&& IRebaseHelper(_params[i].rebaseHelperParams.helper).YIELD_BEARING_TOKEN() == uToken,
Errors.INVALID_TOKEN_IN
);
uTokenReceiver = helper;
}
+ if (_returnNative && uToken == WNATIVE) {
+ uTokenReceiver = address(this);
+ }
_data[_offset] = abi.encodeWithSelector(IInitCore.burnTo.selector, _params[i].pool, uTokenReceiver);
_offset = _offset.uinc();
}
return (_offset, _data);
}
Lines of code
https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/hook/MoneyMarketHook.sol#L168-L196 https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/hook/MoneyMarketHook.sol#L76-L80
Vulnerability details
Impact
When
param.returnNative
is set to true while callingMoneyMarketHook.execute
, users expect the returned token from the withdraw operation to be in native form and sent to the caller. However, in the current implementation, this is not considered and could disrupt user expectations.Proof of Concept
The withdraw functionality inside
MoneyMarketHook
will process theWithdrawParams
provided by users and construct the operations using_handleWithdraw
, which consist of callingdecollateralize
andburnTo
inInitCore
, providing the parameters accordingly.https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/hook/MoneyMarketHook.sol#L168-L196
As it can be observed,
_handleWithdraw
doesn't checkparam.returnNative
and not adjust theuTokenReceiver
token receiver toaddress(this)
whenparam.returnNative
is set to true.Now, when
execute
finish perform the multicall and check that_params.returnNative
is set to true, it will not work properly as the token is not send to the Hook.https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/hook/MoneyMarketHook.sol#L76-L80
This could disrupt user expectations. Consider a third-party contract integrated with this hook that can only operate using native balance, doesn't expect, and cannot handle tokens/ERC20. This can cause issues for the integrator.
Tools Used
Manual review.
Recommended Mitigation Steps
When constructing withdraw operation, check if
_params.returnNative
is set to true, and changeuTokenReceiver
toaddress(this
.Assessed type
Context