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
Pairis 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
Pairobjects and attributes ofPairare managed by administrator. - Borrowers should specify all the attributes of an existing
Pairto 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 PairCR(U?)(D?);collateralsCR(U?)(D?)depositunit testrepayunit testcollectunit 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.pyand 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.pyto 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
.neffiles and the original Python codes of contract. Create breakpoints inExecutionEngine.csto 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
engineto run custom smart contracts.txis an emptycontainerfor your execution of contract. -
Load your contract as a
ContractobjectYour compiler
neo3-boagenerates a.neffile and a.manifest.jsonfile. 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
nefobject andmanifestobject 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
0is the designated id of the contract in the block chain (assure no two contracts of the same id in the blockchain), and the second0is 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: stris 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 giveUInt160type inparams.int,bytesandstrare not allowed.Note that you are just building script in the
sbobject. 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_enginehas 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
