Python 3.8 is recommended for this project.
Alternative refactored general-purpose test suite: https://github.com/Hecate2/neo-test-client .
ruler.py
allows everyone to borrow paired token by paying a sum of collateral token.
In ruler.py
, the administrator of ruler.py is expected to call addPair
at first to allow other users to call deposit
and repay
to borrow paired tokens and pay back.
ruler
does not directly give paired token to the borrower, but only an equivalent amount of Ruler Capital Token (rcTokens) and another equivalent amount of Ruler Repayment Tokens (rrTokens).
rcTokens are expected to be sold on the market for paired token, and represent the right to claim paired token after the loan expires (by calling collect
in ruler.py
after expiry). Usually the price of an rcToken sold on the market is lower than 1 paired token, because rcToken holders can only collect the paired token after a period of time (after expiry). The interest rate of the loan is simply determined by this market-driven price of rcToken.
rrToken is a representation of debt, indicating that the holder must pay back paired token before expiry. rc and rr tokens are created and managed by deploying multiple rToken.py
contracts dynamically by ruler.py
.
The following figure shows the 4 steps of a loan operated by ruler and the market.
- Defaulted loans
- part of collaterals are paid to rcToken holders when part of paired tokens go defaulted
- Ruler protocol offers FUNGIBLE loan
- A
Pair
is utilized by many borrowers. The pool of defaulted paired token is considered as a whole. - Any loan expired with any defaulted paired token caused by any borrower leads to all the rcToken holders receiving some collaterals.
- A
- collateral token: 被抵押的币
- paired token: 被偿还的币
- expiry: a date before which the paired tokens are expected to be paid back
- mint ratio: 1 collateral token for how many rTokens (related to collateral ratio)
- rcToken: a representation of creditor's right; right to claim paired token . Users do not directly get paired token, but only rTokens from the ruler. rTokens are publicly sold on the market for paired token. Market-driven interest.
- rrToken: a representation of debt liability; obligation to pay back paired token
- (I give the ruler 1 paired token; the ruler returns me mint_ratio rcTokens and mint_ratio rrTokens)
- Pair
- collateral token
- paired token
- expiry
- mint ratio
- Representation: symbol of rTokens:
- ticker symbol:
RC_{Collateral}_{Mint Ratio}_{Paired Token}_{Expiry}
- e.g.
RC_wBTC_10000_Dai_12_31_2021
- ticker symbol:
- Pairs:
- All the
Pair
objects and attributes ofPair
are managed by administrator. - Borrowers should specify all the attributes of an existing
Pair
to borrow from that pair contract.
- All the
pip install neo3-boa==0.8.2
First you should compile rToken.py
with the command neo3-boa rToken.py
. This contract is used to manage rTokens, and is dynamically deployed by ruler
whenever new pairs are added.
Then run python get_nef_bytes.py
to print rToken.nef
as bytes. Paste the bytes in ruler.py
at the linerTokenTemplateNef: bytes = b'NEF3neo3-boa by COZ-0.8.1.0 ...
Now compile ruler.py
with
neo3-boa ruler.py
And deploy your ruler.nef
on the neo3 blockchain with the commanddeploy ruler.nef
in neo-cli. Remember to invoke the deploy
method of the ruler
contract to set the administrator. You can invoke the method using an RPC call, implemented by tests/flashLoanRate_administration_test.py
. Read and understand and edit the test code before running it!
/// deposit collateral to a Ruler Pair, sender receives rcTokens and rrTokens |
---|
function deposit |
address _col, address _paired, uint48 _expiry, uint256 _mintRatio, uint256 _colAmt |
/// repay with rrTokens and paired token amount, sender receives collateral, no fees charged on collateral |
---|
function repay |
/// sender collect paired tokens by returning same amount of rcTokens to Ruler |
---|
function collect |
/// redeem with rrTokens and rcTokens before expiry only, sender receives collateral, fees charged on collateral |
---|
function redeem |
/// market make deposit, deposit paired Token to received rcTokens, considered as an immediately repaid loan |
---|
function mmDeposit |
/// Directly receive paired token (instead of rcTokens to be sold) with a fee |
---|
function flashLoan |
function viewCollectible // How much I am eligible to collect
function getCollaterals // Which types of collaterals are available to be paid to borrow paired tokens
function getPairList(address _col) // detailed pair information using this type of collateral
function addPair |
---|
address _col, address _paired, uint48 _expiry, string calldata _expiryStr, uint256 _mintRatio, string calldata _mintRatioStr, uint256 _feeRate |
function _createRToken // deploy new contracts of rcToken and rrToken
function setFlashLoanRate
function updateCollateral
function setPairActive
function setFeeReceiver
function setPaused // pause new deposits
function setOracle
function maxFlashLoan
function flashFee
Part of paired tokens are marked as fees when borrowers repay
. Part of collaterals are marked as fees when lenders collect
. Administrator can set the fee rate and the fee receiver.
- Test of token contract
rToken.py
: almost OK Pair
CR(U?)(D?);collaterals
CR(U?)(D?)deposit
unit testrepay
unit testcollect
unit test
- Different rulers may deploy rToken contracts of the same
Pair(collateral, paired, expiry, mint_ratio)
. The ruler who deploys the rToken later would run into error:Contract Already Exists: {contract_hash}
. A potential idea to resolve the conflict, is to add ruler's executing_script_hash into the name of rToken manifest. However, this method results in'0x05' is invalid within a JSON string. The string should be correctly escaped.
, because executing_script_hash is not valid string. - Precision of amount of returned token: repaying a little bit of GAS leads to 0 NEO returned. Solution: DO NOT USE NATIVE NEO IN RULER!
- It's VERY DIFFICULT to interpret the raw results returned from the contract. There has to be an SDK for users. Do not forget to check out
tests/utils.py
and the test suite in this repository.
- RULER token and liquidity mining
- xRULER token
no contract inheritance in Python
no support for returning multiple values
Cannot use wallet with Python SDK
pip install -r requirements.txt
- VM-based
- Use
ruler_test.py
to run scripts onneo3vm
, utilizingneo-mamba
. - Pros:
- easily set the environment on the chain
- faster execution
- Cons:
- No wallet support for now
- Cannot utilize the latest
neo-vm
- Difficult to know the reason of exceptions raised from inside the
vm
- Use
- private-chain-based
- run a private chain with a consensus node and an outer node. The outer node accepts RPC requests from Python, and is run in a visual studio C# debugger with source codes of NEO.
- Assisted by DumpNef, you can relate the InstructionPointer of the vm to the bytecodes in
.nef
files and the original Python codes of contract. Create breakpoints inExecutionEngine.cs
to debug at assembly level. - Pros:
- Easy to watch the internal procedures in the blockchain. Easy to detect errors.
- Almost the same as production environment
- Cons
- Harder to setup and reset the environment
- Slower execution; unknown time for the transaction to be relayed on the blockchain.
Intuitively, you may want to deploy your smart contract onto your private blockchain, and test it via neo-cli
commands manually, or via RpcServer
plugin of neo-cli
automatically. In fact, you do not always have to run a "real" blockchain to test your contract. The NEO virtual machine, as the backend of the blockchain's node, can execute the smart contract for you. Just follow these steps:
-
Build a "virtual" blockchain snapshot as the environment for the vm, simply with 2 lines of codes.
from neo3 import blockchain blockchain.Blockchain.__it__ = None # This ensures your blockchain as a singleton object. Refer to: # https://github.com/CityOfZion/neo-mamba/blob/873932c8cb25497b90a39b3e327572746764e699/neo3/network/convenience/singleton.py#L12 snapshot = blockchain.Blockchain(store_genesis_block=True).currentSnapshot # blockchain is singleton
This snapshot contains blocks (only the genesis block in our case), contracts deployed on the chain, the history of transactions, and the storage status of all the contracts. This snapshot of blockchain does not update itself automatically. Instead, we use a vm to execute contracts and interact with the chain.
-
Build your vm, using your blockchain as the environment
from neo3 import vm, contracts from neo3.network import payloads from neo3.contracts import ApplicationEngine tx = payloads.Transaction._serializable_init() engine = ApplicationEngine(contracts.TriggerType.APPLICATION, tx, snapshot, 0, test_mode=True)
Now you have an
engine
to run custom smart contracts.tx
is an emptycontainer
for your execution of contract. -
Load your contract as a
Contract
objectYour compiler
neo3-boa
generates a.nef
file and a.manifest.json
file. Load them into your Python environment with:@staticmethod def read_raw_nef_and_raw_manifest(nef_path: str, manifest_path: str = '') -> Tuple[bytes, dict]: with open(nef_path, 'rb') as f: raw_nef = f.read() if not manifest_path: file_path, fullname = os.path.split(nef_path) nef_name, _ = os.path.splitext(fullname) manifest_path = os.path.join(file_path, nef_name + '.manifest.json') with open(manifest_path, 'r') as f: raw_manifest = json.loads(f.read()) return raw_nef, raw_manifest @staticmethod def build_nef_and_manifest_from_raw(raw_nef: bytes, raw_manifest: dict) \ -> Tuple[contracts.NEF, contracts.manifest.ContractManifest]: nef = contracts.NEF.deserialize_from_bytes(raw_nef) manifest = contracts.manifest.ContractManifest.from_json(raw_manifest) return nef, manifest
With the
nef
object andmanifest
object returned by functionbuild_nef_and_manifest_from_raw
, you can build you contract object:contract = contracts.ContractState(0, nef, manifest, 0, types.UInt160.deserialize_from_bytes(raw_nef))
The first
0
is the designated id of the contract in the block chain (assure no two contracts of the same id in the blockchain), and the second0
is the counter of how many times the contract has been updated (usually just leave this as0
). Thetypes.UInt160.deserialize_from_bytes(raw_nef)
is a placeholder for the hash of this contract. Note that the expression does not really generate the correct hash of the contract! -
Deploy your contract
engine.snapshot.contracts.put(contract)
A single line is enough for deploying.
-
Build a script to call a method in your contract
First, you need a
ScriptBuilder
:sb = vm.ScriptBuilder()
Now you can build a script to call a method with arguments:
sb.emit_dynamic_call_with_args(contract.hash, method, params)
or without arguments:
sb.emit_dynamic_call(contract.hash, method)
Here,
method: str
is the name of the method in the contract you want to call, andparams: List[int, str, bytes, UInt160, ...]
is the arguments for the method. If an argument in your method is of typeUInt160
, you should always giveUInt160
type inparams
.int
,bytes
andstr
are not allowed.Note that you are just building script in the
sb
object. You should then let the engine load your script built insb
. -
Load your script
engine.load_script(vm.Script(sb.to_array()))
-
Add signers
Signers are wallets who witness the transaction. This is a safety concern to prevent issues like your transferring another person's tokens into your wallet. Usually you can specify yourself as a signer like this:
from neo3.core import types from neo3.core.types import UInt160 signers = [payloads.Signer(types.UInt160.from_string('your_wallet_scripthash'), payloads.WitnessScope.CalledByEntry)]
And
engine.script_container.signers = signers
-
Execute your contract!
engine.execute()
-
Commit the execution to the snapshot
The execution of your contract should make effect on the blockchain, and in most cases you should persist this effect on the blockchain. So do not forget to commit the changes!
engine.snapshot.commit()
-
Watch the returned values of your method
print(engine.state, str(engine.result_stack))
A correct execution should result in
engine.state == VMState.HALT
. If you getVMState.FAULT
, your engine probably have run into troubles. -
Continuously execute another method
Your blockchain has been changed because of the previous execution, and you should inherit the state of the blockchain from the previous engine.
tx = payloads.Transaction._serializable_init() new_engine = ApplicationEngine(contracts.TriggerType.APPLICATION, tx, engine.snapshot, 0, test_mode=True)
Now your
new_engine
has obtained the state of the blockchain after the previous execution. Happy testing with yournew_engine
! -
The vm testing suite in this repository
Consider using this re-encapsulated engine to run your tests!
https://github.com/Hecate2/neo-ruler/blob/master/neo_test_with_vm/test_engine.py