Build1 publisher3 min readPublished
The decimals offset decided whether the victim lost 50 tokens or 4.98
A Foundry fixture runs the same donation attack against two OpenZeppelin vaults, and the depositor ends with zero shares in one and shares worth 45.02 of 50 tokens in the other. In the zero-offset run the attacker burned about 4,975 tokens to cause it.
The Engineer · Build desk

What happened
- The same sequence run against a vault with a decimals offset of three left the victim holding shares redeemable for about 45.02 tokens instead of nothing at all.
- A third fixture starts with 500 donated tokens and no shares outstanding, and the next depositor pays in 1,000 tokens for shares redeemable for 750.
- The author set out to build a reusable, hardened ERC-4626 base contract and shifted to a Foundry test suite once donation accounting and strategy valuation turned out to be per-vault design choices.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A pass condition of "the depositor received shares" clears both the 45.02-token case and the 750-on-1,000 case, so a suite that never redeems and never denominates in the deposit token cannot see either loss.
- decision Anyone deploying an OpenZeppelin vault is picking a depositor outcome when they pick the decimals offset, which puts that value in the deployment review alongside the strategy.
- exposure At about 99.5 tokens burned per token destroyed, the sequence is affordable to anyone funding vandalism, so a fresh vault's first depositor is reachable without any profit motive on the attacker's side.
- constraint One donation size against one implementation cannot be cited as evidence that other vaults are safe, nor that the attack never pays for a better-funded attacker.
Before the victim arrived, the attacker held one base unit of an 18-decimal token, which is 10^-18 of a token [6][29], and had transferred 10,000 tokens straight into the vault [6]. The 50-token deposit that followed was 0.5% of that donation [28]. It minted zero shares [5]. Redemption amounts under ERC-4626 depend on the implementation's rounding, its fees, and any virtual assets and shares in the calculation [2]. This fixture is an OpenZeppelin-based vault with a decimals offset of zero [7].
OpenZeppelin's virtual asset and share accounting keeps part of a donation out of the donor's reach [8]. In this sequence, destroying a 50-token deposit cost the attacker roughly 4,975 tokens [9]. That is 99.5 tokens burned per token taken from the depositor [25], and 49.75% of the donation [26]. The run produced no attacker profit [11]. "It demonstrates griefing," the author wrote [10].
One parameter changes the failure mode. With a decimals offset of three, the same attacker deposit, the same 10,000-token donation and the same 50-token victim deposit leave the victim holding shares redeemable for about 45.02 tokens [13]. The shortfall is 4.98 tokens, 9.96% of what went in [24].
"A test asserting only that the victim received shares would miss that loss," the author wrote [14]. A third fixture is the same problem with rounder numbers: 500 donated tokens sitting in a vault with no shares outstanding, a 1,000-token deposit, shares redeemable for 750 [17]. The loss is 25%, sitting under a nonzero balance [27]. A shares-greater-than-zero assertion catches exactly one of the three cases here, the one where the depositor received nothing at all.
The comparison between the two vaults has to be picked carefully. Raw share counts are not comparable across them, because the offset changes share precision [15]. What the suite compares instead is the amount of underlying assets those shares can recover [16], and the figures in these fixtures are underlying-token amounts, excluding gas [22].
The project, called Vault Price Honesty, began as something else [23]. The author set out to write a reusable, hardened ERC-4626 base contract, then shifted to a Foundry suite [18]. Donation accounting, strategy valuation and the treatment of assets left after the last redemption depend on the vault's design, and shared protections do not settle those choices [19]. Two of the five situations in the suite are not bugs on sight. A preview that disagrees with execution has to be checked against ERC-4626's preview requirements before anyone calls it a violation [20]. Reported assets can legitimately differ from the balance held at the vault address, because a strategy may hold assets elsewhere [21].
One sequence in one fixture does not establish the economics of other donation amounts, multiple victims or a different vault implementation [12]. For the 4,975-token figure to say anything about another vault, that vault would need the same virtual-share accounting [8], the same offset [7], an 18-decimal asset [6] and a comparable ratio of donation to deposit [28]. What generalises is weaker: a direct donation can raise the value of existing shares without harming a later depositor [4]. The author wrote that "an unexpected price change is not enough to call the accounting wrong" [3].
What to watch
- A fixture in which the attacker ends up ahead; that would move the finding from griefing to a funded attack.
- Whether the suite reports every case in redeemable underlying, so vaults with different decimals offsets can be compared at all.
- Whether the hardened ERC-4626 base contract the project started as ever ships, once the design-dependent cases are separated out.