Withdrawing.sol::withdraw_delegatecall()
- It's possible for users to unintentionally withdraw zero amounts while still paying fees, which is rarely expected or accepted functionality.
Submitted on Thu Aug 15 2024 22:52:26 GMT-0400 (Atlantic Standard Time) by @OxSCSamurai for Boost | IDEX
Report ID: #34566
Report type: Smart Contract
Report severity: Insight
Target: https://github.com/idexio/idex-contracts-ikon/blob/main/contracts/libraries/Withdrawing.sol
Impacts:
- Contract fails to deliver promised returns, but doesn't lose value
- The problem is that the current logic allows for users to withdraw an amount equal to the
maximumGasFee
, which will cause the user to receive nothing but still pay withdrawal transaction fees. - There seems to be no checks to prevent this from happening.
The affected require statement:
require(
arguments.withdrawal.gasFee <= arguments.withdrawal.maximumGasFee &&
arguments.withdrawal.maximumGasFee <= arguments.withdrawal.grossQuantity, /// @audit-issue allows for users to receive zero withdrawal amounts while still paying fees...
"Excessive withdrawal fee"
);
Contract fails to deliver promised returns, but doesn't lose value
- low severity >>> Absolutely no user ever expects to receive zero amount when completing a withdrawal transaction successfully.- Any user would expect to receive a withdrawn amount of > 0, therefore the contract fails to deliver on promised or expected returns or behaviour in terms of withdrawal functionality.
Expected Returns:
- Users generally expect that when they initiate a withdrawal, they will receive a certain value or asset in return for their transaction. In this case, if a user attempts to withdraw assets and receives zero value, the contract is failing to deliver the expected returns.
- Additionally, the require statement's error/revert message states
Excessive withdrawal fee
, which is when withdrawal fee is greater than the withdraw amount. This means the user will NOT get > 0 out in withdrawal amounts, so that's why the require check is there to prevent zero amount withdrawals. But the require check unfortunately fails to prevent zero amount withdrawals for casearguments.withdrawal.maximumGasFee == arguments.withdrawal.grossQuantity
. - So there's an inconsistency here: on one hand the require check is there to prevent a situation where a user withdraws and receives a zero amount of tokens, but the check fails to account for all possible cases where withdrawn amount will end up being zero.
User Experience:
- The ability to withdraw zero value while still being charged fees can negatively impact the user experience. Users may feel that they are being penalized for attempting to withdraw their assets, even if the contract itself is not losing value.
- Such scenarios can affect the trust and credibility of the protocol. Users may question the fairness of the fee structure and the overall design of the contract if they find themselves in situations where they incur costs without receiving any tangible benefits.
idex-contracts-ikon/test/withdrawals.ts
:
it('should revert on excessive withdrawal fee', async function () {
await expect(
exchange
.connect(dispatcherWallet)
.withdraw(
...getWithdrawArguments(withdrawal, '0.70000000', signature),
),
).to.eventually.be.rejectedWith(/excessive withdrawal fee/i);
//withdrawal.maximumGasFee = '1.10000000';
withdrawal.maximumGasFee = '1.00000000'; /// @audit added for PoC/testing purposes >>> withdrawal amount is 1. Due to bug, it should not revert here but allow user to withdraw ZERO amount.
signature = await traderWallet.signTypedData(
...getWithdrawalSignatureTypedData(
withdrawal,
await exchange.getAddress(),
),
);
await expect(
exchange
.connect(dispatcherWallet)
.withdraw(
...getWithdrawArguments(withdrawal, '0.10000000', signature),
),
).to.eventually.be.rejectedWith(/excessive withdrawal fee/i);
});
Test 1: Demonstrating that a user can receive zero amount after trying to withdraw > 0 amount, and still be charged fees:
Using:
require(
arguments.withdrawal.gasFee <= arguments.withdrawal.maximumGasFee &&
arguments.withdrawal.maximumGasFee <= arguments.withdrawal.grossQuantity,
//arguments.withdrawal.maximumGasFee < arguments.withdrawal.grossQuantity, /// @audit added for PoC/testing purposes
"Excessive withdrawal fee"
);
$ npm run test test/withdrawals.ts -- --grep "should revert on excessive withdrawal fee"
> @idexio/[email protected] test
> COVERAGE=1 hardhat test test/withdrawals.ts --grep should revert on excessive withdrawal fee
Compiled 3 Solidity files successfully (evm target: paris).
Exchange
withdraw
1) should revert on excessive withdrawal fee
0 passing (3s)
1 failing
1) Exchange
withdraw
should revert on excessive withdrawal fee:
AssertionError: expected promise to be rejected with an error matching /excessive withdrawal fee/i but it was fulfilled with ContractTransactionResponse{ …(18) }
- The withdraw transaction was successful, and user has received zero withdrawal amount but was still charged fees...
Test 2: Demonstrating the bugfix, now the user shouldnt be able to receive zero withdrawal amount. The transaction should revert.
- Yes, the user probably still pays miner/validator fees here(for reverted/failed txs) but this is expected.
- But it's not expected to have a successful withdrawal transaction and receive zero withdrawal amount. Using:
require(
arguments.withdrawal.gasFee <= arguments.withdrawal.maximumGasFee &&
//arguments.withdrawal.maximumGasFee <= arguments.withdrawal.grossQuantity,
arguments.withdrawal.maximumGasFee < arguments.withdrawal.grossQuantity, /// @audit added for PoC/testing purposes
"Excessive withdrawal fee"
);
$ npm run test test/withdrawals.ts -- --grep "should revert on excessive withdrawal fee"
> @idexio/[email protected] test
> COVERAGE=1 hardhat test test/withdrawals.ts --grep should revert on excessive withdrawal fee
Compiled 3 Solidity files successfully (evm target: paris).
Exchange
withdraw
✔ should revert on excessive withdrawal fee
1 passing (3s)
- The withdraw tx failed/reverted, as it should.
- With the bugfix implemented the withdraw tx now reverts, preventing user from receiving zero withdraw amount.
- To prevent users from withdrawing an amount that results in zero value after fees, ensure that the gross quantity is greater than the maximum gas fee while also verifying that the specified gas fee does not exceed the maximum allowable gas fee:
require(
arguments.withdrawal.gasFee <= arguments.withdrawal.maximumGasFee &&
- arguments.withdrawal.maximumGasFee <= arguments.withdrawal.grossQuantity,
+ arguments.withdrawal.maximumGasFee < arguments.withdrawal.grossQuantity,
"Excessive withdrawal fee"
);