
@taichiaudit Founder | Core Member @DeFiHackLabs
GMT+8
Joined January 2018
- Tweets3.1K
- Following3.9K
- Followers2.6K
- Likes8.4K
Paid $250 bucks for a mediation and got closed after 25 minutes with this response, stay away from this scam. Do not pay for mediation.
Glad to join the @HackenProof Vanguard program! Will spend more time hunting bugs on HP and share knowledge with the community馃
Passed Daml fundamentals cert exam馃榿Thanks @digitalasset for the great intro course
Translation: you passed verification but we won't allow you to do your regular job, what you gonna do about it kid?
There is a second bug that makes this path reachable much more easily: the insolvency gate is broken.
The comments say liquidation should depend on the borrower鈥檚 shortfall (the underwater amount), but calculateDiscountedRepayToEvenAmount() reads the target account鈥檚 shortfall and then never uses it. You can ctrl+f for accountShortfall_TargetUser and see it is not used anywhere in the contract.
Instead, the close cap (the maximum amount of the borrower鈥檚 debt that this liquidation is allowed to repay in one call) is effectively derived from close factor and borrow balance.
During the write phase it stores the borrower-side result, then stores the liquidator-side result.
Because both storage variables point to the same slot, the second write overwrites the first one. Net result: the slot ends up with a + b, not a neutral self-transfer.
So the liquidation repays real debt but credits extra internal collateral to the attacker.
After that, the code first computes localResults.updatedSupplyBalance_TargetCollateralAsset = localResults.currentSupplyBalance_TargetCollateralAsset - localResults.seizeSupplyAmount_TargetCollateralAsset.
It then computes localResults.updatedSupplyBalance_LiquidatorCollateralAsset = localResults.currentSupplyBalance_LiquidatorCollateralAsset + localResults.seizeSupplyAmount_TargetCollateralAsset.
You can think of this as localResults.updatedSupplyBalance_TargetCollateralAsset = a - b and updatedSupplyBalance_LiquidatorCollateralAsset = a + b.
Then the function converts checkpointed storage balances into accumulated local balances. borrowBalance_TargeUnderwaterAsset becomes localResults.currentBorrowBalance_TargetUnderwaterAsset via calculateBalance(borrowBalance_TargeUnderwaterAsset.principal, borrowBalance_TargeUnderwaterAsset.interestIndex, localResults.newBorrowIndex_UnderwaterAsset).
Likewise, supplyBalance_TargetCollateralAsset becomes localResults.currentSupplyBalance_TargetCollateralAsset, and supplyBalance_LiquidatorCollateralAsset becomes localResults.currentSupplyBalance_LiquidatorCollateralAsset, by calling calculateBalance() with each balance's stored principal, stored interestIndex, and the shared localResults.newSupplyIndex_CollateralAsset.
Here you can see that localResults.currentSupplyBalance_TargetCollateralAsset and localResults.currentSupplyBalance_LiquidatorCollateralAsset will have the same value.
The exploitable case is targetAccount == msg.sender and assetBorrow == assetCollateral.
Under that condition, supplyBalance_TargetCollateralAsset and supplyBalance_LiquidatorCollateralAsset are not two independent positions. They are two storage references to the exact same supplyBalances slot.
So the function is effectively trying to transfer collateral from an account back to itself using two aliased pointers.
High level understanding first:
Alkemi tracks user positions in internal balance records:
- supplyBalances[account][asset] for supplied collateral
- borrowBalances[account][asset] for debt
Each record is just a checkpointed principal plus an interestIndex. When the protocol needs the live amount, it accrues interest into a local variable with calculateBalance(). That function just computes the formula below:














