CS 765: Introduction to Blockchains, Cryptocurrencies and Smart Contracts
Building your own DeFi Money Market
24m2126 Siddhi Pevekar
Instructor: Vinay Ribeiro
22b0969 Tanmay Gejapati
1 Tracking and Analysis
1.1 VL Token Price Over Time
The current randomization configure is too harsh for the market to be stable hence the VLT’s
price plummets to a fraction of what it started with, $0.0069 is the final price (Starting at
$100)
1
1.2 Liquidity & Demand
Total Value Locked (TVL): The total USD value of all collateral deposited in the proto-
col.
Interpretation: TVL represents the supply side: how much capital is available. Increases
with deposits and fluctuates with VL token price changes. A healthy protocol maintains
stable or growing TVL. While our simulation (for this specific configuration) crashes dra-
matically.
1.3 Risk & Solvency
2
Total Outstanding Borrowings: The total USD value of all active loans (debt) in the
protocol.
Interpretation: Represents the demand side: how much capital is being borrowed. Should
always be less than TVL due to over-collateralization requirements. High utilization indicates
strong demand but also higher risk. Our simulation depicted a modest average overall.
Average Health Factor: The average safety margin of all borrowers with active loans.
HF > 1 means safe, HF < 1 means liquidatable.
Interpretation: Indicates overall protocol health. Sharp drops during market crashes show
systemic risk. The red dashed line at 1.0 marks the liquidation threshold: positions below
this line can be liquidated.
3
1.4 Recovery Activity
Cumulative Liquidations: The running total USD value of debt repaid through liquida-
tions over time.
Interpretation: Tracks the protocol’s recovery mechanism effectiveness. Steep increases
indicate periods of market stress where many positions became undercollateralized. The
filled area emphasizes the cumulative nature. This value only grows, never decreases. In our
simulation this has more growth in the latter half of the simulation.
Under-collateralized Debt: The total USD value of loans where Health Factor < 1 at
any given moment.
4
Interpretation: Represents "bad debt" or positions at risk. Spikes during market crashes
when collateral values drop. Should quickly return to zero as liquidators clear these positions.
Persistent high values indicate liquidation mechanism failures or extreme market conditions.
This is clearly complimented with the latter half of the cumulative liquidation plot, hence
the decline in this plot.
2 Security Frameworks
2.1 OpenZeppelin Security Modules
• ReentrancyGuard: All state-modifying external functions use the nonReentrant mod-
ifier to prevent reentrancy attacks
• SafeERC20: All token transfers use safeTransfer and safeTransferFrom to handle
non-standard ERC20 implementations
• Ownable: Access control for administrative functions
3 Constructor Validation
3.1 Address Zero Checks
c o n s t r u c t o r ( a d d r e s s _vl , a d d r e s s _sb , a d d r e s s _ o r a c l e ) {
r e q u i r e ( _vl != a d d r e s s ( 0 ) && _sb != a d d r e s s ( 0 )
&& _ o r a c l e != a d d r e s s ( 0 ) , " I n v a l i d a d d r e s s e s " ) ;
}
Purpose: Prevents deployment with invalid contract addresses, ensuring all critical
dependencies are properly initialized.
4 Risk Parameter Validation
4.1 setRiskParams() Sanity Checks
The setRiskParams function enforces multiple constraints:
f u n c t i o n setRiskParams (
uint256 _collateralFactorBps ,
uint256 _liquidationThresholdBps ,
u i n t 2 5 6 _liquidationBonusBps ,
uint256 _closeFactorBps
) e x t e r n a l onlyOwner {
r e q u i r e ( _ c o l l a t e r a l F a c t o r B p s <= PCT_SCALE, "CF > 1 0 0 % " ) ;
r e q u i r e ( _ l i q u i d a t i o n T h r e s h o l d B p s <= PCT_SCALE, "LT > 1 0 0 % " ) ;
r e q u i r e ( _ l i q u i d a t i o n B o n u s B p s <= 2 0 0 0 , "LB > 2 0 % " ) ;
5
r e q u i r e ( _ c l o s e F a c t o r B p s <= PCT_SCALE, " C l o s e > 1 0 0 % " ) ;
r e q u i r e ( _ c o l l a t e r a l F a c t o r B p s <= _ l i q u i d a t i o n T h r e s h o l d B p s ,
"CF <= LT r e q u i r e d " ) ;
}
Checks Implemented:
1. Collateral Factor ≤ 100%: CFbps ≤ 10000
2. Liquidation Threshold ≤ 100%: LTbps ≤ 10000
3. Liquidation Bonus ≤ 20%: LBbps ≤ 2000 (sanity cap)
4. Close Factor ≤ 100%: closeF actorbps ≤ 10000
5. Economic Invariant: CF ≤ LT (ensures liquidation threshold exceeds borrowing
capacity)
5 Price Oracle Validation
5.1 _getPrice() Checks
f u n c t i o n _ g e t P r i c e ( a d d r e s s token ) i n t e r n a l view r e t u r n s ( u i n t 2 5 6 ) {
u i n t 2 5 6 p r i c e = o r a c l e . g e t A s s e t P r i c e ( token ) ;
require ( price > 0 , " Invalid price : zero " ) ;
r e q u i r e ( p r i c e <= type ( u i n t 1 2 8 ) . max ,
" Invalid price : overflow risk " ) ;
return price ;
}
Checks Implemented:
1. Non-Zero Price: price > 0 prevents division by zero and invalid pricing
2. Overflow Prevention: price ≤ 2128 − 1 prevents multiplication overflow in down-
stream calculations
Mathematical Justification: Limiting price to uint128 ensures that price × amount
fits in uint256 for reasonable token amounts.
6 Token Conversion Functions
6.1 _tokenToUsdScaled() Safe Arithmetic
6
f u n c t i o n _tokenToUsdScaled ( IERC20 token , u i n t 2 5 6 amount )
i n t e r n a l view r e t u r n s ( u i n t 2 5 6 ) {
i f ( amount == 0 ) r e t u r n 0 ;
u i n t 2 5 6 p r i c e = _ g e t P r i c e ( a d d r e s s ( token ) ) ;
u i n t 8 dec = _getDecimals ( a d d r e s s ( token ) ) ;
r e q u i r e ( dec <= 7 7 , " Decimals t oo l a r g e " ) ;
u i n t 2 5 6 d i v i s o r = 10 ∗∗ dec ;
require ( divisor > 0 , " Invalid divisor " ) ;
r e q u i r e ( amount <= type ( u i n t 2 5 6 ) . max / p r i c e ,
" Overflow i n p r i c e c a l c u l a t i o n " ) ;
r e t u r n ( amount ∗ p r i c e ) / d i v i s o r ;
}
Checks Implemented:
1. Zero Amount Early Return: Optimizes gas for zero inputs
2. Decimals Bound: decimals ≤ 77 ensures 10decimals < 2256
3. Non-Zero Divisor: Prevents division by zero
2256 −1
4. Multiplication Overflow Check: amount ≤ price
prevents overflow in amount ×
price
Formula:
amount × price
U SDscaled =
10decimals
6.2 _usdScaledToToken() Safe Arithmetic
f u n c t i o n _usdScaledToToken ( IERC20 token , u i n t 2 5 6 u s d S c a l e d )
i n t e r n a l view r e t u r n s ( u i n t 2 5 6 ) {
i f ( u s d S c a l e d == 0 ) r e t u r n 0 ;
u i n t 2 5 6 p r i c e = _ g e t P r i c e ( a d d r e s s ( token ) ) ;
require ( price > 0 , " Invalid price for conversion " ) ;
u i n t 8 dec = _getDecimals ( a d d r e s s ( token ) ) ;
r e q u i r e ( dec <= 7 7 , " Decimals t oo l a r g e " ) ;
u i n t 2 5 6 m u l t i p l i e r = 10 ∗∗ dec ;
r e q u i r e ( u s d S c a l e d <= type ( u i n t 2 5 6 ) . max / m u l t i p l i e r ,
" Overflow i n token c o n v e r s i o n " ) ;
return ( usdScaled ∗ m u l t i p l i e r ) / p r i c e ;
}
Checks Implemented:
7
1. Zero USD Early Return: Gas optimization
2. Non-Zero Price: Prevents division by zero
3. Decimals Bound: Same as above
2256 −1
4. Multiplication Overflow Check: usdScaled ≤ 10decimals
Formula:
U SDscaled × 10decimals
tokens =
price
7 Health Factor Computation
7.1 computeHealthFactorScaled() Safe Arithmetic
fu nct ion computeHealthFactorScaled ( address user )
p u b l i c view r e t u r n s ( u i n t 2 5 6 ) {
u i n t 2 5 6 debtUsd = getUserDebtUsdScaled ( u s e r ) ;
i f ( debtUsd == 0 ) {
r e t u r n type ( u i n t 2 5 6 ) . max ; // i n f i n i t e h e a l t h
}
uint256 c o l l a t e r a l U s d = getUserCollateralUsdScaled ( user ) ;
r e q u i r e ( c o l l a t e r a l U s d <= type ( u i n t 2 5 6 ) . max
/ liquidationThresholdBps ,
" Overflow i n HF numerator " ) ;
u i n t 2 5 6 numerator = c o l l a t e r a l U s d ∗ l i q u i d a t i o n T h r e s h o l d B p s ;
r e q u i r e ( debtUsd <= type ( u i n t 2 5 6 ) . max / PCT_SCALE,
" Overflow i n HF denominator " ) ;
u i n t 2 5 6 denominator = debtUsd ∗ PCT_SCALE;
r e q u i r e ( denominator > 0 , " I n v a l i d denominator i n HF " ) ;
r e q u i r e ( numerator <= type ( u i n t 2 5 6 ) . max / PRICE_SCALE,
" Overflow i n HF s c a l i n g " ) ;
r e t u r n ( numerator ∗ PRICE_SCALE) / denominator ;
}
Checks Implemented:
1. Zero Debt Special Case: Returns 2256 − 1 (infinite health) when no debt exists
2256 −1
2. Numerator Overflow Check: collateralU sd ≤ LTbps
2256 −1
3. Denominator Overflow Check: debtU sd ≤ P CT _SCALE
8
4. Non-Zero Denominator: Prevents division by zero
2256 −1
5. Scaling Overflow Check: numerator ≤ P RICE_SCALE
Formula:
collateralU SD × LTbps × P RICE_SCALE
HF =
debtU SD × P CT _SCALE
where HF ≥ P RICE_SCALE (i.e., ≥ 1.0) indicates healthy position.
8 Deposit Function
8.1 deposit() Sanity Checks
f u n c t i o n d e p o s i t ( u i n t 2 5 6 amountVL ) e x t e r n a l nonReentrant {
r e q u i r e ( amountVL > 0 , " d e p o s i t : amount <= 0 " ) ;
r e q u i r e ( msg . s e n d e r != a d d r e s s ( 0 ) , " d e p o s i t : z e r o a d d r e s s " ) ;
r e q u i r e ( suppliedVL [ msg . s e n d e r ] <= type ( u i n t 2 5 6 ) . max
− amountVL , " d e p o s i t : b a l a n c e o v e r f l o w " ) ;
VL . s a f e T r a n s f e r F r o m ( msg . sender , a d d r e s s ( t h i s ) , amountVL ) ;
suppliedVL [ msg . s e n d e r ] += amountVL ;
emit D e p o s i t ( msg . sender , amountVL ) ;
}
Checks Implemented:
1. Non-Zero Amount: amountV L > 0
2. Non-Zero Sender: [Link] ̸= 0x0
3. Balance Overflow Check: currentBalance + amountV L ≤ 2256 − 1
4. Safe Transfer: Uses OpenZeppelin’s safeTransferFrom
9 Withdraw Function
9.1 withdraw() Sanity Checks
f u n c t i o n withdraw ( u i n t 2 5 6 amountVL ) e x t e r n a l nonReentrant {
r e q u i r e ( amountVL > 0 , " withdraw : amount <= 0 " ) ;
r e q u i r e ( msg . s e n d e r != a d d r e s s ( 0 ) , " withdraw : z e r o a d d r e s s " ) ;
u i n t 2 5 6 u s e r B a l = suppliedVL [ msg . s e n d e r ] ;
r e q u i r e ( amountVL <= u s e r B a l ,
" withdraw : i n s u f f i c i e n t c o l l a t e r a l " ) ;
u i n t 2 5 6 n e w C o l l a t e r a l = u s e r B a l − amountVL ;
9
u i n t 2 5 6 n e w C o l l a t e r a l U s d = _tokenToUsdScaled (VL, n e w C o l l a t e r a l ) ;
u i n t 2 5 6 debtUsd = getUserDebtUsdScaled ( msg . s e n d e r ) ;
i f ( debtUsd != 0 ) {
r e q u i r e ( n e w C o l l a t e r a l U s d <= type ( u i n t 2 5 6 ) . max
/ liquidationThresholdBps ,
" withdraw : o v e r f l o w i n HF c a l c " ) ;
u i n t 2 5 6 numerator = n e w C o l l a t e r a l U s d
∗ liquidationThresholdBps ;
r e q u i r e ( debtUsd <= type ( u i n t 2 5 6 ) . max / PCT_SCALE,
" withdraw : o v e r f l o w i n denominator " ) ;
u i n t 2 5 6 denominator = debtUsd ∗ PCT_SCALE;
r e q u i r e ( denominator > 0 , " withdraw : i n v a l i d denominator " ) ;
r e q u i r e ( numerator <= type ( u i n t 2 5 6 ) . max / PRICE_SCALE,
" withdraw : o v e r f l o w i n s c a l i n g " ) ;
u i n t 2 5 6 h f S c a l e d = ( numerator ∗ PRICE_SCALE) / denominator ;
r e q u i r e ( h f S c a l e d >= PRICE_SCALE,
" withdraw : would u n d e r c o l l a t e r a l i z e " ) ;
}
suppliedVL [ msg . s e n d e r ] = n e w C o l l a t e r a l ;
VL . s a f e T r a n s f e r ( msg . sender , amountVL ) ;
emit Withdraw ( msg . sender , amountVL ) ;
}
Checks Implemented:
1. Non-Zero Amount: amountV L > 0
2. Non-Zero Sender: [Link] ̸= 0x0
3. Sufficient Balance: amountV L ≤ userBalance
4. Health Factor Preservation: If debt exists, ensures HFnew ≥ 1.0 after withdrawal
5. Overflow Checks in HF Calculation: Same as computeHealthFactorScaled()
10 Borrow Function
10.1 borrow() Sanity Checks
f u n c t i o n borrow ( u i n t 2 5 6 amountSB ) e x t e r n a l nonReentrant {
r e q u i r e ( amountSB > 0 , " borrow : amount 0 " ) ;
r e q u i r e ( msg . s e n d e r != a d d r e s s ( 0 ) , " borrow : z e r o a d d r e s s " ) ;
10
u i n t 2 5 6 c o l l a t e r a l U s d = g e t U s e r C o l l a t e r a l U s d S c a l e d ( msg . s e n d e r ) ;
r e q u i r e ( c o l l a t e r a l U s d <= type ( u i n t 2 5 6 ) . max
/ collateralFactorBps ,
" borrow : o v e r f l o w i n power c a l c " ) ;
u i n t 2 5 6 borrowingPowerUsd = ( c o l l a t e r a l U s d
∗ c o l l a t e r a l F a c t o r B p s ) / PCT_SCALE;
u i n t 2 5 6 currentDebtUsd = getUserDebtUsdScaled ( msg . s e n d e r ) ;
u i n t 2 5 6 amountSBUsdScaled = _tokenToUsdScaled (SB , amountSB ) ;
r e q u i r e ( currentDebtUsd <= type ( u i n t 2 5 6 ) . max
− amountSBUsdScaled , " borrow : debt o v e r f l o w " ) ;
r e q u i r e ( currentDebtUsd + amountSBUsdScaled
<= borrowingPowerUsd ,
" borrow : e x c e e d s borrowing power " ) ;
r e q u i r e ( borrowedSB [ msg . s e n d e r ] <= type ( u i n t 2 5 6 ) . max
− amountSB , " borrow : b a l a n c e o v e r f l o w " ) ;
borrowedSB [ msg . s e n d e r ] += amountSB ;
SB . s a f e T r a n s f e r ( msg . sender , amountSB ) ;
emit Borrow ( msg . sender , amountSB ) ;
}
Checks Implemented:
1. Non-Zero Amount: amountSB > 0
2. Non-Zero Sender: [Link] ̸= 0x0
2256 −1
3. Borrowing Power Overflow Check: collateralU sd ≤ CFbps
4. Debt Addition Overflow Check: currentDebt + newDebt ≤ 2256 − 1
5. Borrowing Power Constraint: totalDebt ≤ borrowingP ower
6. Balance Overflow Check: borrowedBalance + amountSB ≤ 2256 − 1
Formula:
collateralU SD × CFbps
borrowingP ower =
P CT _SCALE
11 Repay Function
11.1 repay() Sanity Checks
11
f u n c t i o n repay ( u i n t 2 5 6 amountSB ) e x t e r n a l nonReentrant {
r e q u i r e ( amountSB > 0 , " repay : amount 0 " ) ;
r e q u i r e ( msg . s e n d e r != a d d r e s s ( 0 ) , " repay : z e r o a d d r e s s " ) ;
u i n t 2 5 6 debt = borrowedSB [ msg . s e n d e r ] ;
r e q u i r e ( debt > 0 , " repay : no debt " ) ;
SB . s a f e T r a n s f e r F r o m ( msg . sender , a d d r e s s ( t h i s ) , amountSB ) ;
i f ( amountSB >= debt ) {
borrowedSB [ msg . s e n d e r ] = 0 ;
} else {
borrowedSB [ msg . s e n d e r ] = debt − amountSB ;
}
emit Repay ( msg . sender , amountSB ) ;
}
Checks Implemented:
1. Non-Zero Amount: amountSB > 0
2. Non-Zero Sender: [Link] ̸= 0x0
3. Debt Exists: debt > 0
4. Overpayment Handling: Caps repayment at actual debt
5. Safe Transfer: Uses OpenZeppelin’s safeTransferFrom
12 Liquidation Function
12.1 liquidate() Sanity Checks
f u n c t i o n l i q u i d a t e ( a d d r e s s borrower , u i n t 2 5 6 repayAmountSB )
e x t e r n a l nonReentrant {
r e q u i r e ( borrower != a d d r e s s ( 0 ) , " l i q u i d a t e : z e r o borrower " ) ;
u i n t 2 5 6 h f S c a l e d = c o m p u t e H e a l t h F a c t o r S c a l e d ( borrower ) ;
r e q u i r e ( h f S c a l e d < PRICE_SCALE, " l i q u i d a t e : borrower h e a l t h y " ) ;
u i n t 2 5 6 borrowerDebt = borrowedSB [ borrower ] ;
r e q u i r e ( borrowerDebt > 0 , " l i q u i d a t e : no debt " ) ;
r e q u i r e ( borrowerDebt <= type ( u i n t 2 5 6 ) . max / c l o s e F a c t o r B p s ,
" l i q u i d a t e : o v e r f l o w i n max repay " ) ;
12
u i n t 2 5 6 maxRepay = ( borrowerDebt ∗ c l o s e F a c t o r B p s ) / PCT_SCALE;
u i n t 2 5 6 actualRepay = repayAmountSB ;
i f ( actualRepay > maxRepay ) actualRepay = maxRepay ;
i f ( actualRepay > borrowerDebt ) actualRepay = borrowerDebt ;
r e q u i r e ( actualRepay > 0 , " l i q u i d a t e : repay 0 " ) ;
r e q u i r e ( actualRepay <= borrowerDebt ,
" l i q u i d a t e : repay e x c e e d s debt " ) ;
SB . s a f e T r a n s f e r F r o m ( msg . sender , a d d r e s s ( t h i s ) , actualRepay ) ;
borrowedSB [ borrower ] = borrowerDebt − actualRepay ;
u i n t 2 5 6 repayUsdScaled = _tokenToUsdScaled (SB , actualRepay ) ;
u i n t 2 5 6 b o n u s M u l t i p l i e r = PCT_SCALE + l i q u i d a t i o n B o n u s B p s ;
r e q u i r e ( repayUsdScaled <= type ( u i n t 2 5 6 ) . max
/ bonusMultiplier ,
" l i q u i d a t e : overflow in s e i z e calc " ) ;
u i n t 2 5 6 s e i z e U s d S c a l e d = ( repayUsdScaled ∗ b o n u s M u l t i p l i e r )
/ PCT_SCALE;
u i n t 2 5 6 s e i z e V l T o k e n s = _usdScaledToToken (VL, s e i z e U s d S c a l e d ) ;
u i n t 2 5 6 b o r r o w e r C o l l a t e r a l = suppliedVL [ borrower ] ;
i f ( seizeVlTokens > borrowerCollateral ) {
seizeVlTokens = borrowerCollateral ;
}
suppliedVL [ borrower ] = b o r r o w e r C o l l a t e r a l − s e i z e V l T o k e n s ;
VL . s a f e T r a n s f e r ( msg . sender , s e i z e V l T o k e n s ) ;
emit L i q u i d a t i o n ( msg . sender , borrower , actualRepay ,
seizeVlTokens ) ;
}
Checks Implemented:
1. Non-Zero Borrower: borrower ̸= 0x0
2. Unhealthy Position: HF < 1.0 (liquidation condition)
3. Debt Exists: borrowerDebt > 0
2256 −1
4. Close Factor Overflow Check: borrowerDebt ≤ closeF actorbps
borrowerDebt×closeF actor
5. Close Factor Cap: actualRepay ≤ P CT _SCALE
6. Non-Zero Repayment: actualRepay > 0
7. Repayment Bound: actualRepay ≤ borrowerDebt
13
2256 −1
8. Seize Calculation Overflow Check: repayU sd ≤ P CT _SCALE+LBbps
9. Collateral Cap: seizeT okens ≤ borrowerCollateral (prevents over-seizure)
Formulas:
borrowerDebt × closeF actorbps
maxRepay =
P CT _SCALE
repayU SD × (P CT _SCALE + LBbps )
seizeV alue =
P CT _SCALE
13 Market Event Recording
13.1 recordMarketCrash() and recordMarketGain()
f u n c t i o n recordMarketCrash ( u i n t 2 5 6 o l d P r i c e , u i n t 2 5 6 newPrice ,
u i n t 2 5 6 c r a s h P e r c e n t B p s ) e x t e r n a l onlyOwner {
r e q u i r e ( newPrice < o l d P r i c e ,
" recordMarketCrash : p r i c e must d e c r e a s e " ) ;
r e q u i r e ( c r a s h P e r c e n t B p s > 0 && c r a s h P e r c e n t B p s <= 1 00 0 0 ,
" Invalid crash percent " ) ;
emit MarketCrash ( o l d P r i c e , newPrice , c r a s h P e r c e n t B p s ) ;
}
f u n c t i o n recordMarketGain ( u i n t 2 5 6 o l d P r i c e , u i n t 2 5 6 newPrice ,
u i n t 2 5 6 g a i n P e r c e n t B p s ) e x t e r n a l onlyOwner {
r e q u i r e ( newPrice > o l d P r i c e ,
" recordMarketGain : p r i c e must i n c r e a s e " ) ;
r e q u i r e ( g a i n P e r c e n t B p s > 0 && g a i n P e r c e n t B p s <= 1 0 00 0 ,
" Invalid gain percent " ) ;
emit MarketGain ( o l d P r i c e , newPrice , g a i n P e r c e n t B p s ) ;
}
Checks Implemented:
1. Price Direction Validation: Ensures price moved in expected direction
2. Percentage Bounds: 0 < percentbps ≤ 10000 (0-100%)
14 Summary of Safety Mechanisms
15 Conclusion
The MoneyMarket contract implements a comprehensive defense-in-depth strategy with 63+
explicit safety checks beyond Solidity 0.8.20’s built-in overflow protection. Key safety prin-
ciples include:
14
Safety Mechanism Count
Overflow checks in multiplications 15
Division by zero prevention 8
Zero address validation 6
Non-zero amount validation 6
Balance sufficiency checks 4
Health factor validation 3
Price validation 3
Parameter bound checks 6
ReentrancyGuard usage 6
SafeERC20 usage 6
Total Safety Checks 63
Table 1: Summary of safety mechanisms implemented
• Explicit overflow prevention before all multiplications
• Division by zero guards in all division operations
• Input validation for all external parameters
• State consistency checks before state modifications
• Economic invariant enforcement (e.g., CF ≤ LT , HF ≥ 1 for withdrawals)
• Reentrancy protection on all state-modifying functions
• Safe token operations using OpenZeppelin’s SafeERC20
These mechanisms ensure the contract operates safely under all conditions, including
edge cases, malicious inputs, and extreme market conditions.
15