Skip to content

Instantly share code, notes, and snippets.

@Pet3ris
Created April 4, 2023 16:12
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 Pet3ris/2287406696964c58d63cadca0ab905cc to your computer and use it in GitHub Desktop.
Save Pet3ris/2287406696964c58d63cadca0ab905cc to your computer and use it in GitHub Desktop.
Yagi XBank Lending Vault Code
# SPDX-License-Identifier: AGPL-3.0-or-later
%lang starknet
from starkware.cairo.common.bool import TRUE, FALSE
from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.starknet.common.syscalls import get_caller_address, get_contract_address
from starkware.cairo.common.uint256 import ALL_ONES, Uint256, uint256_check, uint256_eq
from openzeppelin.token.erc20.interfaces.IERC20 import IERC20
from openzeppelin.token.erc20.library import ERC20
from yagi.erc4626.library import ERC4626, ERC4626_asset, Deposit, Withdraw
from yagi.utils.fixedpointmathlib import mul_div_down, mul_div_up
from starkware.cairo.common.uint256 import uint256_le
from openzeppelin.security.safemath import uint256_checked_sub_le
from yagi.xbank.interfaces.IXtroller import IXtroller
from yagi.xbank.interfaces.IXToken import IXToken
# @title xBank ERC4626 lending vault.
# @description An ERC4626-style vault implementation that lends to xBank.
# @author Peteris <github.com/Pet3ris>
#############################################
# CONSTRUCTOR #
#############################################
@constructor
func constructor{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
asset : felt, name : felt, symbol : felt, xtroller : felt, xtoken : felt):
ERC4626.initializer(asset, name, symbol)
xtroller_.write(xtroller)
xtoken_.write(xtoken)
return ()
end
#############################################
# GETTERS #
#############################################
@view
func asset{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (asset : felt):
return ERC4626_asset.read()
end
@view
func xtroller{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (
xtroller : felt):
return xtroller_.read()
end
@view
func xtoken{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (xtoken : felt):
return xtoken_.read()
end
#############################################
# STORAGE #
#############################################
@storage_var
func xtroller_() -> (xtroller : felt):
end
@storage_var
func xtoken_() -> (xtoken : felt):
end
#############################################
# ACTIONS #
#############################################
@external
func deposit{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
assets : Uint256, receiver : felt) -> (shares : Uint256):
alloc_locals
# Check for rounding error since we round down in previewDeposit.
let (local shares) = previewDeposit(assets)
with_attr error_message("ERC4626: cannot deposit 0 shares"):
let ZERO = Uint256(0, 0)
let (shares_is_zero) = uint256_eq(shares, ZERO)
assert shares_is_zero = FALSE
end
# Need to transfer before minting or ERC777s could reenter.
let (asset) = ERC4626_asset.read()
let (local msg_sender) = get_caller_address()
let (local this) = get_contract_address()
IERC20.transferFrom(contract_address=asset, sender=msg_sender, recipient=this, amount=assets)
ERC20._mint(receiver, shares)
Deposit.emit(msg_sender, receiver, assets, shares)
_after_deposit(assets, shares)
return (shares)
end
@external
func mint{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
shares : Uint256, receiver : felt) -> (assets : Uint256):
alloc_locals
# No need to check for rounding error, previewMint rounds up.
let (local assets) = previewMint(shares)
# Need to transfer before minting or ERC777s could reenter.
let (asset) = ERC4626_asset.read()
let (local msg_sender) = get_caller_address()
let (local this) = get_contract_address()
IERC20.transferFrom(contract_address=asset, sender=msg_sender, recipient=this, amount=assets)
ERC20._mint(receiver, shares)
Deposit.emit(msg_sender, receiver, assets, shares)
_after_deposit(assets, shares)
return (assets)
end
@external
func withdraw{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
assets : Uint256, receiver : felt, owner : felt) -> (shares : Uint256):
alloc_locals
# No need to check for rounding error, previewWithdraw rounds up.
let (local shares) = previewWithdraw(assets)
let (local msg_sender) = get_caller_address()
ERC4626.ERC20_decrease_allowance_manual(owner, msg_sender, shares)
_before_withdraw(assets, shares)
ERC20._burn(owner, shares)
Withdraw.emit(owner, receiver, assets, shares)
let (asset) = ERC4626_asset.read()
IERC20.transfer(contract_address=asset, recipient=receiver, amount=assets)
return (shares)
end
@external
func redeem{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
shares : Uint256, receiver : felt, owner : felt) -> (assets : Uint256):
alloc_locals
let (local msg_sender) = get_caller_address()
ERC4626.ERC20_decrease_allowance_manual(owner, msg_sender, shares)
# Check for rounding error since we round down in previewRedeem.
let (local assets) = previewRedeem(shares)
let ZERO = Uint256(0, 0)
let (assets_is_zero) = uint256_eq(assets, ZERO)
with_attr error_message("ERC4626: cannot redeem 0 assets"):
assert assets_is_zero = FALSE
end
_before_withdraw(assets, shares)
ERC20._burn(owner, shares)
Withdraw.emit(owner, receiver, assets, shares)
let (asset) = ERC4626_asset.read()
IERC20.transfer(contract_address=asset, recipient=receiver, amount=assets)
return (assets)
end
#############################################
# MAX ACTIONS #
#############################################
@view
func maxDeposit{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(to : felt) -> (
maxAssets : Uint256):
alloc_locals
let (xtroller) = xtroller_.read()
let (xtoken) = xtoken_.read()
let (mint_guardian_paused) = IXtroller.get_mint_guardian_paused(
contract_address=xtroller, _xtoken=xtoken)
if mint_guardian_paused == TRUE:
return (Uint256(0, 0))
end
# So far there is no supply cap
let supply_cap = Uint256(ALL_ONES, ALL_ONES)
let ONE = Uint256(1000000000000000000, 0)
let (total_supply) = IXToken.totalSupply(contract_address=xtoken)
let (exchange_rate) = IXToken.get_exchange_rate_current(contract_address=xtoken)
# TODO: Review this in detail because it's modifying the function usage
let (assets_deposited) = mul_div_down(total_supply, exchange_rate, ONE)
let (max_assets) = uint256_checked_sub_le(supply_cap, assets_deposited)
return (max_assets)
end
@view
func maxMint{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(to : felt) -> (
maxShares : Uint256):
let (max_assets) = maxDeposit(to)
return convertToShares(max_assets)
end
@view
func maxWithdraw{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
from_ : felt) -> (maxAssets : Uint256):
alloc_locals
let (xtoken) = xtoken_.read()
let (cash) = IXToken.get_cash(contract_address=xtoken)
let (balance) = ERC20.balance_of(from_)
# TODO: Rename to balance_in_assets to map to other use case
let (assets_balance) = convertToAssets(balance)
# min(assets_balance, cash)
let (is_le) = uint256_le(assets_balance, cash)
if is_le == TRUE:
return (assets_balance)
end
return (cash)
end
@view
func maxRedeem{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
caller : felt) -> (maxShares : Uint256):
alloc_locals
let (xtoken) = xtoken_.read()
let (cash) = IXToken.get_cash(contract_address=xtoken)
let (cash_in_shares) = convertToShares(cash)
let (balance) = ERC20.balance_of(caller)
# min(balance, cash_in_shares)
let (is_le) = uint256_le(balance, cash_in_shares)
if is_le == TRUE:
return (balance)
end
return (cash_in_shares)
end
#############################################
# PREVIEW ACTIONS #
#############################################
@view
func previewDeposit{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
assets : Uint256) -> (shares : Uint256):
return convertToShares(assets)
end
@view
func previewMint{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
shares : Uint256) -> (assets : Uint256):
alloc_locals
# Probably not needed
with_attr error_message("ERC4626: shares is not a valid Uint256"):
uint256_check(shares)
end
let (local supply) = ERC20.total_supply()
let (local all_assets) = totalAssets()
let ZERO = Uint256(0, 0)
let (supply_is_zero) = uint256_eq(supply, ZERO)
if supply_is_zero == TRUE:
return (shares)
end
let (local z) = mul_div_up(shares, all_assets, supply)
return (z)
end
@view
func previewWithdraw{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
assets : Uint256) -> (shares : Uint256):
alloc_locals
# Probably not needed
with_attr error_message("ERC4626: assets is not a valid Uint256"):
uint256_check(assets)
end
let (local supply) = ERC20.total_supply()
let (local all_assets) = totalAssets()
let ZERO = Uint256(0, 0)
let (supply_is_zero) = uint256_eq(supply, ZERO)
if supply_is_zero == TRUE:
return (assets)
end
let (local z) = mul_div_up(assets, supply, all_assets)
return (z)
end
@view
func previewRedeem{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
shares : Uint256) -> (assets : Uint256):
return convertToAssets(shares)
end
#############################################
# CONVERT ACTIONS #
#############################################
@view
func convertToShares{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
assets : Uint256) -> (shares : Uint256):
alloc_locals
with_attr error_message("ERC4626: assets is not a valid Uint256"):
uint256_check(assets)
end
let (local supply) = ERC20.total_supply()
let (local allAssets) = totalAssets()
let ZERO = Uint256(0, 0)
let (supply_is_zero) = uint256_eq(supply, ZERO)
if supply_is_zero == TRUE:
return (assets)
end
let (local z) = mul_div_down(assets, supply, allAssets)
return (z)
end
@view
func convertToAssets{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
shares : Uint256) -> (assets : Uint256):
alloc_locals
with_attr error_message("ERC4626: shares is not a valid Uint256"):
uint256_check(shares)
end
let (local supply) = ERC20.total_supply()
let (local allAssets) = totalAssets()
let ZERO = Uint256(0, 0)
let (supply_is_zero) = uint256_eq(supply, ZERO)
if supply_is_zero == TRUE:
return (shares)
end
let (local z) = mul_div_down(shares, allAssets, supply)
return (z)
end
#############################################
# HOOKS TO OVERRIDE #
#############################################
@external
func totalAssets{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (
totalManagedAssets : Uint256):
alloc_locals
let (local this) = get_contract_address()
let (xtoken) = xtoken_.read()
let (balance) = IERC20.balanceOf(contract_address=xtoken, account=this)
let (exchange_rate) = IXToken.get_exchange_rate_current(contract_address=xtoken)
let ONE = Uint256(1000000000000000000, 0)
let (total_assets) = mul_div_down(balance, exchange_rate, ONE)
return (total_assets)
end
func _before_withdraw{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
assets : Uint256, shares : Uint256):
let (xtoken) = xtoken_.read()
# Here xbank differs from Compound in that redeem_underlying doesn't have
# a return value
IXToken.redeem_underlying(contract_address=xtoken, _underlying_token_amount=assets)
return ()
end
func _after_deposit{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
assets : Uint256, shares : Uint256):
alloc_locals
let (asset) = ERC4626_asset.read()
let (xtoken) = xtoken_.read()
IERC20.approve(contract_address=asset, spender=xtoken, amount=assets)
# Actual mint amount potentially differs from Compound interface
let (actual_mint_amount) = IXToken.mint(contract_address=xtoken, _mint_amount=assets)
let (fully_minted) = uint256_eq(assets, actual_mint_amount)
with_attr error_message("xBank ERC4626: MINT FAILED"):
assert fully_minted = TRUE
end
return ()
end
#############################################
# ERC20 #
#############################################
#
# Getters
#
@view
func name{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (name : felt):
let (name) = ERC20.name()
return (name)
end
@view
func symbol{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (symbol : felt):
let (symbol) = ERC20.symbol()
return (symbol)
end
@view
func totalSupply{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (
totalSupply : Uint256):
let (totalSupply : Uint256) = ERC20.total_supply()
return (totalSupply)
end
@view
func decimals{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() -> (
decimals : felt):
let (decimals) = ERC20.decimals()
return (decimals)
end
@view
func balanceOf{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
account : felt) -> (balance : Uint256):
let (balance : Uint256) = ERC20.balance_of(account)
return (balance)
end
@view
func allowance{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
owner : felt, spender : felt) -> (remaining : Uint256):
let (remaining : Uint256) = ERC20.allowance(owner, spender)
return (remaining)
end
#
# Externals
#
@external
func transfer{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
recipient : felt, amount : Uint256) -> (success : felt):
ERC20.transfer(recipient, amount)
return (TRUE)
end
@external
func transferFrom{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
sender : felt, recipient : felt, amount : Uint256) -> (success : felt):
ERC20.transfer_from(sender, recipient, amount)
return (TRUE)
end
@external
func approve{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
spender : felt, amount : Uint256) -> (success : felt):
ERC20.approve(spender, amount)
return (TRUE)
end
@external
func increaseAllowance{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
spender : felt, added_value : Uint256) -> (success : felt):
ERC20.increase_allowance(spender, added_value)
return (TRUE)
end
@external
func decreaseAllowance{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
spender : felt, subtracted_value : Uint256) -> (success : felt):
ERC20.decrease_allowance(spender, subtracted_value)
return (TRUE)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment