Borrowing¶
Traders can borrow the desk’s base coin and post a longable token as collateral using one of two functions depending on which of the following use cases applies:
Leveraging: The trader holds a favorable view about a longable token and wants to multiply potential profits through leverage (note that leverage also multiplies potential losses).
Financing: The trader owns a longable token and needs funds but does not wish to sell that token.
The two functions available for borrowing are:
- borrow_then_post_longable(_amount: uint256, _longable: address, _longable_amount: uint256, _horizon: uint256, _contract: address, data: Bytes[256], _provider: address = empty(address)¶
Borrow base coin and post longable satisfying value-to-loan ratio in one transaction using a smart contract deployed by the borrower
_amount: The number of base_coin tokens to be borrowed_longableThe address of the ERC20 token that will be posted as collateral. Must be one of the approved longable tokens for this desk_longable_amount: The amount of longable to be posted against the loan at the end of the transaction_horizon: If a new loan, the loan expires at the block height _horizon blocks hence_contract: The address of the smart contract receiving the loan and that will also provide the required _longable_amount of the longable tokendata: Opaque calldata to be passed to the smart contract receiving the loan_provider: For use by third-party, front-end or other integration provider if applicable. Use default otherwise
Note
The provider in this case could be a third party that provided the smart contract to its users.
- post_longable_then_borrow(_amount: uint256, _longable: address, _longable_amount: uint256, _horizon: uint256, _provider: address = empty(address)¶
Post longable as collateral and borrow against it in one transaction. Can be used to borrow against excess collateral.
_amount: The amount of base_coin to borrow_longable: The address of the ERC20 token used as longable_longable_amount: Amount of longable being posted against the loan. Can be 0 if borrowing against longable already posted_horizon: If a new loan, the loan expires at the block height _horizon blocks hence_provider: For use by third-party, front-end or other integration provider if applicable. Use default otherwise
Note
The two borrowing functions will revert if the loan amount is so small the internally calculated borrowing fee is 0.
Users can call this function to determine the cost of borrowing:
- borrow_fee(_amount: uint256, _longable: address, _horizon: uint256, _is_extend: bool = False) uint256¶
Return the fee that will be charged for the loan. This is a
viewfunction._amount: The (new) amount to be borrowed. This is ignored if_is_extend = True_longable: The token that will be deposited against the loan_horizon: The desired horizon. If a loan formsg.senderagainst_longablealready exists, then_horizonis ignored and the expiration of the loan is unchanged_is_extend: Optional.Truemeans an existing loan is being extended. Default isFalse.
Note
The fee will depend on the horizon unless a loan against the same longable token is active in which case the fee is based on the rate used at the inception of the loan.
Notes on borrowing¶
A user can only have one active loan against a specific longable token. This means that all borrowed funds secured by that longable token expire at the same time.
A user can have multiple loans each secured by a different longable token and having different expiration dates.
When a user with an active loan against a specific longable token borrows additional amounts of base token against that same longable token, _horizon is ignored and the original expiration date of the loan remains unchanged.
Borrowing fees are added to the amount borrowed and are due, with the rest of the borrowed funds, at loan expiration.
Extending loan¶
A loan expires at the block height determined by the chosen horizon at the time the loan is taken. This function allows extending the loan:
- extend_loan(_longable: address, _horizon: uint256)¶
Extend the life of the loan taken by msg.sender against _longable longable
_longable: Address of longable used as collateral for this loan_horizon: The horizon. The extended loan expires_horizonblocks after the original expiration block
Note
The fee for extending the loan is the same fee that applies for a new loan for the amount and horizon of the loan being extended.
Managing Loans¶
Two functions can be used to manage an existing loan in order to maintain the minimal value-to-loan ratio required by the desk.
- post_longable(_longable_amount: uint256, _longable: address, longable_owner: address = msg.sender)¶
Add to the longable token backing an existing loan against that longable token in order to increase the value-to-loan ratio or park the longable tokens with a view of collecting fees from flashloans against that token.
_longable_amount: Amount of longable being deposited_longable: ERC20 token address to use as longablelongable_owner: Optional. Address to be credited for longable. This can be a borrowing smart contract.
Note
The last argument allows a user to deposit the longable token on behalf of another address. A use case for this is the user adding collateral to a loan taken by a smart contract the user deployed.
- withdraw_longable(_longable_amount: uint256, _longable: address)¶
Withdraw some/all of existing longable posted for an existing or a previous (repaid) loan.
_longable_amount: Amount of the longable token to withdraw_longable: Longable token to withdraw
Note
For an existing loan, this function will revert if the amount of longable token remaining after the withdrawal will violate the required minimum value-to-loan ratio.
- loanOf(_user: address, _longable: address)¶
Returns information about the loan the user has taken against the specified longable: (
loan_amount,collateral_amount,loan_expiration). This is aviewfunction._user: The address of the user_longable: The ERC20 token posted against the loan
Note
Note that a user can have only one loan against a given longable token. The loan_expiration value is the block number at which the loan is due to expire.
Template Borrow Contracts¶
Smart contracts deployed by leveraged traders can follow these two templates. The first is written in Vyper and the second in Solidity.
# @version ^0.3.7
from vyper.interfaces import ERC20
MAX_DATA_LENGTH: constant(int128) = 256
interface CrayonDesk:
def borrow_then_post_longable(
_amount: uint256,
_longable: address,
_longable_amount: uint256,
_horizon: uint256,
_contract: address,
data: Bytes[MAX_DATA_LENGTH],
_provider: address = empty(address)
): nonpayable
def withdraw_longable_then_repay(
_amount: uint256,
_longable: address,
_longable_amount: uint256,
_contract: address,
data: Bytes[MAX_DATA_LENGTH],
_provider: address = empty(address)
): nonpayable
def base_coin() -> address: view
def loanOf(
_user: address,
_longable: address
) -> (uint256, uint256, uint256): view
def control_contract() -> address: view
interface C_Control:
def mint_all_reward_token(
_user: address
): nonpayable
def token_contract() -> address: view
enum Action:
buy
sell
desk: address
owner: address
@external
def __init__(
_desk: address
):
self.desk = _desk
self.owner = msg.sender
@external
def on_bridge_loan(
_initiator: address,
_token: address,
_amount: uint256,
data: Bytes[MAX_DATA_LENGTH]
) -> bytes32:
"""
@dev Callback function used by desk when smart contract calls borrow_then_post_longable() or withdraw_longable_then_repay()
@param _initiator The contract initiating the call
@param _token The desk's base coin (when borrowing) or a longable (when unwinding the trade with repay())
@param _amount The amount of tokens the desk transferred to this contract
@param data Data that was initially built by this contract and that, for example, contains actions upon callback
"""
# Only accept calls initiated from registered desk
assert msg.sender == self.desk
assert _initiator == self
action : Action = _abi_decode(data, Action)
# assert data was uncorrupted after passing through Pool
if action == Action.buy:
# enter position: for example, write the code to acquire the longable tokens
pass
elif action == Action.sell:
# exit position: for example, write the code to swap the longable tokens for base tokens
pass
else:
raise "on_bridge_loan(): unknown action"
return keccak256('IBridgeBorrower.on_bridge_loan')
@external
def borrow(
_amount: uint256,
_horizon: uint256,
_longable_token: address,
_longable_amount: uint256,
_provider: address
):
"""
@dev Borrow base coin and post longable satisfying value_to_loan_ratio in one transaction
@param _amount The number of base_coin tokens to be borrowed
@param _horizon The horizon for this loan, i.e., the period for which the loan is desired. Must be one of the acceptable horizons
@param _longable_token The address of the ERC20 token that will be posted. Must be one of the acceptable longable tokens
@param _longable_amount The amount of longable to be posted against the loan at the end of the transaction
@param _provider Set to empty(address) if writing your own contract. For use by front-end and/or third-party providers
"""
assert msg.sender == self.owner
# bespoke logic. we want to use the borrowed base coin to buy the longable token that we then post as collateral
data : Bytes[MAX_DATA_LENGTH] = _abi_encode(Action.buy)
# check how much borrower was already approved for
allowance : uint256 = ERC20(_longable_token).allowance(self, self.desk)
assert ERC20(_longable_token).approve(self.desk, allowance + _longable_amount)
CrayonDesk(self.desk).borrow_then_post_longable(_amount, _longable_token, _longable_amount, _horizon, self, data, _provider)
@external
def repay(
_longable_token: address,
_provider: address
):
"""
@dev Withdraw token posted as collateral and repay part/all of the loan in one transaction
@param _longable_token The address of the ERC20 token that was posted
@param _provider Set to empty(address) if writing your own contract. For use by front-end and/or third-party providers
"""
assert msg.sender == self.owner
amount : uint256 = 0
longable_amount : uint256 = 0
expiration : uint256 = max_value(uint256)
# get loan position of this contract
amount, longable_amount, expiration = CrayonDesk(self.desk).loanOf(self, _longable_token)
borrow_token : address = CrayonDesk(self.desk).base_coin()
allowance : uint256 = ERC20(borrow_token).allowance(self, self.desk)
assert ERC20(borrow_token).approve(self.desk, allowance + amount)
# bespoke logic. we want to sell the withdrawn collateral to generate the funds to pay back the loan
data : Bytes[MAX_DATA_LENGTH] = _abi_encode(Action.sell)
# pay back the full loan
CrayonDesk(self.desk).withdraw_longable_then_repay(amount, _longable_token, longable_amount, self, data, _provider)
@external
def mint(
_is_transferring : bool = False
):
"""
@dev Mint the XCRAY reward tokens that have accumulated to this smart contract
@param _is_transferring Optional parameter. True means transfer minted tokens to owner. Default is False
"""
# One way to do it...
control_contract : address = CrayonDesk(self.desk).control_contract()
c_control : C_Control = C_Control(control_contract)
c_control.mint_all_reward_token(self)
if _is_transferring:
xcToken : ERC20 = ERC20(c_control.token_contract())
xcToken.transfer(self.owner, xcToken.balanceOf(self))
And a template for Solidity traders:
// SPDX-License-Identifier: MIT
pragma solidity=0.8.19;
interface IErc20 {
function allowance(address _from, address _to) external view returns(uint256);
function transfer(address _to, uint _amount) external returns(bool success);
function approve(address _to, uint _amount) external returns(bool success);
function balanceOf(address _from) external view returns(uint256);
}
interface CrayonDesk {
function borrow_then_post_longable(
uint256 _amount,
address _longable,
uint256 _longable_amount,
uint256 _horizon,
address _contract,
bytes calldata data,
address _provider) external;
function withdraw_longable_then_repay(
uint256 _amount,
address _longable,
uint256 _longable_amount,
address _contract,
bytes calldata data,
address _provider) external;
function base_coin() external view returns(address);
function loanOf(
address _user,
address _longable) external view returns(uint256, uint256, uint256);
function control_contract() external view returns(address);
}
interface C_Control {
function mint_all_reward_token(
address _user
) external;
function token_contract() external view returns(address);
}
contract BorrowerS {
enum Action{ BUY, SELL }
address desk;
address owner;
constructor(address _desk) {
/*
* _desk is the desk from which we want to borrow
*/
desk = _desk;
owner = msg.sender;
}
function on_bridge_loan(
address _initiator,
address _token,
uint256 _amount,
bytes calldata data
) external returns(bytes32) {
/**
* @dev Callback function used by desk when contract calls borrow_then_post_longable() or withdraw_longable_then_repay()
* @param _initiator The contract initiating the call
* @param _token The desk's base coin (when borrowing) or a longable (when unwinding the trade with repay())
* @param _amount The amount of tokens the desk transferred to this contract
* @param data Data that was initially built by this contract and that, for example, contains actions upon callback
*/
require(msg.sender == desk);
require(_initiator == address(this));
(Action action) = abi.decode(data, (Action));
if (action == Action.BUY) {
// enter position: for example, write the code to acquire the longable tokens
} else if (action == Action.SELL) {
// exit position: for example, write the code to swap the longable tokens for base tokens
} else {
revert("on_bridge_loan(): unknown action");
}
return keccak256("IBridgeBorrower.on_bridge_loan");
}
function borrow(
uint256 _amount,
uint256 _horizon,
address _longable_token,
uint256 _longable_amount,
address _provider) external {
/**
* @dev Borrow base coin and post longable satisfying value_to_loan_ratio in one transaction
* @param _amount The number of base_coin tokens to be borrowed
* @param _horizon The horizon for this loan, i.e., the period for which the loan is desired. Must be one of the acceptable horizons
* @param _longable_token The address of the ERC20 token that will be posted. Must be one of the acceptable longable tokens
* @param _longable_amount The amount of longable to be posted against the loan at the end of the transaction
* @param _provider Set to empty(address) if writing your own contract. For use by front-end and/or third-party providers
*/
require(msg.sender == owner);
bytes memory data = abi.encode((Action.BUY));
uint256 allowance = IErc20(_longable_token).allowance(address(this), desk);
require(IErc20(_longable_token).approve(desk, allowance + _longable_amount));
CrayonDesk(desk).borrow_then_post_longable(_amount, _longable_token, _longable_amount, _horizon, address(this), data, _provider);
}
function repay(
address _longable_token,
address _provider) external {
/**
* @dev Withdraw token posted as collateral and repay part/all of the loan in one transaction
* @param _longable_token The address of the ERC20 token that was posted
* @param _provider Set to empty(address) if writing your own contract. For use by front-end and/or third-party providers
*/
require(msg.sender == owner);
uint256 amount = 0;
uint256 longable_amount = 0;
uint256 expiration = type(uint256).max;
// get loan position of this contract
(amount, longable_amount, expiration) = CrayonDesk(desk).loanOf(address(this), _longable_token);
address borrow_token = CrayonDesk(desk).base_coin();
uint256 allowance = IErc20(borrow_token).allowance(address(this), desk);
require(IErc20(borrow_token).approve(desk, allowance + amount));
// bespoke logic. we want to sell the withdrawn collateral to generate the funds to pay back the loan
bytes memory data = abi.encode(Action.SELL);
// pay back the full loan
CrayonDesk(desk).withdraw_longable_then_repay(amount, _longable_token, longable_amount, address(this), data, _provider);
}
function mint(
bool _is_transferring
) external {
/**
* @dev Mint the XCRAY reward tokens that have accumulated to this contract
* @param _is_transferring Optional parameter. True means transfer minted tokens to owner
*/
// One way to do it...
address control_contract = CrayonDesk(desk).control_contract();
C_Control c_control = C_Control(control_contract);
c_control.mint_all_reward_token(address(this));
if (_is_transferring) {
IErc20 xcToken = IErc20(c_control.token_contract());
xcToken.transfer(owner, xcToken.balanceOf(address(this)));
}
}
}