Omniscia Mean Finance Audit
ArbitraryExecutionPermit2Adapter Manual Review Findings
ArbitraryExecutionPermit2Adapter Manual Review Findings
AEP-01M: Potential Increase of Functionality
Type | Severity | Location |
---|---|---|
Logical Fault | ArbitraryExecutionPermit2Adapter.sol:L86-L92 |
Description:
Given that the ArbitraryExecutionPermit2Adapter
contract is meant to acquire the necessary funds from the caller and perform a sequence of calls with them, it is expected that any leftover funds are to be refunded to the caller.
The current code relies on a properly structured _transferOut
entry that may ultimately not be supplied by the function's callers, leading to leftover funds being up for grabs by any party.
Impact:
While the function will behave as expected if used properly, we advise minimizing its reliance on input arguments to ensure that callers can interact with the contract in a safety-first manner rather than a flexibility-first approach.
Example:
25/// @inheritdoc IArbitraryExecutionPermit2Adapter26function executeWithPermit(27 SinglePermit calldata _permit,28 AllowanceTarget[] calldata _allowanceTargets,29 ContractCall[] calldata _contractCalls,30 TransferOut[] calldata _transferOut,31 uint256 _deadline32)33 external34 payable35 checkDeadline(_deadline)36 returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)37{38 PERMIT2.takeFromCaller(_permit.token, _permit.amount, _permit.nonce, _deadline, _permit.signature);39 return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);40}41
42/// @inheritdoc IArbitraryExecutionPermit2Adapter43function executeWithBatchPermit(44 BatchPermit calldata _batchPermit,45 AllowanceTarget[] calldata _allowanceTargets,46 ContractCall[] calldata _contractCalls,47 TransferOut[] calldata _transferOut,48 uint256 _deadline49)50 external51 payable52 checkDeadline(_deadline)53 returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)54{55 PERMIT2.batchTakeFromCaller(_batchPermit.tokens, _batchPermit.nonce, _deadline, _batchPermit.signature);56 return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);57}58
59function _approveExecuteAndTransfer(60 AllowanceTarget[] calldata _allowanceTargets,61 ContractCall[] calldata _contractCalls,62 TransferOut[] calldata _transferOut63)64 internal65 returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)66{67 // Approve targets68 for (uint256 i; i < _allowanceTargets.length;) {69 IERC20(_allowanceTargets[i].token).maxApprove(_allowanceTargets[i].allowanceTarget);70 unchecked {71 ++i;72 }73 }74
75 // Call contracts76 _executionResults = new bytes[](_contractCalls.length);77 for (uint256 i; i < _contractCalls.length;) {78 _executionResults[i] =79 _contractCalls[i].target.functionCallWithValue(_contractCalls[i].data, _contractCalls[i].value);80 unchecked {81 ++i;82 }83 }84
85 // Distribute tokens86 _tokenBalances = new uint256[](_transferOut.length);87 for (uint256 i; i < _transferOut.length;) {88 _tokenBalances[i] = _transferOut[i].token.distributeTo(_transferOut[i].distribution);89 unchecked {90 ++i;91 }92 }93}
Recommendation:
We advise the code to either build an automatic _transferOut
list from the supplied input tokens of an arbitrary call, or to validate that the _transferOut
array contains at least all input tokens of an arbitrary call.
We consider either of the two approaches sufficient for alleviating this exhibit.
Alleviation:
The Mean Finance team evaluated this exhibit and opted to acknowledge it as they wish to leave the onus of proper calls on the caller.
AEP-02M: Lingering Token Approvals
Type | Severity | Location |
---|---|---|
Logical Fault | ArbitraryExecutionPermit2Adapter.sol:L69 |
Description:
The ArbitraryExecutionPermit2Adapter
contract is meant to act as an intermediary in a variety of interactions with other protocols. In the current implementation, the contract will approve the maximum amount possible to a potential call target.
As such, a non-zero approval may remain after the call. This can be exploited by scammers and other similar hijacking attacks by approving a malicious contract and attempt to inject this contract in an unsuspecting user's call-chain even if they never approved it explicitly during their invocation of the function.
Impact:
The current implementation of ArbitraryExecutionPermit2Adapter::_approveExecuteAndTransfer
is not atomic, permitting it to be exploited in numerous ways including re-entrancies as well as secondary-call approval attacks.
Example:
59function _approveExecuteAndTransfer(60 AllowanceTarget[] calldata _allowanceTargets,61 ContractCall[] calldata _contractCalls,62 TransferOut[] calldata _transferOut63)64 internal65 returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)66{67 // Approve targets68 for (uint256 i; i < _allowanceTargets.length;) {69 IERC20(_allowanceTargets[i].token).maxApprove(_allowanceTargets[i].allowanceTarget);70 unchecked {71 ++i;72 }73 }74
75 // Call contracts76 _executionResults = new bytes[](_contractCalls.length);77 for (uint256 i; i < _contractCalls.length;) {78 _executionResults[i] =79 _contractCalls[i].target.functionCallWithValue(_contractCalls[i].data, _contractCalls[i].value);80 unchecked {81 ++i;82 }83 }84
85 // Distribute tokens86 _tokenBalances = new uint256[](_transferOut.length);87 for (uint256 i; i < _transferOut.length;) {88 _tokenBalances[i] = _transferOut[i].token.distributeTo(_transferOut[i].distribution);89 unchecked {90 ++i;91 }92 }93}
Recommendation:
We advise all approvals performed before a function call to be zeroed out after the calls have been performed. Additionally, we advise the ArbitraryExecutionPermit2Adapter::_approveExecuteAndTransfer
function to be set as non-reentrant given that each Token::distributeTo
call can become re-entrant and could result in the loss of ensuing token funds.
Alleviation:
The Mean Finance team introduced a code segment right after the external calls are processed in the ArbitraryExecutionPermit2Adapter::_approveExecuteAndTransfer
function that resets the allowances to 1
, essentially "erasing" them after the external calls have concluded. As such, we consider this exhibit fully alleviated given that no lingering approvals will remain post-execution of a Permit2
arbitrary execution call.
AEP-03M: Race-Condition of Permit2 Approvals
Type | Severity | Location |
---|---|---|
Logical Fault | ArbitraryExecutionPermit2Adapter.sol:L38-L39, L55-L56 |
Description:
The ArbitraryExecutionPermit2Adapter
will integrate with the Permit2
contract in a non-standard way, consuming a direct permission of a transfer-from operation.
The contract is susceptible to on-chain conditions that would result in the compromise of a user's permitted funds.
To elaborate, the IPermit2::permitTransferFrom
function can be invoked by an arbitrary party. As such, an ArbitraryExecutionPermit2Adapter::executeWithPermit
function call can be detected by off-chain bots and its permit consumed by a direct call to the Permit2
contract.
As the ArbitraryExecutionPermit2Adapter
permits arbitrary executions, the consumption of the permit can be coupled with a call to the contract that siphons those funds to the malicious party.
Impact:
It is presently possible to siphon any funds supplied as part of an ArbitraryExecutionPermit2Adapter::executeWithPermit
or ArbitraryExecutionPermit2Adapter::executeWithBatchPermit
function call due to the way these functions attempt to interact with the Permit2
contract of Uniswap.
Example:
25/// @inheritdoc IArbitraryExecutionPermit2Adapter26function executeWithPermit(27 SinglePermit calldata _permit,28 AllowanceTarget[] calldata _allowanceTargets,29 ContractCall[] calldata _contractCalls,30 TransferOut[] calldata _transferOut,31 uint256 _deadline32)33 external34 payable35 checkDeadline(_deadline)36 returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)37{38 PERMIT2.takeFromCaller(_permit.token, _permit.amount, _permit.nonce, _deadline, _permit.signature);39 return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);40}41
42/// @inheritdoc IArbitraryExecutionPermit2Adapter43function executeWithBatchPermit(44 BatchPermit calldata _batchPermit,45 AllowanceTarget[] calldata _allowanceTargets,46 ContractCall[] calldata _contractCalls,47 TransferOut[] calldata _transferOut,48 uint256 _deadline49)50 external51 payable52 checkDeadline(_deadline)53 returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)54{55 PERMIT2.batchTakeFromCaller(_batchPermit.tokens, _batchPermit.nonce, _deadline, _batchPermit.signature);56 return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);57}
Recommendation:
We advise the code to revise its Permit2
integration approach. Similarly to the universal router of Uniswap, the Mean Finance team may opt to utilize the IPermit2::transferFrom
functions that will attempt to consume an existing approval and supply the "from" argument as the msg.sender
.
Alleviation:
After consulting with the Mean Finance team as well as revisiting the Uniswap codebase, we have concluded that the original finding was incorrect given that the Permit2
system of Uniswap performs caller validation at a nested call.
As such, we consider this exhibit nullified.