Security disclosure for Clearpool PayFi Vaults: Medium caller-sensitive rate provider bypass #31
LoG1331
started this conversation in
Bug reports
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
I have been reviewing the Clearpool PayFi vault contracts during May 2026, primarily the clearpool-payfi-vaults repository at commit dccfd52659f3c00721554c7a09a55191ce36435c. I am submitting this through the documented GitHub Bug Reports channel so the Clearpool team can review and address it.
Project submission requirements checked on 2026-05-20:
Found 1 issue in total - Critical: 0, High: 0, Medium: 1, Low: 0. The issue is not anonymously exploitable, but it defeats the intended 5% rate-provider replacement guard for a privileged exchange-rate role and can cause vault shares to be over-minted against live deployed state.
2.1 Caller-sensitive rate providers can bypass the replacement deviation check and over-mint vault shares
Severity : Medium
Confidence : HIGH
Attack Complexity : MEDIUM
PoC Status : Confirmed
Description
The Accountant attempts to limit dangerous rate-provider replacement by probing the new provider in setRateProviderData() and comparing its returned rate against the prior effective rate. For an asset that was previously pegged, the prior effective rate is treated as 1e18, and a new non-pegged provider should be rejected if it differs by more than 5%.
The check assumes that the provider's getRate() result is independent of the caller. That assumption is not enforced. During registration, the call to getRate() is made by AccountantWithRateProviders. Later, when users deposit through TellerWithMultiAssetSupport, the Teller reads the provider address from the Accountant and calls getRate() itself. A malicious or compromised provider can return a safe value when msg.sender is the Accountant, then return an inflated value when msg.sender is the Teller.
This bypasses the 5% replacement cap while still affecting share minting. In the live fork PoC, USDC starts as a pegged asset with no rate provider. The exchangeRateBot role installs a provider that returns 1e18 to the Accountant probe and 2e18 to the Teller. The provider is accepted, and a 1 USDC deposit mints about twice the shares expected at the live 1:1 USDC valuation.
Location
Preconditions
Impact
A privileged exchange-rate role can install a provider that passes the Accountant's replacement check while causing the Teller to overvalue deposits and mint too many vault shares. The confirmed fork result is a 1 USDC deposit minting 1.989180845381967478 vault shares instead of the expected 0.994590422690983739 shares at the live exchange rate, about a 2x over-mint.
The impact scales with the inflated rate returned to the Teller and is bounded in practice by deposit-cap headroom, available vault liquidity, and any downstream redemption or market path for the over-minted shares. Once shares have been minted to the attacker, the effect is not automatically reversible without an explicit burn, compensation, or accounting correction.
Exploit Cost
The capital requirement is the deposit asset amount. The fork PoC uses 1 USDC. Gas cost includes deploying the rate provider, calling setRateProviderData(), approving the vault, and depositing through the Teller. The fork test reports 438,260 gas for the test transaction sequence, excluding the normal cost of deploying the test harness itself.
Steps to Reproduce
Proof-of-Concept Result
PoC reference: poc_caller_sensitive_rate_provider.t.sol
Suggested Fix
Do not let Teller call arbitrary rate providers directly if the safety check was performed from the Accountant's caller context. A practical fix is to centralize asset-rate reads inside the Accountant and make the Teller call an Accountant function such as getAssetRateInBase(asset), so the registration check and the deposit-time pricing call execute through the same contract context.
I would also recommend moving setRateProviderData() out of the hot UPDATE_EXCHANGE_RATE_ROLE path and putting provider replacement behind protocolAdmin or a timelock. If fast operations still need to update ordinary exchange rates, keep that separate from provider installation. For additional defense in depth, consider an allowlist for provider implementations or code hashes, and add regression tests for caller-sensitive providers that return different values to the Accountant and Teller.
Happy to answer questions or adjust the PoC if your internal deployment addresses differ from the public mainnet state I verified. You can reach me through the same channel where this report was submitted.
Best,
Phan Long
All reactions