Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions modules/aave_v3_liquidity_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from templates.liquidity_module import LiquidityModule, Token
from typing import Dict, Optional
from datetime import datetime
from decimal import Decimal

from modules.utils.pool_math import AaveV3PoolMath
from modules.utils.base_math import *

class Aavev3LiquidityModule(LiquidityModule):
def get_amount_out(
self,
pool_states: Dict,
fixed_parameters: Dict,
input_token: Token,
output_token: Token,
input_amount: int,
) -> tuple[int | None, int | None]:
# Implement logic to calculate output amount given input amount
d = datetime.now()
reserveData = pool_states['ReserveData'] ## Fetched from the aavev3 pool getReserveData
scaledBalance = pool_states['ScaledBalance'] ## Fetched from the aavev3 pool scaledBalanceOf
previousIndex = pool_states['PreviousIndex'] ## Fetched from the aavev3 pool getPreviousIndex
currentScaledVariableDebt = pool_states['ScaledTotalSupply'] ## Fetched from the aavev3 pool getScaledTotalSupply
reserveConfigurationData = pool_states['ReserveConfigurationData'] ## Fetched from the aavev3 pool getReserveConfigurationData
isPaused = pool_states['IsPaused'] ## Fetched from the aavev3 pool isPaused
reserveCaps = pool_states['ReserveCaps'] ## Fetched from the aavev3 pool getReserveCaps
scaledTotalSupply = pool_states['ATokenScaledTotalSupply'] ## Fetched from the aavev3 pool getScaledTotalSupply

liquidityRate = reserveData['liquidityRate']
variableBorrowRate = reserveData['variableBorrowRate']
liquidityIndex = reserveData['liquidityIndex']
variableBorrowIndex = reserveData['variableBorrowIndex']
lastUpdateTimestamp = reserveData['lastUpdateTimestamp']
currentTimeStamp = int(d.timestamp())
isActive = reserveConfigurationData['isActive']
isFrozen = reserveConfigurationData['isFrozen']
supplyCap = reserveCaps['supplyCap']
accruedToTreasury = reserveData['accruedToTreasury']
decimals = reserveConfigurationData['decimals']

poolMath = AaveV3PoolMath(liquidityRate, variableBorrowRate, liquidityIndex, variableBorrowIndex, lastUpdateTimestamp, currentScaledVariableDebt, scaledBalance, previousIndex, currentTimeStamp)

if input_token.address == fixed_parameters['reserve_token']:
fee, output_amount, nextLiquidityIndex = poolMath.supply(input_amount)
# Validate Supply
if nextLiquidityIndex == None:
nextLiquidityIndex = liquidityIndex
if isActive == False or isFrozen == True or isPaused == True:
fee, output_amount = None, None
elif supplyCap !=0:
if rayMul((scaledTotalSupply + accruedToTreasury), nextLiquidityIndex) + input_amount > supplyCap * (10**decimals):
fee, output_amount = None, None
else:
fee, output_amount, nextLiquidityIndex = poolMath.withdraw(input_amount)
# Validate Withdraw
if isActive == False or isPaused == True:
fee, output_amount = None, None

return fee, output_amount

def get_amount_in(
self,
pool_states: Dict,
fixed_parameters: Dict,
input_token: Token,
output_token: Token,
output_amount: int
) -> tuple[int | None, int | None]:
d = datetime.now()

reserveData = pool_states['ReserveData'] ## Fetched from the aavev3 pool getReserveData
scaledBalance = pool_states['ScaledBalance'] ## Fetched from the aavev3 pool scaledBalanceOf
previousIndex = pool_states['PreviousIndex'] ## Fetched from the aavev3 pool getPreviousIndex
currentScaledVariableDebt = pool_states['ScaledTotalSupply'] ## Fetched from the aavev3 pool getScaledTotalSupply
reserveConfigurationData = pool_states['ReserveConfigurationData'] ## Fetched from the aavev3 pool getReserveConfigurationData
isPaused = pool_states['IsPaused'] ## Fetched from the aavev3 pool isPaused
reserveCaps = pool_states['ReserveCaps'] ## Fetched from the aavev3 pool getReserveCaps
scaledTotalSupply = pool_states['ATokenScaledTotalSupply'] ## Fetched from the aavev3 pool getScaledTotalSupply

liquidityRate = reserveData['liquidityRate']
variableBorrowRate = reserveData['variableBorrowRate']
liquidityIndex = reserveData['liquidityIndex']
variableBorrowIndex = reserveData['variableBorrowIndex']
lastUpdateTimestamp = reserveData['lastUpdateTimestamp']
currentTimeStamp = int(d.timestamp())
isActive = reserveConfigurationData['isActive']
isFrozen = reserveConfigurationData['isFrozen']
supplyCap = reserveCaps['supplyCap']
accruedToTreasury = reserveData['accruedToTreasury']
decimals = reserveConfigurationData['decimals']

poolMath = AaveV3PoolMath(liquidityRate, variableBorrowRate, liquidityIndex, variableBorrowIndex, lastUpdateTimestamp, currentScaledVariableDebt, scaledBalance, previousIndex, currentTimeStamp)

if input_token.address == fixed_parameters['reserve_token']:
fee, input_amount, nextLiquidityIndex = poolMath.withdraw(output_amount)
# Validate Supply
if nextLiquidityIndex == None:
nextLiquidityIndex = liquidityIndex
if isActive == False or isFrozen == True or isPaused == True:
fee, input_amount = None, None
elif supplyCap !=0:
if rayMul((scaledTotalSupply + accruedToTreasury), nextLiquidityIndex) + input_amount > supplyCap * (10**decimals):
fee, input_amount = None, None
else:
fee, input_amount, nextLiquidityIndex = poolMath.supply(output_amount)
# Validate Withdraw
if isActive == False or isPaused == True:
fee, input_amount = None, None

return fee, input_amount

def get_apy(self, pool_state: Dict) -> Decimal:
# Implement APY calculation logic
pass

def get_tvl(self, pool_state: Dict, token: Optional[Token] = None) -> Decimal:
# Implement TVL calculation logic
pass
59 changes: 59 additions & 0 deletions modules/utils/base_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from decimal import Decimal, getcontext

# Setting the precision for decimal operations
getcontext().prec = 30

WAD = Decimal('1e18')
halfWAD = WAD / 2

RAY = Decimal('1e27')
halfRAY = RAY / 2

WAD_RAY_RATIO = Decimal('1e9')

def ray():
return RAY

def wad():
return WAD

def halfRay():
return halfRAY

def halfWad():
return halfWAD

def wadMul(a, b):
return (halfWAD + (a * b)) / WAD

def wadDiv(a, b):
halfB = Decimal(b / 2)
return (halfB + (a * WAD)) / b

def rayMul(a, b):
a = Decimal(a)
b = Decimal(b)
return (halfRAY + (a * b)) / RAY

def rayDiv(a, b):
a = Decimal(a)
b = Decimal(b)
halfB = b / 2
return (halfB + (a * RAY)) / b

def rayToWad(a):
halfRatio = WAD_RAY_RATIO / 2
return (halfRatio + a) / WAD_RAY_RATIO

def wadToRay(a):
return a * WAD_RAY_RATIO

def rayPow(x, n):
z = x if n % 2 != 0 else RAY
n = n // 2
while n != 0:
x = rayMul(x, x)
if n % 2 != 0:
z = rayMul(z, x)
n = n // 2
return z
84 changes: 84 additions & 0 deletions modules/utils/pool_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from decimal import Decimal, getcontext

from modules.utils.base_math import *

getcontext().prec = 30
SECONDS_PER_YEAR = 365 * 24 * 60 * 60

class AaveV3PoolMath:
def __init__(self, liquidityRate, variableBorrowRate, liquidityIndex, variableBorrowIndex, lastUpdateTimestamp, currentScaledVariableDebt, scaledBalance, previousIndex, currentTimeStamp):
self.liquidityRate = liquidityRate
self.liquidityIndex = liquidityIndex
self.variableBorrowRate = variableBorrowRate
self.variableBorrowIndex = variableBorrowIndex
self.lastUpdateTimestamp = lastUpdateTimestamp
self.currentScaledVariableDebt = currentScaledVariableDebt
self.scaledBalance = scaledBalance
self.previousIndex = previousIndex
self.currentTimeStamp = currentTimeStamp

def calculateLinearInterest(self, rate, lastUpdateTimestamp, currentTimeStamp):
time_difference = currentTimeStamp - lastUpdateTimestamp
result = (rate * time_difference) / SECONDS_PER_YEAR
return RAY + Decimal(result)

def calculateCompoundedInterest(self, rate, lastUpdateTimestamp, currentTimeStamp):
exp = currentTimeStamp - lastUpdateTimestamp
if(exp == 0):
return RAY
expMinusOne = exp - 1
expMinusTwo = exp - 2 if exp > 2 else 0
basePowerTwo = rayMul(rate, rate) / (SECONDS_PER_YEAR * SECONDS_PER_YEAR)
basePowerThree = rayMul(basePowerTwo, rate) / SECONDS_PER_YEAR
secondTerm = exp * expMinusOne * basePowerTwo
secondTerm = secondTerm / 2
thirdTerm = exp * expMinusOne * expMinusTwo * basePowerThree
thirdTerm = thirdTerm / 6
result = (rate * exp) / Decimal(SECONDS_PER_YEAR) + Decimal(secondTerm) + Decimal(thirdTerm)
return RAY + Decimal(result)

def updateIndexes(self, currentLiquidityRate, liquidityIndex, currentVariableBorrowRate, variableBorrowIndex, currentScaledVariableDebt, lastUpdateTimestamp, currentTimeStamp):
if(currentLiquidityRate != 0):
cumulatedLiquidityInterest = self.calculateLinearInterest(currentLiquidityRate, lastUpdateTimestamp, currentTimeStamp)
nextLiquidityIndex = rayMul(cumulatedLiquidityInterest, liquidityIndex)

return nextLiquidityIndex

def updateState(self):
if(self.lastUpdateTimestamp == self.currentTimeStamp):
return self.liquidityIndex
nextLiquidityIndex = self.updateIndexes(self.liquidityRate, self.liquidityIndex, self.variableBorrowRate, self.variableBorrowIndex, self.currentScaledVariableDebt, self.lastUpdateTimestamp, self.currentTimeStamp)
self.lastUpdateTimestamp = self.currentTimeStamp
return nextLiquidityIndex

def burn(self, amount, nextLiquidityIndex, scaledBalance, previousIndex):
# amountScaled = rayDiv(amount, nextLiquidityIndex)
balanceIncrease = rayMul(scaledBalance, nextLiquidityIndex) - rayMul(scaledBalance, previousIndex)
amountToTransfer = 0
if(Decimal(balanceIncrease) > Decimal(amount)):
amountToTransfer = Decimal(balanceIncrease) - Decimal(amount)
else:
amountToTransfer = Decimal(amount) - Decimal(balanceIncrease)
return amountToTransfer

def mint(self, amount, nextLiquidityIndex, scaledBalance, previousIndex):
# amountScaled = rayDiv(amount, nextLiquidityIndex)
balanceIncrease = rayMul(scaledBalance, nextLiquidityIndex) - rayMul(scaledBalance, previousIndex)
amountToMint = Decimal(amount) + Decimal(balanceIncrease)
return amountToMint

def withdraw(self, amount):
try:
nextLiquidityIndex = self.updateState()
amountScaled = self.burn(amount, nextLiquidityIndex, self.scaledBalance, self.previousIndex)
return 0, int(amountScaled), nextLiquidityIndex
except:
return 0, amount, None

def supply(self, amount):
try:
nextLiquidityIndex = self.updateState()
amountScaled = self.mint(amount, nextLiquidityIndex, self.scaledBalance, self.previousIndex)
return 0, int(amountScaled), nextLiquidityIndex
except:
return 0, amount, None
115 changes: 115 additions & 0 deletions tests/test_aave_v3_liquidity_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import unittest
from unittest.mock import patch, MagicMock
from decimal import Decimal
from datetime import datetime

import sys
from os import listdir, path, walk

sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
sys.path.append(path.dirname(path.dirname(path.dirname(path.abspath(__file__)))))
sys.path.append(path.dirname(path.dirname(path.dirname(path.dirname(path.abspath(__file__))))))
sys.path.append(path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(path.abspath(__file__)))))))
sys.path.append(path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(path.abspath(__file__))))))))

from modules.aave_v3_liquidity_module import Aavev3LiquidityModule
from templates.liquidity_module import Token

SECONDS_PER_YEAR = 365 * 24 * 60 * 60
RAY = 10**27

class TestAavev3LiquidityModule(unittest.TestCase):
def setUp(self):
self.module = Aavev3LiquidityModule()
self.now = int(datetime.now().timestamp())
self.input_token = Token(address="0xTokenA", decimals=18, symbol="TokenA", reference_price=1)
self.output_token = Token(address="0xTokenA", decimals=18, symbol="TokenA", reference_price=1) # Same address for supply

self.valid_pool_states = {
"ReserveData": {
"liquidityRate": int(0.03 * RAY),
"variableBorrowRate": int(0.05 * RAY),
"liquidityIndex": RAY,
"variableBorrowIndex": RAY,
"lastUpdateTimestamp": self.now - 1000,
"accruedToTreasury": 0
},
"ScaledBalance": 1000,
"PreviousIndex": RAY,
"ScaledTotalSupply": 500000,
"ReserveConfigurationData": {
"isActive": True,
"isFrozen": False,
"decimals": 18
},
"IsPaused": False,
"ReserveCaps": {
"supplyCap": 1000000
},
"ATokenScaledTotalSupply": 100000
}

self.fixed_parameters = {
"reserve_token": "0xTokenA"
}

def test_supply_successful(self):
fee, amount_out = self.module.get_amount_out(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 1000
)
self.assertIsNotNone(amount_out)
self.assertEqual(amount_out, 1000)

def test_withdraw_successful(self):
self.input_token.address = "0xOtherToken" # force withdraw path
fee, amount_out = self.module.get_amount_out(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 1000
)
self.assertIsNotNone(amount_out)
self.assertGreaterEqual(amount_out, 0)

def test_supply_paused(self):
self.valid_pool_states["IsPaused"] = True
fee, amount_out = self.module.get_amount_out(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 1000
)
self.assertIsNone(amount_out)

def test_supply_above_cap(self):
self.valid_pool_states["ReserveCaps"]["supplyCap"] = 1 # very low cap
fee, amount_out = self.module.get_amount_out(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 10**20
)
self.assertIsNone(amount_out)

def test_withdraw_when_frozen(self):
self.valid_pool_states["ReserveConfigurationData"]["isFrozen"] = True
self.input_token.address = "0xTokenA"
fee, amount_out = self.module.get_amount_out(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 1000
)
self.assertIsNone(amount_out)

def test_get_amount_in_supply(self):
fee, amount_in = self.module.get_amount_in(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 1000
)
self.assertIsNotNone(amount_in)
self.assertGreater(amount_in, 0)

def test_get_amount_in_paused(self):
self.valid_pool_states["IsPaused"] = True
fee, amount_in = self.module.get_amount_in(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 1000
)
self.assertIsNone(amount_in)

def test_get_amount_in_withdraw(self):
self.input_token.address = "0xOtherToken"
fee, amount_in = self.module.get_amount_in(
self.valid_pool_states, self.fixed_parameters, self.input_token, self.output_token, 1000
)
self.assertIsNotNone(amount_in)

if __name__ == '__main__':
unittest.main()
Empty file removed tests/test_liquidity_module.py
Empty file.