Created
April 4, 2023 16:12
-
-
Save Pet3ris/2287406696964c58d63cadca0ab905cc to your computer and use it in GitHub Desktop.
Yagi XBank Lending Vault Code
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
# 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