Last active
October 6, 2021 10:11
-
-
Save gg2001/e655a7e8880b652c5b22292a34a85fe8 to your computer and use it in GitHub Desktop.
Python implementation of Compound's CToken & InterestRateModel contract
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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