Reproduced Exploit
Particle — openPosition charges a fee on the user's own marginFrom
openPosition sets the fee base to marginFrom + amountFromBorrowed. marginFrom is collateral the user posts, not a borrow. At ETH 3000, a long that posts 6000 of the quote asset is charged on that 6000. With feeFactor 100 and BASIS_POINT 10000 the fee is 60.
Chain
Ethereum
Category
logic
Date
Feb 2024
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 40715-openposition-should-not-charge-fees-on-marginfrom-or-charge. Standalone Foundry PoC and full write-up: 40715-openposition-should-not-charge-fees-on-marginfrom-or-charge_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/fee-calculation · vuln/defi/fee-manipulation
Key info#
| Protocol | Particle |
| Finding | Cantina, February 2024, AuditVault 40715 |
| Vulnerable code | openPosition fees marginFrom + amountFromBorrowed |
| Harm | A 6000 marginFrom with nothing borrowed pays 60. The fair fee is 0. |
| Chain | local synthetic (audited Ethereum contracts) |
TL;DR#
openPosition sets the fee base to marginFrom + amountFromBorrowed. marginFrom is collateral the user posts, not a borrow. At ETH 3000, a long that posts 6000 of the quote asset is charged on that 6000. With feeFactor 100 and BASIS_POINT 10000 the fee is 60.
The same open with marginFrom 0 and marginTo 2 charges 0. Users who follow the marginFrom path pay 60 that the borrowed-only formula does not charge.
The vulnerable code#
if (cache.feeFactor > 0) {
// @> VULN: marginFrom is the user's own collateral, but it is added into the fee base.
cache.feeAmount = ((params.marginFrom + cache.amountFromBorrowed) * cache.feeFactor) / Base.BASIS_POINT;
}
Fair base, which is what the report recommends:
cache.feeAmount = (cache.amountFromBorrowed * cache.feeFactor) / Base.BASIS_POINT;
Root cause#
The fee is described as a charge on borrowed principal, but the product also includes marginFrom. Supplying the other side as marginTo zeroes marginFrom and drops that part of the fee. The two ways of posting the same collateral do not pay the same fee.
Preconditions#
feeFactor is 100. The open borrows nothing, so amountFromBorrowed is 0. The user holds 6000 of the margin token. One transaction.
Attack walkthrough#
- The trader is minted 6000 and approves the book.
openPositionwithmarginFrom6000 andmarginTo0.- The fee line computes
(6000 + 0) * 100 / 10000 = 60. - The book pulls 60. Its balance is 60.
fairFee(0)is 0. - A second open with
marginFrom0 andmarginTo2 returns 0. The book still holds 60.
Control opens with marginTo only. The fee is 0.
Diagrams#
Impact#
The trader pays 60 units that a borrowed-only fee would not take. The marginTo path pays 0 for the same shape of position, so marginFrom users are the ones overcharged.
Remediation#
Drop marginFrom from the fee base, or charge marginTo as well so the two paths match.
How to reproduce#
cd 40715-openposition-should-not-charge-fees-on-marginfrom-or-charge_exp
forge test -vv
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/40715-openposition-should-not-charge-fees-on-marginfrom-or-charge.md
- Cantina report: https://cdn.cantina.xyz/reports/cantina_competition_particle_feb2024.pdf
- Reduced from the
feeAmountexpression quoted in that finding. Particle's server-side signature is not what this reduction exercises. The fee product itself is.
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 40715-openposition-should-not-charge-fees-on-marginfrom-or-charge_exp (evm-hack-registry mirror).
- AuditVault finding: 40715-openposition-should-not-charge-fees-on-marginfrom-or-charge.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.