Algo Ssstablecoinsss

AI First Flight #2
Beginner FriendlyDeFi
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Constructor accepts the same collateral token twice, and `_get_account_collateral_value` then double-counts every deposit — 100% backing passes as a 200% health factor

Summary

dsc_engine.__init__ registers whatever two token/feed pairs it is given into the fixed COLLATERAL_TOKENS array without checking that the two tokens are different. _get_account_collateral_value computes a user's collateral value by looping over that array and summing _get_usd_value(token, deposited[token]) per array entry — so if the same token is registered twice, every deposit is valued twice. Health factors, minting capacity and liquidation thresholds are all computed from the doubled figure.

Vulnerability Details

In src/dsc_engine.vy:

@deploy
def __init__(token_addresses: address[2], price_feed_addresses: address[2], dsc_address: address):
DSC = i_decentralized_stable_coin(dsc_address)
COLLATERAL_TOKENS = token_addresses # no duplicate / sanity check
self.token_address_to_price_feed[token_addresses[0]] = price_feed_addresses[0]
self.token_address_to_price_feed[token_addresses[1]] = price_feed_addresses[1]
​
@internal
@view
def _get_account_collateral_value(user: address) -> uint256:
total_collateral_value_in_usd: uint256 = 0
for token: address in COLLATERAL_TOKENS: # same token twice -> counted twice
amount: uint256 = self.user_to_token_address_to_amount_deposited[user][token]
total_collateral_value_in_usd += self._get_usd_value(token, amount)
return total_collateral_value_in_usd

The project's own solvency invariant (in the fuzz suite) cannot catch this: it values the engine's holdings with the same double-counting get_usd_value path.

Impact

With WETH registered twice (a one-argument deployment mistake, with no guard to catch it), a user who deposits 10 WETH ($20,000) is credited with $40,000 of collateral value and can mint 20,000 DSC at a displayed health factor of exactly 1.0, while the real backing is $20,000 for $20,000 of debt — 100% collateralized in a system whose safety model requires 200%. The position does not become liquidatable until the real price falls by more than half (at $1,500 the engine still reads HF 0.75, by which point the real shortfall is already $5,000 per such position). Every duplicate-registered deployment silently halves the protocol's collateralization requirement.

Proof of Concept

Boa/pytest test (passes locally):

def test_poc_duplicate_collateral_token_double_counts(dsc, weth, eth_usd, some_user):
dup_engine = dsc_engine.deploy(
[weth.address, weth.address], [eth_usd.address, eth_usd.address], dsc
)
dsc.set_minter(dup_engine.address, True)
dsc.transfer_ownership(dup_engine.address)
​
with boa.env.prank(some_user):
weth.approve(dup_engine, to_wei(10, "ether"))
dup_engine.deposit_collateral(weth, to_wei(10, "ether"))
_, counted_value = dup_engine.get_account_information(some_user)
assert counted_value == to_wei(40_000, "ether") # $20,000 deposit counted as $40,000
dup_engine.mint_dsc(to_wei(20_000, "ether"))
assert dup_engine.health_factor(some_user) == to_wei(1, "ether") # reads "safe" at 100% backing

Recommended Mitigation

Reject duplicate registrations in the constructor (assert token_addresses[0] != token_addresses[1]), and more generally assert each token is non-empty and not already registered before writing its price feed. A uniqueness check is the only line of defense, because the valuation loop trusts the array blindly.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 7 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!