- git - tested with v2.25.1
- node.js & npm - tested with node.js v10.19.0 and npm v6.14.4
- curl - tested with v7.68.0
- yarn - tested with v1.22.5
- python3 v3.8.5 or greater, python3-dev
- pip3 - tested with v20.0.2
- ganache-cli - tested with v6.9.1
- brownie v1.12.4 or greater
- solc-select only if you are having multiple solc installed locally and globally.
First install all the npm packages:
npm ci
And then install all the python packages using pip3:
pip3 install -r requirements.txt
To run prettier:
npm run prettier
To run linting of contracts:
npm run lint-contracts
To analyze the contracts using Slither:
npm run analyze-contracts
To run the tests written in python:
npm run test
To run tests written in JS:
npm run test
To check the test coverage of JS:
npm run coverage
Note: Sometimes it might show an error "JavaScript heap out of memory", then please increase the memory allocation using:
export NODE_OPTIONS=--max_old_space_size=4096
If still the error persists, make sure that you closed the shell and opened another shell. Also, if possible, increase the number from 4096
to a higher number in the above command.
- Add account with RBTC balance to brownie
brownie accounts new rskdeployer
- Add network Rsk-testnet
brownie networks add rsk testnet host=https://testnet.sovryn.app/rpc chainid=31
OR
brownie networks add rsk testnet host=https://public-node.testnet.rsk.co chainid=31
Note: If you want to work with mainnet, please use host as wss://mainnet.sovryn.app/ws
and chainid as 30
- Deploy contracts locally
brownie run deploy_everything.py
or use an absolute path to the script
brownie run ~/Code/Sovryn-smart-contracts/scripts/deployment/deploy_everything.py
- Deploy contracts on testnet
brownie run deploy_everything.py --network testnet
or use an absolute path to the script
brownie run ~/Code/Sovryn-smart-contracts/scripts/deployment/deploy_everything.py --network testnet
- Start
ganache
with
ganache-cli --gasLimit 6800000 --port 8545
Overriding default brownie
port will make it connect to our local chain and keep it open.
If you changed the port in the brownie config, use that port instead.
- Deploy contracts
brownie run deploy_everything.py
or use an absolute path to the script
brownie run ~/Code/Sovryn-smart-contracts/scripts/deployment/deploy_everything.py
-
Copy
SUSD
andRBTC
token addresses from the command line output -
Use addresses from #2 as reserves in the SovrynSwap codebase (
solidity/utils/config
) -
Deploy SovrynSwap contracts using the same chain and the updated config. Consult the README in
solidity/utils
. -
After deployment, copy the address of the deployed
ContractRegistry
and update thescripts/swap_test.json
accordingly. -
Run the
swap_test.py
script to set the SovrynSwap ContractRegistry address
brownie run swap_test.py
or use an absolute path to the script
brownie run ~/Code/Sovryn-smart-contracts/scripts/swapTest/swap_test.py
- Get MoC medianizer SC address (BTC to USD)
- Testnet: 0x667bd3d048FaEBb85bAa0E9f9D87cF4c8CDFE849
- Mainnet: See MoC Contracts verification.md
-
Modify
scripts/deploy_protocol.py
a. Deploy PriceFeedsMoC.sol
price_feed_moc = acct.deploy(PriceFeedsMoC, moc_medianizer_address)
b. instead of deploy PriceFeedsLocal use PriceFeeds.sol
feeds = acct.deploy(PriceFeeds, tokens.wrbtc.address, sovryn.address)
c. Set price feeds
feeds.setPriceFeed([tokens.rbtc.address, ...], [price_feed_moc.address, ...])
The smart contracts consist of 3 main components:
- The lending pools aka loan tokens
- The Sovryn trading protocol
- The Sovryn swap network
1 + 2 are part of this repository. 3 can be found in oracle-based-amm.
The watcher is a separate component located in its own repository. It does all of the work which requires an external watcher: liquidating and rolling over position and balancing the AMM.
The user provides funds to the lending pool using the mint
function and withdraws funds from the lending pool using the burn
function. Mint and burn refer to minting and burning iTokens. iTokens represent a share of the pool and gather interest over time.
In order to open a margin trade position, the user
- calls
marginTrade
on the loan token contract. - The loan token contract provides the loan and sends it for processing to the protocol proxy contract.
- The protocol proxy contract uses the module
LoanOpening
to create a position and swap the loan tokens to collateral tokens. - The Sovryn Swap network looks up the correct converter and swaps the tokens.
If successful, the position is being held by the protocol proxy contract, which is why positions need to be closed at the protocol proxy contract. There are 2 ways to close a position. They are explained in the sections 3.2 and 4.2.
Borrowing works similar to a trade. The user takes a loan from the loanToken contract, which tells the protocol to swap it to the currency of the collateral. The borrowed amount is paid out to the user instead of being held by the protocol. Therefore, each loan needs to be overcollateralized.
No trading and no borrowing can take place without a swap. Users can add and remove one-sided liquidity by calling addLiquidity
or removeLiquidity
on the LiquidityV2Converter contract. The AMM can be found in oracle-based-amm.
Whenever the current margin of a loan falls below maintenance margin, it needs to be liquidated. Anybody can initiate a liquidation and buy the collateral tokens at a discounted rate (5%). Liquidation is implemented in the LoanClosing
module.
Each loan has a duration. In case of a margin trade it is set to 28 days, in case of borrowing, it can be set by the user. On loan opnening, the user pays the interest for this duration in advance. If closing early, he gets the excess refunded. If it is not closed before the end date, it needs to be rolled over. On rollover the interest is paid for the next period. In case of margin trading it's 28 days, in case of borrowing it's a month.
The AMM allows for spot trading by interacting with the network contract directly. This is what the arbitraging procedure of the watcher is doing. The AMM tries to keep the balances of its reserves in equilibrium and therefore adjusts the rates to incentivize user to restore the balance. For an excellent explanation, look up this article.
To set the loan pool parameter, you need to call setLoanPool
on the protocol contract.
setLoanPool
is expecting the following parameter:
address[] calldata pools,
address[] calldata assets
pools
is an array of iToken addresses.
assets
is an array of underlying asset addresses.
For example: The underlying asset of iSUSD is sUSD.
To set up the margin pool parameter, you need to call updateSettings
on the iToken contract (LoanToken.sol).
updateSettings
is expecting the following parameter:
address settingsTarget,
bytes memory callData
settingsTarget
is the address of the settings contract (LoanTokenSettingsLowerAdmin.sol)
callData
is the encoded input for setupLoanParams
on the settings contract, which expects an array of LoanParams
, a struct defined in LoanParamsStruct.sol.
A LoanParams
object consists of following fields:
bytes32 id;
bool active;
address owner;
address loanToken;
address collateralToken;
uint256 minInitialMargin;
uint256 maintenanceMargin;
uint256 maxLoanTerm;
id
is the id of loan params object. Can be any bytes32.
active
tells if this object can be used for future loans.
owner
owner of this object (typically the contract owner).
loanToken
the underlying token. For example: sUSD in case of iSUSD. If calling updateSetting
this value can be left empty, because it will be overwritten anyway.
collateralToken
the required collateral token. For example: rBTC in case of iRBTC.
minInitialMargin
The minimum initial margin in percent with 18 decimals. For example: 20e18 for 20%.
maintenanceMargin
The minimum margin in percent with 18 decimals. If the margin drops below this value, the loan can and should be liquidated.
maxLoanTerm
The maximum loan term. If calling updateSetting
this value can be left empty, because it will be overwritten anyway (28 days).
Interest rates are being set up by calling setDemandCurve
on the loanToken contract. The formula for the interest rate is:
interestRate = baseRate + utilizationRate * rateMultiplier
In order to provide funds to the pool, call mint
on the respective iToken contract. This will take your deposit and give you iTokens in return. If you want to provide sUSD, call it to the iSUSD contract. If you want to provide rBTC, call it to the iRBTC contract.
mint
is expecting following parameter:
address receiver,
uint256 depositAmount
receiver
is the user address.
depositAmount
is the amount of tokens to be provided (not the number of iTokens to mint).
The function retrieves the tokens from the message sender, so make sure to first approve the iToken contract to access your funds. This is done by calling approve(address spender, uint amount)
on the ERC20 token contract, where spender is the iToken contract address and amount is the amount to be deposited.
In order to withdraw funds to the pool, call burn
on the respective iToken contract. This will burn your iTokens and send you the underlying token in exchange.
burn
is expecting the following parameter:
address receiver,
uint256 burnAmount
receiver
is the user address.
burnAmount
is the amount of tokens to be burned (not the number of underlying tokens to withdraw).
In order to enter a trade, call marginTrade
on the respective iToken contract.
Let's say you want to trade RBTC against SUSD. You enter a BTC long position by sending either of these currencies to the iSUSD contract and a short position by sending either of them to the iRBTC contract. The process is depicted below.
If you are sending ERC20 tokens as collateral, you first need to approve the iToken contract to access your funds. This is done by calling approve(address spender, uint amount)
on the ERC20 token contract, where spender is the iToken contract address and amount is the required collateral.
marginTrade
is expecting the following parameter:
bytes32 loanId,
uint256 leverageAmount,
uint256 loanTokenSent,
uint256 collateralTokenSent,
address collateralTokenAddress,
address trader,
bytes memory loanDataBytes
loanId
is 0 in case a new loan is opened for this position (the case most of the time). If an existing loan is used, this ID needs to be passed.
leverageAmount
is telling, if the position should open with 2x, 3x, 4x or 5x leverage. It is expected to be passed with 18 decimals.
loanTokenSent
and collateralTokenSent
are telling the contract about the amount of tokens provided as margin. If the margin is provided in the underlying currency of the iToken contract (e.g. SUSD for iSUSD), the contract will swap it to the collateral token (e.g. RBTC). The user can provide either one of the currencies or both of them.
collateralTokenAddress
specifies which collateral token is to be used. In theory an iToken contract can support multiple tokens as collateral. Which tokens are supported is specified during the margin pool setup (see above). In our case, there are just two tokens: RBTC and SUSD. RBTC is the collateral token for iSUSD and SUSD is the collateral token for iRBTC.
trader
is the user's wallet address.
loanDataBytes
is empty in case of ERC20 tokens.
There are 2 functions for ending a loan on the protocol contract: closeWithSwap
and closeWithDeposit
. Margin trade positions are always closed with a swap.
closeWithSwap
is expecting following parameter:
bytes32 loanId,
address receiver,
uint256 swapAmount,
bool returnTokenIsCollateral,
bytes memory loanDataBytes
loanId
is the ID of the loan, which is created on loan opening. It can be obtained either by parsing the Trade event or by reading the open loans from the contract by calling getActiveLoans
or getUserLoans
.
receiver
is the user's address.
swapAmount
defines how much of the position should be closed and is denominated in collateral tokens (e.g. rBTC on a iSUSD contract). If swapAmount >= collateral
, the complete position will be closed. Else if returnTokenIsCollateral == True
(swapAmount/collateral) * principal
will be swapped (partial closure). Else the closure amount will be the principal's covered amount
returnTokenIsCollateral
pass true
if you want to withdraw remaining collateral + profit in collateral tokens (e.g. rBTC on a iSUSD contract), false
if you want to withdraw it in loan tokens (e.g. sUSD on a iSUSD contract).
loanDataBytes
is not used at this point. Pass empty bytes.
Borrowing works similar to margin trading, just that the borrowed amount is withdrawn by the user instead of being held by the protocol. Just like with margin trading, the collateral is held in a differnet currency than the loan token. In our rBTC/sUSD example, sUSD is being used as collateral for rBTC and vice versa. In contrast to the margin trade call, it's not possible to send loan tokens, but collateral tokens must be sent: 150% of the withdrawn amount converted to collateral tokens.
borrow
expects following parameter:
bytes32 loanId,
uint256 withdrawAmount,
uint256 initialLoanDuration,
uint256 collateralTokenSent,
address collateralTokenAddress,
address borrower,
address receiver,
bytes memory loanDataBytes
loanId
is the ID of the loan, 0 for a new loan
withdrawAmount
is the amount to be withdrawn (actually borrowed)
initialLoanDuration
is the duration of the loan in seconds. if the loan is not paid back until then, it'll need to be rolled over
collateralTokenSent
is the amount of collateral token sent (150% of the withdrawn amount worth in collateral tokens). If the collateral is rBTC,
collateralTokenSent
needs to be added as value to the transaction as well.
collateralTokenAddress
is the address of the token to be used as collateral. cannot be the loan token address
borrower
is the address of the user paying for the collateral
receiver
is the address to receive the withdrawn amount
Loans can either be closed with a swap like explained in 3.2, or with a deposit. Closing with deposit might be considered the standard behaviour. With closeWithDeposit
the user pays back the loan fully or partially by sending loan tokens to the protocol contract. But users should always have the choice if they prefer to pay back the loan in loan tokens or pay it from their collateral.
closeWithDeposit
expects the following parameter:
bytes32 loanId,
address receiver,
uint256 depositAmount
loanId
is the id of the loan
receiver
is the receiver of the collateral
depositAmount
defines how much of the position should be paid back. It is denominated in loan tokens.
In order to add margin to a open position, call depositCollateral
on the protocol contract.
depositCollateral
expects following parameter:
bytes32 loanId,
uint256 depositAmount
loanId
is the ID of the loan
depositAmount
is the amount of collateral tokens to deposit.
When the maximum loan duration has been exceeded, the position will need to be rolled over. The function rollover
on the protocol contract extends the loan duration by the maximum term (28 days for margin trades at the moment of writing), pays the interest to the lender and refunds the caller for the gas cost by sending 2 * the gas cost using the fast gas price as base for the calculation.
rollover
expects following parameter:
bytes32 loanId,
bytes calldata loanDataBytes
loanId
is the ID of the loan.
loanDataBytes
is a placeholder for future use. Send an empty bytes array.
In order to liquidate an open position, call liquidate
on the protocol contract. Requirements:
- current margin < maintenance margin
- liquidator approved the protocol to spend sufficient tokens
liquidate
will compute the maximum seizable amount and buy it using the caller's tokens. Therefore, the caller needs to possess enough funds to purchase the tokens. The liquidator gets an discount on the collateral token price. The discount is set on State.sol and is called liquidationIncentivePercent
. Currently, it is hardcoded to 5%.
liquidate
expects following parameter:
bytes32 loanId,
address receiver,
uint256 closeAmount
loanId
is the ID of the loan
receiver
is the address receiving the seized funds
closeAmount
is the amount to liquidate. If closeAmount > maxLiquidatable, the maximum amount will be liquidated.
You can read all active loans from the smart contract calling getActiveLoans
. All active loans for a specific user can be retrieved with getUserLoans
. Both function will return a array of LoanReturnData
objects.
To query a single loan, use getLoan
.
LoanReturnData
objects contain following data:
bytes32 loanId;
address loanToken;
address collateralToken;
uint256 principal;
uint256 collateral;
uint256 interestOwedPerDay;
uint256 interestDepositRemaining;
uint256 startRate;
uint256 startMargin;
uint256 maintenanceMargin;
uint256 currentMargin;
uint256 maxLoanTerm;
uint256 endTimestamp;
uint256 maxLiquidatable;
uint256 maxSeizable;
loanId
is the ID of the loan
loanToken
is the address of the loan token
collateralToken
is the address of the collateral token
principal
is the complete borrowed amount (in loan tokens)
collateral
is the complete position size (loan + margin) (in collateral tokens)
interestOwedPerDay
is the interest per day
startRate
is the exchange rate at the beginning (collateral token to loan token)
startMargin
is the margin at the beginning (in percent, 18 decimals)
maintenanceMargin
is the minimum margin. If the current margin drops below, the position will be partially liquidated
currentMargin
is the current margin
maxLoanTerm
is the max duration of the loan
endTimestamp
afterwards the loan needs to be rolled over
maxLiquidatable
is the amount which can be liquidated (in loan tokens)
maxSeizable
is the amount which can be retrieved through liquidation (in collateral tokens)
totalAssetSupply()
returns the total amount of funds supplied to the iToken contract by the liquidity providers (lenders).
totalAssetBorrow()
returns the total amount of funds which is currently borrowed from the smart contract.
To read the current interest rate for liquidity providers (lenders), call supplyInterestRate()
. If you would like to obtain the potential interest rate after x assets are being lent to the contract, use nextSupplyInterestRate(x)
To read the current interest rate for borrowers and/or traders, call borrowInterestRate()
. If you would like to determine the interest rate to be paid considering the loan size x nextBorrowInterestRate(x)
.
avgBorrowInterestRate()
returns the average interest rate paid by borrowers at the moment. Since the interest rate depends on the ratio demand/supply, some borrower are paying more than others.
tokenPrice()
returns the current iToken price denominated in underlying tokens. In case of iSUSD, the price would be given in SUSD.
Each lender has 2 balances on the iToken contract. The balance of iTokens (e.g. iSUSD) and the balance of underlying tokens (e.g. SUSD).
balanceOf(address)
returns the iToken balance of the given address.
assetBalanceOf(address)
returns the underlying token balance of the given address.
The loan token (iToken) contract as well as the protocol contract act as proxies, delegating all calls to underlying contracts. Therefore, if you want to interact with them using web3, you need to use the ABIs from the contracts containing the actual logic or the interface contract.
ABI for LoanToken
contracts: LoanTokenLogicStandard
ABI for Protocol
contract: ISovryn
When deploying the protocol you need to:
- Configure the protocol address. It is an ERC20 token.
- Configure price rates between loan and protocol tokens. Using PriceFeeds
- Mint or approve tokens to protocol.
- Deposit protocol token.
sovryn.depositProtocolToken(amount)
When a user calls one of the below functions the protocol pays SOV rewards.
- closeWithSwap
- closeWithDeposit
- extendLoanDuration
- reduceLoanDuration
- borrow
- marginTrade
- rollover
- liquidate
The function which pays the reward is PayFeeReward
from FeesHelper.sol
.
The protocol can withdraw SOV tokens using sovryn.withdrawProtocolToken()
from ProtocolSettings.sol
. This function is executable only by the owner.
This project is licensed under the Apache License, Version 2.0.