Skip to content

Instantly share code, notes, and snippets.

@gg2001
Last active October 6, 2021 10:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gg2001/e655a7e8880b652c5b22292a34a85fe8 to your computer and use it in GitHub Desktop.
Save gg2001/e655a7e8880b652c5b22292a34a85fe8 to your computer and use it in GitHub Desktop.
Python implementation of Compound's CToken & InterestRateModel contract
import abc
def truncate(n: int, decimals: int = 18) -> float:
return n / (10 ** decimals)
def apy(ratePerBlock: int) -> float:
return ((((((ratePerBlock / 1e18) * 6570) + 1) ** 365)) - 1) * 100
def oneCTokenInUnderlying(
exchangeRateCurrent: int, underlyingDecimals: int = 18, cTokenDecimals: int = 8
) -> float:
return exchangeRateCurrent / (
1 * (10 ** (18 + underlyingDecimals - cTokenDecimals))
)
class InterestRateModel(abc.ABC):
@abc.abstractmethod
def utilizationRate(self, cash: int, borrows: int, reserves: int) -> int:
pass
@abc.abstractmethod
def getBorrowRate(self, cash: int, borrows: int, reserves: int) -> int:
pass
@abc.abstractmethod
def getSupplyRate(
self, cash: int, borrows: int, reserves: int, reserveFactorMantissa: int
) -> int:
pass
@abc.abstractmethod
def __repr__(self) -> str:
pass
def __str__(self) -> str:
return self.__repr__()
class JumpRateModelV2(InterestRateModel):
blocksPerYear: int = 2102400
def __init__(
self,
baseRatePerYear: int,
multiplierPerYear: int,
jumpMultiplierPerYear: int,
kink_: int,
) -> None:
self.baseRatePerBlock: int = int(baseRatePerYear / self.blocksPerYear)
self.multiplierPerBlock: int = int(
(multiplierPerYear * 1e18) / (self.blocksPerYear * kink_)
)
self.jumpMultiplierPerBlock: int = int(
jumpMultiplierPerYear / self.blocksPerYear
)
self.kink: int = kink_
def utilizationRate(self, cash: int, borrows: int, reserves: int) -> int:
"""
If borrows == 0, 0
Else, (borrows * 1e18) / (cash + borrows - reserves)
"""
if borrows == 0:
return 0
return int((borrows * 1e18) / (cash + borrows - reserves))
def getBorrowRate(self, cash: int, borrows: int, reserves: int) -> int:
"""
If utilizationRate <= kink, util * multiplierPerBlock / 1e18 + baseRatePerBlock
Else, (util - kink) * jumpMultiplierPerBlock / 1e18 + (kink * multiplierPerBlock / 1e18 + baseRatePerBlock)
"""
util: int = self.utilizationRate(cash, borrows, reserves)
if util <= self.kink:
return int((util * self.multiplierPerBlock) / 1e18) + self.baseRatePerBlock
else:
normalRate: int = (
int((self.kink * self.multiplierPerBlock) / 1e18)
+ self.baseRatePerBlock
)
excessUtil: int = util - self.kink
return int((excessUtil * self.jumpMultiplierPerBlock) / 1e18) + normalRate
def getSupplyRate(
self, cash: int, borrows: int, reserves: int, reserveFactorMantissa: int
) -> int:
"""
utilizationRate * (borrowRate * (1e18 - reserveFactorMantissa) / 1e18) / 1e18
"""
oneMinusReserveFactor: int = int(1e18 - reserveFactorMantissa)
borrowRate: int = self.getBorrowRate(cash, borrows, reserves)
rateToPool: int = int(borrowRate * oneMinusReserveFactor / 1e18)
return int((self.utilizationRate(cash, borrows, reserves) * rateToPool) / 1e18)
def __repr__(self) -> str:
return "JumpRateModelV2()"
class CToken:
def __init__(
self,
cash: int,
borrowIndex: int,
totalBorrows: int,
totalReserves: int,
totalSupply: int,
reserveFactorMantissa: int,
interestRateModel: InterestRateModel,
initialExchangeRateMantissa: int = 200000000000000000000000000,
) -> None:
self.cash: int = cash
self.borrowIndex: int = borrowIndex
self.totalBorrows: int = totalBorrows
self.totalReserves: int = totalReserves
self.totalSupply: int = totalSupply
self.reserveFactorMantissa: int = reserveFactorMantissa
self.interestRateModel: InterestRateModel = interestRateModel
self.initialExchangeRateMantissa: int = initialExchangeRateMantissa
def utilizationRate(self) -> int:
return self.interestRateModel.utilizationRate(
self.cash, self.totalBorrows, self.totalReserves
)
def borrowRatePerBlock(self) -> int:
return self.interestRateModel.getBorrowRate(
self.cash, self.totalBorrows, self.totalReserves
)
def supplyRatePerBlock(self) -> int:
return self.interestRateModel.getSupplyRate(
self.cash, self.totalBorrows, self.totalReserves, self.reserveFactorMantissa
)
def accrueInterest(self, blocks: int = 1) -> None:
"""
State changing function
borrowIndex += ((borrowRatePerBlock * blocks) * borrowIndex / 1e18)
totalBorrows += (borrowRatePerBlock * blocks) * totalBorrows / 1e18
totalReserves += reserveFactorMantissa * ((borrowRatePerBlock * blocks) * totalBorrows / 1e18) / 1e18
"""
simpleInterestFactor: int = self.borrowRatePerBlock() * blocks
interestAccumulated: int = int(
(simpleInterestFactor * self.totalBorrows) / 1e18
)
self.borrowIndex += int((simpleInterestFactor * self.borrowIndex) / 1e18)
self.totalBorrows += interestAccumulated
self.totalReserves += int(
(self.reserveFactorMantissa * interestAccumulated) / 1e18
)
def exchangeRateCurrent(self) -> int:
"""
If totalSupply == 0, initialExchangeRateMantissa
Else, (cash + totalBorrows - totalReserves) * 1e18 / totalSupply
"""
if self.totalSupply == 0:
return self.initialExchangeRateMantissa
else:
return int(
((self.cash + self.totalBorrows - self.totalReserves) * 1e18)
/ self.totalSupply
)
def mint(self, amount: int) -> int:
"""
State changing function
Mints ((1e18 * amount) * 1e18 / exchangeRate) / 1e18 cTokens
"""
mintTokens: int = int(
int((1e18 * amount) * 1e18 / self.exchangeRateCurrent()) / 1e18
)
self.cash += amount
self.totalSupply += mintTokens
return mintTokens
def redeem(self, tokens: int) -> int:
"""
State changing function
Redeems exchangeRate * tokens / 1e18 underlying tokens
Burns tokens cTokens
"""
assert tokens <= self.totalSupply, "tokens must be <= totalSupply"
redeemAmount: int = int((self.exchangeRateCurrent() * tokens) / 1e18)
assert redeemAmount <= self.cash, "redeemAmount must be <= cash"
self.totalSupply -= tokens
self.cash -= redeemAmount
return redeemAmount
def redeemUnderlying(self, amount: int) -> int:
"""
State changing function
Redeems amount underlying tokens
Burns ((1e18 * amount) * 1e18 / exchangeRate) / 1e18 cTokens
"""
assert amount <= self.cash, "amount must be <= cash"
redeemTokens: int = int(
int((1e18 * amount) * 1e18 / self.exchangeRateCurrent()) / 1e18
)
assert redeemTokens <= self.totalSupply, "redeemTokens must be <= totalSupply"
self.totalSupply -= redeemTokens
self.cash -= amount
return redeemTokens
def borrow(self, amount: int) -> int:
"""
State changing function
"""
assert amount <= self.cash, "amount must be <= cash"
self.cash -= amount
self.totalBorrows += amount
return self.borrowIndex
def repayBorrow(self, amount: int) -> int:
"""
State changing function
"""
assert amount <= self.totalBorrows, "amount must be <= totalBorrows"
self.cash += amount
self.totalBorrows -= amount
return self.borrowIndex
def __repr__(self) -> str:
return f"CToken({self.cash}, {self.borrowIndex}, {self.totalBorrows}, {self.totalReserves}, {self.reserveFactorMantissa}, {self.interestRateModel}, {self.initialExchangeRateMantissa})"
def __str__(self) -> str:
return (
f"borrowIndex: {truncate(self.borrowIndex)}\n"
f"totalBorrows: {truncate(self.totalBorrows)} underlying\n"
f"totalReserves: {truncate(self.totalReserves)} underlying\n"
f"totalSupply: {truncate(self.totalSupply, 8)} cTokens\n"
f"Reserve Factor: {truncate(self.reserveFactorMantissa) * 100}%\n"
f"Utilization Rate: {truncate(self.utilizationRate()) * 100}%\n"
f"Borrow Rate: {apy(self.borrowRatePerBlock())}% APY\n"
f"Supply Rate: {apy(self.supplyRatePerBlock())}% APY\n"
f"Exchange Rate: 1 cToken = {oneCTokenInUnderlying(self.exchangeRateCurrent())} underlying"
)
class CTokenMockRates(CToken):
"""
Fixes the borrow and supply rates at initialization
"""
def __init__(
self,
cash: int,
borrowIndex: int,
totalBorrows: int,
totalReserves: int,
totalSupply: int,
reserveFactorMantissa: int,
interestRateModel: InterestRateModel,
) -> None:
super().__init__(
cash,
borrowIndex,
totalBorrows,
totalReserves,
totalSupply,
reserveFactorMantissa,
interestRateModel,
)
self.borrowRate: int = super().borrowRatePerBlock()
self.supplyRate: int = super().supplyRatePerBlock()
def borrowRatePerBlock(self) -> int:
return self.borrowRate
def supplyRatePerBlock(self) -> int:
return self.supplyRate
jumpRateModelV2: JumpRateModelV2 = JumpRateModelV2(
0, 40000000000000000, 1090000000000000000, 800000000000000000
)
cDAI: CToken = CToken(
560418991855600979032438074,
1110647625071840888,
1940211227502872799813599871,
11802415646753942679061263,
11501220189449317063,
150000000000000000,
jumpRateModelV2,
)
def main() -> None:
print("----- Year 0 -----")
print(cDAI)
years: int = 2
for year in range(1, years + 1):
print(f"----- Year {year} -----")
for _ in range(
2398050 # Number of blocks per year, assuming each block is mined every 13.5 seconds
):
cDAI.accrueInterest()
print(cDAI)
print("----- Done -----")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment