In the current implementation, the calculation of decimalOffset is dynamic and based on the difference between SYSTEM_DECIMALS (18) and the decimals of the indexToken:
This offset is used in the formula:
The purpose of adding 10 ** decimalOffset is to introduce a virtual share adjustment to mitigate donation attacks.
OpenZeppelin ERC-4626 Documentation
The value of decimalOffset depends on indexToken.decimals(), leading to inconsistent behavior across different tokens.
If indexToken.decimals() is 18, decimalOffset is 0, effectively bypassing the intended protection.
If indexToken.decimals() is 6, decimalOffset is 12, which may be overly restrictive.
IERC20Metadata(vault.indexToken).decimals()The decimals() function returns the sum of the asset token’s decimals and decimalsOffset:
decimalsOffset is set by the deployer of the ZLP vault.
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/zlp/ZlpVault.sol#L74
If an 18-decimal asset is used, decimalsOffset must be set to zero to ensure that indexToken.decimals() remains 18.
This means that the offset used to mitigate donation attacks will also be zero, nullifying its intended protection.
A decimalOffset of 0 results in an added value of 1, offering no real defense.
A decimalOffset of 1 or 2 introduces a minor buffer but still allows donation attacks.
The inconsistency makes the protocol vulnerable in some cases and overly strict in others.
The protocol includes a check that reverts deposits if they result in zero shares, preventing donation attacks. However, this allows attackers to create a denial-of-service (DoS) scenario for users with small balances by increasing the minimum asset deposit required to receive at least one share. This issue arises because, for tokens with 18 decimals, no effective offset is applied, making donations inexpensive.
In the createZlpVaults function used for testing, the decimalsOffset is determined by Constants.SYSTEM_DECIMALS - vaultsConfig[i].decimals:
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/script/vaults/Vaults.sol#L109
For an asset with 18 decimals:
The decimalsOffset in the vault is calculated as Constants.SYSTEM_DECIMALS - 18 = 0.
The indexToken decimals become 18 + 0 = 18.
The decimalOffset used for donation protection is 18 - 18 = 0.
This means that for assets with 18 decimals, no effective offset is applied, making donation attacks inexpensive and reducing the protocol’s protection against such exploits.
To ensure consistent and effective mitigation of donation attacks, we propose replacing the dynamic decimalOffset with a fixed value.
Instead of computing decimalOffset dynamically, define it as a constant:
Note: An offset of 3 forces an attacker to make a donation 1,000 times as large.
Then modify the calculation as follows:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.