Reports

Smart Contract Security Assessment

Euler Layer-Credit

LayerCredit is a permissionless fixed-rate, fixed-term lending protocol built on the Euler Vault Kit (EVK). It allows anyone to deploy ungoverned bond vaults with configurable collateral, interest rates, and early repayment penalties.

13
Issues
1
C/H/M
Period
Jan 27, 2026 - Jan 30, 2026
Auditors
HHK, Panda

Review Summary

Protocol Overview

LayerCredit is a permissionless fixed-rate, fixed-term lending protocol built on the Euler Vault Kit (EVK). It allows anyone to deploy ungoverned bond vaults with configurable collateral, interest rates, and early repayment penalties.

Protocol
Euler Finance
Timeline
Jan 27, 2026 - Jan 30, 2026
Audit Team
HHK, Panda

Audit Overview

Scope and Resources

Scope

This audit covers 2 smart contracts totaling approximately 500~ lines of code across 3.25 days of review.

Overall Assessment

Evaluation Matrix

access control
Good

Permissionless with optional per-bond restrictions via EVK hooks. No admin or owner. Minor gaps around flashloan hooks and penalty receiver validation.

mathematics
Average

Sound compound interest via RPow. Liquidation profitability boundary issue where penalty can exceed max discount.

complexity
Good

Clean contract with clear 3-state lifecycle. Leverages EVK rather than reimplementing vault mechanics.

libraries
Excellent

Battle-tested EVK, OpenZeppelin 5.5.0, and EVC.

decentralization
Excellent

No admin, owner, or governance. Permissionless deployment, immutable parameters, autonomous time-based state transitions.

code stability
Excellent

The code remained stable during the review.

documentation
Excellent

Comprehensive whitepaper and NatSpec on public functions.

monitoring
Good

Events plus on-chain history system tracking all operations. DFloat16 limits precision to ~3 significant digits.

testing
Average

Core lifecycle and happy paths covered. Lacks edge case, fuzz, and invariant testing.

Key Findings

Findings Summary

0
Critical
0
High
1
Medium
2
Low
9
Informational
1
Gas
M-1 Finding

M-1: High interest rates with penalty can make liquidations unprofitable, leading to bad debt

Medium

Description:

The liquidation discount is capped at 15% (LayerCredit.sol#L170), but there's no validation that bond parameters create liquidatable positions.

When a liquidator assumes debt during liquidation, they must either:

  1. Repay immediately and pay the early repayment penalty
  2. Hold the debt until maturity and pay accrued interest

Both options become unprofitable when remaining interest exceeds the 15% max discount:

| Interest Rate | Time Remaining | Remaining Interest | Profitable? |
|--------------|----------------|-------------------|-------------|
| 15% APY | 1 year | ~16% | no |
| 20% APY | 1 year | ~22% | no |
| 10% APY | 2 years | ~21% | no |

The whitepaper suggests setting LTVs carefully, but this doesn't solve the issue since:

  • LTV controls when liquidation triggers, not the liquidator's profit margin
  • Remaining interest is based on time until maturity, not position health
  • The 15% discount cap is independent of LTV settings

Impact:

Medium. Bonds with penalty and high interest rates or long durations can have positions that are unprofitable to liquidate, leading to bad debt accumulation and losses for lenders.

Recommendation:

Validate at deployment that maxPossiblePenalty <= maxLiquidationDiscount. Add in deployBond():

// Calculate maximum interest over the full term duration
// multiplier is in 1e27 scale (e.g., 1.22e27 for 22% total interest)
(uint256 multiplier,) = RPow.rpow(uint256(p.interestRate) + 1e27, p.termDuration, 1e27);

// maxInterestFraction: the interest portion as a fraction in 1e27 scale
// e.g., 0.22e27 represents 22% interest
uint256 maxInterestFraction = multiplier - 1e27;

// maxPenaltyFraction: penalty as a fraction of principal in 1e27 scale
// earlyRepayPenalty is in 1e4 scale (1e4 = 100%), so divide by 1e4
uint256 maxPenaltyFraction = maxInterestFraction * p.earlyRepayPenalty / 1e4;

// Max liquidation discount is 15% = 0.15e27 in 1e27 scale
require(maxPenaltyFraction <= 0.15e27, "Penalty exceeds max liquidation discount");

Developer Response:

Fixed in commit 6257ae2 and 4a9f75a.

The penalty model was changed from a percentage of remaining interest (earlyRepayPenalty) to a duration-based model (penaltyDuration), where the penalty equals the interest over that duration. Max liquidation discount was increased to 25%, and a deployment-time check ensures the penalty cannot exceed 20% of principal resulting in a 5% buffer.

L-1 Finding

L-1: `transferFromMax` records assets while other transfers record shares

Low

Description:

In LayerCredit.sol#L628-L631, transferFromMax uses convertToAssets(balanceOf(from)):

function transferFromMax(address from, address to) external {
    (address bond,) = hookInfo();
    _transferInternal(bond, from, to, IEVault(bond).convertToAssets(IEVault(bond).balanceOf(from)));
}

But transfer and transferFrom pass the amount directly (shares). When the exchange rate > 1, the history records inconsistent units for the same action type.

Since transfers are treated as HISTORY_ACTION_WITHDRAW and HISTORY_ACTION_DEPOSIT, it would make more sense to always convert to asset.

Impact:

Low. History data becomes corrupted with mixed units, making off-chain accounting unreliable.

Recommendation:

Modify _transferInternal() to always convert to assets.

Developer Response:

Fixed in commit: 8fac836.

L-2 Finding

L-2: Blacklisted `penaltyReceiver` blocks early repayments

Low

Description:

repayBond() transfers the penalty to penaltyReceiver via safeTransfer at LayerCredit.sol#L313. For blacklist tokens (USDC, USDT), if penaltyReceiver is blacklisted, this transfer reverts.

During Active state with earlyRepayPenalty > 0, the repay hook blocks direct repay() calls with DirectRepayNotAllowed(). This forces borrowers to use repayBond() as the only repayment path. If the penalty transfer fails, borrowers cannot repay until the bond transitions to Settlement/Final.

This also affects liquidators - after assuming debt via liquidation, they cannot early repay either, forcing them to hold the position until term end.

Impact:

Low. Borrowers and liquidators cannot deleverage during Active period if penaltyReceiver is blacklisted. They must wait until term ends for direct repayment to become available. Increases liquidation risk as positions cannot be managed.

Recommendation:

Use a pull pattern - accrue penalties in a mapping and let penaltyReceiver claim later:

mapping(address bond => uint256) public accruedPenalties;

// In repayBond(): instead of safeTransfer
accruedPenalties[bond] += penalty;

// New function
function claimPenalty(address bond) external {
    uint256 amount = accruedPenalties[bond];
    accruedPenalties[bond] = 0;
    IERC20(IEVault(bond).asset()).safeTransfer(bondsByVault[bond].penaltyReceiver, amount);
}

Developer Response:

Partially fixed in commit be963e5. Documented the issue inside the whitepaper.

This is a good point. However, the suggested pull mechanism is not compatible with our Euler Earn contract, which cannot call arbitrary methods on other contracts.

I-1 Finding

I-1: Missing validation for `penaltyReceiver` when penalty is not 0

Informational

Impact:

Informational. A missconfigured penaltyReceiver will result in the impossibility to repay early.

Recommendation:

If the earlyRepayPenalty is not zero, make sure to check the penaltyReceiver is not zero.

Developer Response:

Fixed in commit: 985aa9f7

Description:

No validation prevents setting penaltyReceiver to address(0) when earlyRepayPenalty > 0. This results in reverts when a standard ERC20 tokens is used.

313 token.safeTransfer(bondsByVault[bond].penaltyReceiver, penalty);

LayerCredit.sol#L313-L313

I-2 Finding

I-2: Flashloans not hooked could impact institutional use case

Informational

Description:

Flashloans bypass all Layer Credit restrictions because OP_FLASHLOAN is not included in the hook configuration at LayerCredit.sol#L165-L169.

The contract allows restricting borrowing to a specific address via the borrower parameter, but this restriction does not extend to flashloans since OP_FLASHLOAN is not hooked. Institutional use cases may require disabling flashloans entirely or limiting them to the whitelisted borrower.

Impact:

Informational. Bonds with restricted borrowers cannot prevent arbitrary addresses from executing flashloans, undermining access control for institutional use cases.

Recommendation:

Add OP_FLASHLOAN to hooks and introduce a blockFlashLoans parameter in DeployBondParams to allow bond creators to disable or restrict flashloans.

Developer Response:

Acknowledged. I'd like to keep the configuration-surface as small as possible, so that bonds are as uniform and predictable as possible.

For now we'll just acknowledge this. If there is a solid use-case that crops up we can revisit this for future versions of the contract.

I-3 Finding

I-3: Early repay penalty bypassable via rounding on low decimal tokens

Informational

Description:

The penalty calculation uses integer division that can round to zero for small repayment amounts:

uint256 interestRemaining = (multiplier - 1e27) * amount / 1e27;
return (amount, interestRemaining * b.earlyRepayPenalty / 1e4);

For penalty to be zero: interestRemaining * earlyRepayPenalty < 1e4.

With low-decimal, high-value tokens like WBTC (8 decimals), borrowers can repay in small chunks that each round to zero penalty.

Impact:

Informational. Exploitable on very low fees L2s.

Recommendation:

Round up the division.

Developer Response:

Fixed in commit: 213aec

I-4 Finding

I-4: `blockIdleDeposits` can be bypassed when `earlyRepayPenalty` is zero

Informational

Description:

When blockIdleDeposits = true, the contract validates that deposits are matched by borrowing within the same batch (LayerCredit.sol#L681-L682). However, this check only runs at deposit time - a borrower can later repay and leave lender funds idle.

If earlyRepayPenalty = 0, borrowers can repay instantly with no cost, defeating the purpose of blockIdleDeposits:

Impact:

Informational. Configuration inconsistency where blockIdleDeposits can be circumvented, leaving lenders with idle funds despite the protection being enabled.

Recommendation:

Require earlyRepayPenalty > 0 when blockIdleDeposits is enabled:

if (p.blockIdleDeposits) {
    require(p.earlyRepayPenalty > 0, "Early repay penalty required when blocking idle deposits");
}

Developer Response:

Partially fixed in commit 7b02cd3. Documented the edge case inside the whitepaper.

I-5 Finding

I-5: Zero penalty transfer in `repayBond()`

Informational

Description:

In repayBond(), the penalty is transferred unconditionally at LayerCredit.sol#L313. When penalty == 0, this wastes gas and may revert for tokens that don't allow zero-value transfers.

Impact:

Informational/Gas. Gas inefficiency and potential incompatibility with some ERC20 tokens.

Recommendation:

Skip the transfer when penalty is zero: if (penalty > 0) token.safeTransfer(...)

Developer Response:

Fixed in commit: d2949b.

I-6 Finding

I-6: Metadata check uses `<` instead of `<=`

Informational

Description:

The metadata validation at LayerCredit.sol#L449 uses < type(uint80).max, unnecessarily rejecting the max uint80 value.

Impact:

Informational. Off-by-one that prevents using the full 80-bit range.

Recommendation:

Change to <= type(uint80).max.

Developer Response:

Fixed in commit: 470afc3.

I-7 Finding

I-7: `DFloat16` precision loss significant for high-value low-decimal tokens

Informational

Description:

History uses DFloat16 (3 significant digits, ~0.1-1% error). For high-value low-decimal tokens, this means meaningful discrepancies:

  • 1.234567 WBTC → 1.23 WBTC (~$400 error)
  • 1,234,567 USDC → 1,230,000 USDC (~$4,567 error)

The metadata field has 80 bits but uses at most 32. DFloat40 would provide ~10 significant digits while still fitting two values for REPAY. It would also use less divisions, saving gas at the same time.

Impact:

Informational. Off-chain accounting may see discrepancies for large transactions and history will have unused storage space.

Recommendation:

Consider DFloat40 for improved precision and gas savings.

Developer Response:

Acknowledged. This is a good suggestion. When I started development I wasn't sure how much space I would have in each history entry. In hindsight it could've been greater than 16-bits.

However, as stated in the whitepaper, the specific amounts should not be relied on for any precise accounting. For now we'll just acknowledge this.

I-8 Finding

I-8: Interest rate not validated against EVK's maximum

Informational

Description:

LayerCredit stores interestRate as uint80 (max ~1.2e24), but EVK caps rates at MAX_ALLOWED_INTEREST_RATE (2.9e20, ~1,000,000% APY). If a bond is deployed with a rate exceeding this cap:

  1. EVK internally caps at MAX_ALLOWED_INTEREST_RATE for interest accrual
  2. But getRepayPenalty() uses the uncapped stored value in RPow.rpow()
  3. The overflow flag from RPow.rpow() is ignored at LayerCredit.sol#L286

Additionally, during SETTLEMENT state the rate is tripled at LayerCredit.sol#L391, so the base rate must be at most MAX_ALLOWED_INTEREST_RATE / 3 to stay within bounds.

Impact:

Informational. A misconfiguration (no legitimate bond would have >333,333% APY), but could cause undefined behavior in penalty calculations or during settlement.

Recommendation:

Validate in deployBond():

error InterestRateTooHigh();

require(p.interestRate < MAX_ALLOWED_INTEREST_RATE / 3, InterestRateTooHigh());

Developer Response:

Fixed in commit: 90d91c5.

I-9 Finding

I-9: History ordering affected by reentrancy during penalty transfer

Informational

Description:

repayBond() writes history after the penalty transfer at LayerCredit.sol#L315. For ERC777-style tokens with receiver callbacks, the penaltyReceiver can reenter during the transfer and trigger share transfers, which write their history entries before the repay entry.

Impact:

Informational. History ordering may not reflect logical operation order when using callback-enabled tokens. Only affects audit trail integrity.

Recommendation:

Move history write before the penalty transfer (after L311, before L313).

Developer Response:

Fixed in commit: 5ed236c.

G-1 Finding

G-1: Optional on-chain history

Gas

Summary:

The contract maintains comprehensive on-chain history for all actions (deposits, withdrawals, borrows, repays, liquidations, etc.). This is valuable for protocols that prefer not to rely on off-chain indexers but this comes with a gas cost and a loss of precision.

Description:

Make history recording optional via a simple flag, allowing deployers to choose between:

  • Full history mode: Current behavior, complete on-chain audit trail
  • Events-only mode: Rely on existing events + off-chain indexing

Chain indexing can be done with thegraph and is compatible with all the chain except Swellchain, Plasma and Bob.

Gas Comparison (LayerCredit Contract):

| Function | History Enabled | History Disabled | Savings | % |
| ---------- | --------------- | ---------------- | ------- | ------- |
| repayBond | 258,192 | 205,870 | 52,322 | 20% |
| transition | 223,575 | 184,118 | 39,457 | 18% |

Gas Comparison (EVK Borrowing Operations):

| Function | History Enabled | History Disabled | Savings | % |
| -------- | --------------- | ---------------- | ------- | ------- |
| borrow | 224,905 | 126,329 | 98,576 | 44% |
| pullDebt | 158,797 | 71,596 | 87,201 | 55% |
| repay | 93,358 | 67,765 | 25,593 | 27% |

Impact:

Gas savings.

Recommendation:

On all chains where thegraph can be used deploy without history enabled while when it's not history can be enabled.

// Add to state variables
bool public immutable historyEnabled;

constructor(address evc, address eVaultFactory_, address routerFactory_, bool historyEnabled_) EVCUtil(evc) {
    ...
    historyEnabled = historyEnabled_;
}

function _addToHistory(uint8 action, address bond, address who, uint256 metadata, address extra) internal {
    if (!historyEnabled) return;  // Early exit if disabled
    
    require(metadata < type(uint80).max, MetadataTooBig());
    // ... rest of existing implementation
}

Developer Response:

Acknowledged. Since the history is used for several things such as showing the current set of borrowers on a market, and discovering which borrowers might be eligible for liquidation, it is much more useful if it is a comprehensive tracking of all actions.

Thank you for doing the benchmarks. I believe there is some amortisation that isn't visible in these benchmarks. For instance, adding an index entry for an account that has never interacted with the system before is more expensive than a repeat user.

Lately gas has been less of a concern on mainnet (and is hardly a concern at all on other networks), so I'm inclined to say the benefits exceed the costs. We'll just acknowledge this for now.

Final Remarks

LayerCredit is a well-architected protocol that leverages the Euler Vault Kit effectively for fixed-rate, fixed-term lending. The permissionless, ungoverned design is clean — governance is renounced and bond parameters are immutable. The Euler team was responsive and quick to address findings during the review. The primary concern was the interaction between the early repayment penalty and the hardcoded max liquidation discount, which could have made liquidations unprofitable for certain configurations. The on-chain history system is an interesting design choice but introduces some gas overhead and diverges from the rest of Euler's ecosystem which relies on indexers and off-chain infrastructure. Bond isolation is strong with no cross-bond contamination vectors identified. Most findings relate to edge cases in penalty mechanics and parameter validation rather than fundamental design flaws.

Methodology

Severity Classification

Critical

Immediate threat to user funds or protocol integrity

Direct loss of funds, protocol compromise

High

Significant security risk requiring urgent attention

Potential fund loss, major functionality disruption

Medium

Important issue that should be addressed

Limited fund risk, functionality concerns

Low

Minor issue with minimal impact

Best practice violations, minor inefficiencies

Gas

Findings that improve gas efficiency

Increased transaction costs

Informational

Code quality and best practice recommendations

Reduced maintainability and readability