Skip to content

Commit c3bf0aa

Browse files
committed
Prepare for submission
- add PTE UI git submodule - add oracle for price lookups - add helper functions for displaying global contract position data - add redemption and insolvency check code
1 parent 2049c47 commit c3bf0aa

File tree

7 files changed

+247
-20
lines changed

7 files changed

+247
-20
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "3-lending/rai-test/RAI-Scrypto-Lending-Platform-PTE"]
2+
path = 3-lending/rai-test/RAI-Scrypto-Lending-Platform-PTE
3+
url = [email protected]:dekentz/RAI-Scrypto-Lending-Platform-PTE.git
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "oracle_placeholder"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.4.1" }
8+
scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.4.1" }
9+
10+
[dev-dependencies]
11+
radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.4.1" }
12+
13+
[profile.release]
14+
opt-level = 's' # Optimize for size.
15+
lto = true # Enable Link Time Optimization.
16+
codegen-units = 1 # Reduce number of codegen units to increase optimizations.
17+
panic = 'abort' # Abort on panic.
18+
strip = "debuginfo" # Strip debug info.
19+
20+
[lib]
21+
crate-type = ["cdylib", "lib"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use scrypto::prelude::*;
2+
3+
blueprint! {
4+
struct OraclePlaceholder {
5+
xrd_price: Decimal
6+
}
7+
8+
impl OraclePlaceholder {
9+
pub fn new() -> ComponentAddress {
10+
info!("New OraclePlaceholder with default xrd_price: $0.10");
11+
12+
Self{
13+
xrd_price: dec!("0.10") // Default starting price of xrd as .10
14+
}
15+
.instantiate()
16+
.globalize()
17+
}
18+
19+
pub fn set_price(&mut self, price: Decimal) {
20+
info!("Oracle set_price: {}", price);
21+
self.xrd_price = price;
22+
}
23+
24+
pub fn get_price(&self) -> Decimal {
25+
self.xrd_price
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use radix_engine::ledger::*;
2+
use radix_engine::transaction::*;
3+
use scrypto::prelude::*;
4+
5+
#[test]
6+
fn test_hello() {
7+
// Set up environment.
8+
let mut ledger = InMemorySubstateStore::with_bootstrap();
9+
let mut executor = TransactionExecutor::new(&mut ledger, false);
10+
let (pk, sk, account) = executor.new_account();
11+
let package = executor.publish_package(compile_package!()).unwrap();
12+
13+
// Test the `instantiate_hello` function.
14+
let transaction1 = TransactionBuilder::new()
15+
.call_function(package, "Hello", "instantiate_hello", args![])
16+
.build(executor.get_nonce([pk]))
17+
.sign([&sk]);
18+
let receipt1 = executor.validate_and_execute(&transaction1).unwrap();
19+
println!("{:?}\n", receipt1);
20+
assert!(receipt1.result.is_ok());
21+
22+
// Test the `free_token` method.
23+
let component = receipt1.new_component_addresses[0];
24+
let transaction2 = TransactionBuilder::new()
25+
.call_method(component, "free_token", args![])
26+
.call_method_with_all_resources(account, "deposit_batch")
27+
.build(executor.get_nonce([pk]))
28+
.sign([&sk]);
29+
let receipt2 = executor.validate_and_execute(&transaction2).unwrap();
30+
println!("{:?}\n", receipt2);
31+
assert!(receipt2.result.is_ok());
32+
}

3-lending/rai-test/raitest.rev

+49-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
reset
22
new-account -> act
33

4+
publish ./dependencies/oracle_placeholder -> oracle_package
5+
call-function $oracle_package OraclePlaceholder new -> oracle_component
6+
47
publish . -> package
5-
call-function $package RaiTest new -> component adminbadge minterbadge position rai
8+
call-function $package RaiTest new $oracle_component -> component adminbadge minterbadge position rai
69
call-method $component open_position 500,030000000000000000000000000000000000000000000000000004
710
call-method $component draw 1,$position 5
811
call-method $component close_position_with_payment 1,$position 5,$rai
912

1013
reset
1114
new-account -> act
1215

16+
publish ./dependencies/oracle_placeholder -> oracle_package
17+
call-function $oracle_package OraclePlaceholder new -> oracle_component
18+
1319
publish . -> package
14-
call-function $package RaiTest new -> component adminbadge minterbadge position rai
20+
call-function $package RaiTest new $oracle_component -> component adminbadge minterbadge position rai
1521
call-method $component open_position 500,030000000000000000000000000000000000000000000000000004
1622
call-method $component draw 1,$position 5
1723
set-current-epoch 15000
@@ -22,11 +28,50 @@ call-method $component partial_withdraw_collateral 1,$position 400
2228
reset
2329
new-account -> act
2430

31+
publish ./dependencies/oracle_placeholder -> oracle_package
32+
call-function $oracle_package OraclePlaceholder new -> oracle_component
33+
34+
publish . -> package
35+
call-function $package RaiTest new $oracle_component -> component adminbadge minterbadge position rai
36+
call-method $component open_position 500,030000000000000000000000000000000000000000000000000004
37+
call-method $component draw #0000000000000000,$position 30
38+
set-current-epoch 15000
39+
call-method $component open_position 1000,030000000000000000000000000000000000000000000000000004
40+
call-method $component draw #0000000000000001,$position 50
41+
call-method $oracle_component set_price .075
42+
call-method $component liquidate 0000000000000000 50,$rai
43+
44+
reset
45+
new-account -> act
46+
47+
publish ./dependencies/oracle_placeholder -> oracle_package
48+
call-function $oracle_package OraclePlaceholder new -> oracle_component
49+
50+
publish . -> package
51+
call-function $package RaiTest new $oracle_component -> component adminbadge minterbadge position rai
52+
call-method $component open_position 500,030000000000000000000000000000000000000000000000000004
53+
call-method $component draw #0000000000000000,$position 30
54+
set-current-epoch 15000
55+
call-method $component open_position 1000,030000000000000000000000000000000000000000000000000004
56+
call-method $component draw #0000000000000001,$position 50
57+
call-method $oracle_component set_price .01
58+
call-method $component liquidate 0000000000000000 50,$rai
59+
call-method $component check_protocol_solvency
60+
call-method $component redeem 48.46186973689145765,$rai
61+
62+
reset
63+
new-account -> act
64+
65+
publish ./dependencies/oracle_placeholder -> oracle_package
66+
call-function $oracle_package OraclePlaceholder new -> oracle_component
67+
2568
publish . -> package
26-
call-function $package RaiTest new -> component adminbadge minterbadge position rai
69+
call-function $package RaiTest new $oracle_component -> component adminbadge minterbadge position rai
2770
call-method $component open_position 500,030000000000000000000000000000000000000000000000000004
2871
call-method $component draw #0000000000000000,$position 33
2972
set-current-epoch 15000
3073
call-method $component open_position 1000,030000000000000000000000000000000000000000000000000004
3174
call-method $component draw #0000000000000001,$position 50
32-
call-method $component liquidate 0 50,$rai
75+
call-method $component open_position 2500,030000000000000000000000000000000000000000000000000004
76+
call-method $component draw #0000000000000002,$position 100
77+
call-method $component print_all_positions

3-lending/rai-test/src/lib.rs

+113-16
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ blueprint! {
2727
minter: Vault,
2828
interest_rate: Decimal,
2929
positions_counter: u64,
30-
is_insolvent: bool
30+
is_insolvent: bool,
31+
oracle_address: ComponentAddress
3132
}
3233

3334
impl RaiTest {
3435

3536
// Create new RAI collateralized claim contract. This contract has sole minting authority over new position identifier nfts and the RAI supply.
36-
pub fn new() -> (ComponentAddress, Bucket) {
37+
pub fn new(oracle: ComponentAddress) -> (ComponentAddress, Bucket) {
3738

3839
let admin_badge: Bucket = ResourceBuilder::new_fungible()
3940
.metadata("name", "RaiTest Admin Badge")
@@ -58,6 +59,7 @@ blueprint! {
5859
.no_initial_supply();
5960

6061
let rules = AccessRules::new()
62+
.method("update_interest_rate", rule!(require(admin_badge.resource_address())))
6163
.default(rule!(allow_all));
6264

6365
let component = Self {
@@ -68,7 +70,8 @@ blueprint! {
6870
minter: Vault::with_bucket(minter),
6971
interest_rate: dec!("0.05"), // TODO - variable loan interest rate. For now, placeholder 5% interest rate.
7072
positions_counter: 0,
71-
is_insolvent: false
73+
is_insolvent: false,
74+
oracle_address: oracle
7275
}
7376
.instantiate()
7477
.add_access_check(rules)
@@ -126,7 +129,7 @@ blueprint! {
126129
"The position_badge bucket must contain exactly one position badge NFT"
127130
);
128131

129-
let required_collateral_xrd_amount = RaiTest::calc_required_collateral_xrd_amount(requested_rai);
132+
let required_collateral_xrd_amount = RaiTest::calc_required_collateral_xrd_amount(requested_rai, self.get_xrd_price());
130133
let position_id = position_badge.non_fungible::<PositionData>().id();
131134
let position = self.positions.get_mut(&position_id).unwrap();
132135

@@ -290,11 +293,13 @@ blueprint! {
290293
"The position_badge bucket does not contain a position badge NFT"
291294
);
292295

296+
let xrd_price = self.get_xrd_price();
297+
293298
let position_id = &position_badge.non_fungible::<PositionData>().id();
294299
let position = self.positions.get_mut(&position_id).unwrap();
295300
let principal_and_interest = RaiTest::calc_principal_and_interest(position.loan_amount, self.interest_rate, position.start_epoch);
296301

297-
let required_collateral_xrd_amount = RaiTest::calc_required_collateral_xrd_amount(principal_and_interest);
302+
let required_collateral_xrd_amount = RaiTest::calc_required_collateral_xrd_amount(principal_and_interest, xrd_price);
298303

299304
info!("Partial Withdraw Collateral - Position ID {} - {:?}", position_id, position);
300305
info!("Position Principal and Interest - {} RAI, minimum collateral required to maintain position - {} XRD", principal_and_interest, required_collateral_xrd_amount);
@@ -309,7 +314,7 @@ blueprint! {
309314
}
310315

311316
// Callable by anyone acting as a liquidator - provide undercollateralized position id and minimum RAI P&I payment to foreclose on position collateral
312-
pub fn liquidate(&mut self, position_id: u64, rai_payment: Bucket) -> (Bucket, Bucket) {
317+
pub fn liquidate(&mut self, position_id: NonFungibleId, rai_payment: Bucket) -> (Bucket, Bucket) {
313318
assert!(
314319
self.is_insolvent == false,
315320
"Protocol Insolvent - locked from liquidating positions and minting/burning RAI"
@@ -318,11 +323,12 @@ blueprint! {
318323
rai_payment.resource_address() == self.rai_resource,
319324
"The rai_payment bucket does not contain RAI"
320325
);
321-
let position_id = NonFungibleId::from_u64(position_id);
326+
let xrd_price = self.get_xrd_price();
327+
322328
let position = self.positions.get_mut(&position_id).unwrap();
323329

324330
let principal_and_interest = RaiTest::calc_principal_and_interest(position.loan_amount, self.interest_rate, position.start_epoch);
325-
let required_collateral_xrd_amount = RaiTest::calc_required_collateral_xrd_amount(principal_and_interest);
331+
let required_collateral_xrd_amount = RaiTest::calc_required_collateral_xrd_amount(principal_and_interest, xrd_price);
326332

327333
assert!(principal_and_interest < required_collateral_xrd_amount);
328334
info!("Position id {} being liquidated, p&i is {} and required collateral xrd is {}, position only contains {} xrd collateral",
@@ -363,9 +369,13 @@ blueprint! {
363369
pub fn check_protocol_solvency(&mut self) {
364370
let rai_manager = borrow_resource_manager!(self.rai_resource);
365371
let total_rai_supply = rai_manager.total_supply();
366-
let pooled_collateral_value = RaiTest::calc_xrd_value(self.pooled_collateral_vault.amount());
372+
let pooled_collateral_value = self.calc_xrd_value(self.pooled_collateral_vault.amount());
373+
info!("Collateral pool xrd amount: {} XRD price: {} Pool value: {} Total RAI supply: {}", self.pooled_collateral_vault.amount(), self.get_xrd_price(), pooled_collateral_value, total_rai_supply);
367374
if total_rai_supply > pooled_collateral_value {
368375
self.is_insolvent = true;
376+
info!("!! Protocol is insolvent !! Freezing liquidations and new positions, redemptions against collateral pool allowed now");
377+
} else {
378+
info!("Protocol solvent");
369379
}
370380
}
371381

@@ -384,6 +394,7 @@ blueprint! {
384394
);
385395
let rai_manager = borrow_resource_manager!(self.rai_resource);
386396
let total_rai_supply = rai_manager.total_supply();
397+
info!("Total RAI supply outstanding: {}", total_rai_supply);
387398
let percentage_of_total = rai_to_redeem.amount() / total_rai_supply;
388399

389400
let collateral_redemption_amount = percentage_of_total * self.pooled_collateral_vault.amount();
@@ -395,21 +406,63 @@ blueprint! {
395406
let rai_manager = borrow_resource_manager!(self.rai_resource);
396407
rai_manager.burn(rai_to_redeem);
397408
});
409+
410+
info!("Redeem - {}% of RAI supply redemption, returning {}% of collateral pool - {} xrd", percentage_of_total*100, percentage_of_total*100, redemption_collateral.amount());
398411

399412
redemption_collateral
400413
}
401414

402-
fn calc_xrd_value(xrd_amount: Decimal) -> Decimal {
403-
// TODO - get xrd price from oracle. Fixed placeholder value for now.
404-
let xrd_price = dec!("0.10");
415+
// Normally, liquidators would be expected to run bots to subscribe to new `open_position` `draw` `paydown` `close_position`
416+
// events and maintain an off-ledger system for identifying which vaults are undercollateralized, and call liquidate on
417+
// the corresponding undercollateralized positions. However since subscription events are not yet available in Babylon,
418+
// provide this convenience function to print the all outstanding positions and identify if any vaults are available for
419+
// liquidation and allow manual inspection for liquidation.
420+
pub fn print_all_positions(&self) {
421+
let xrd_price = self.get_xrd_price();
422+
info!("xrd price ${}", xrd_price);
423+
for position_id in self.positions.keys() {
424+
let position = self.positions.get(position_id).unwrap();
425+
let principal_and_interest = RaiTest::calc_principal_and_interest(position.loan_amount, self.interest_rate, position.start_epoch);
426+
let required_collateral_amount = RaiTest::calc_required_collateral_xrd_amount(principal_and_interest, self.get_xrd_price());
427+
let required_collateral_value = required_collateral_amount * xrd_price;
428+
let undercollateralized = if position.collateral_amount < required_collateral_amount { true } else { false } ;
429+
let collateral_value = self.calc_xrd_value(position.collateral_amount);
430+
info!(
431+
"position_id {} {:?}, P&I {} RAI, collateral_value ${}, required_collateral_value ${}, required_collateral_amount {} XRD",
432+
position_id, position, principal_and_interest, collateral_value, required_collateral_value, required_collateral_amount
433+
);
434+
if undercollateralized {
435+
info!("!! position id {} undercollateralized!", position_id);
436+
}
437+
}
438+
}
405439

406-
xrd_amount / xrd_price
440+
// Allow the admin badge holder to update the interest rate for all positions.
441+
// To keep track of accruing interest across varying interest rates, upon each interest rate change,
442+
// across all positions calculate the accrued principal & interest and store that value and update the start_epoch field.
443+
// From that point on, it is treated as a newly originated loan at that start_epoch with the new interest rate.
444+
// In this fashion, historical interest accrued at previous interest rates is included in calculations as the interest rate varies.
445+
pub fn update_interest_rate(&mut self, new_interest_rate: Decimal) {
446+
for (position_id, position) in self.positions.iter_mut() {
447+
let principal_and_interest = RaiTest::calc_principal_and_interest(position.loan_amount, self.interest_rate, position.start_epoch);
448+
position.loan_amount = principal_and_interest;
449+
position.start_epoch = Runtime::current_epoch();
450+
info!("Compounded P&I under previous interest rate and updated position_id {} {:?}", position_id, position)
451+
}
452+
self.interest_rate = new_interest_rate;
453+
info!("Updated interest rate - new interest rate {}", self.interest_rate)
407454
}
408455

409-
fn calc_required_collateral_xrd_amount(loan_amount: Decimal) -> Decimal {
410-
// TODO - get xrd price from oracle. Fixed placeholder value for now.
411-
let xrd_price = dec!("0.10");
456+
fn get_xrd_price(&self) -> Decimal {
457+
let oracle: OraclePlaceholder = self.oracle_address.into();
458+
oracle.get_price()
459+
}
412460

461+
fn calc_xrd_value(&self, xrd_amount: Decimal) -> Decimal {
462+
xrd_amount * self.get_xrd_price()
463+
}
464+
465+
fn calc_required_collateral_xrd_amount(loan_amount: Decimal, xrd_price: Decimal) -> Decimal {
413466
let required_collateral_value = loan_amount * dec!("1.50");
414467
required_collateral_value / xrd_price
415468
}
@@ -444,3 +497,47 @@ fn decimal_pow(base: Decimal, mut power: u64) -> Decimal {
444497

445498
result
446499
}
500+
501+
import! { r#"
502+
{
503+
"package_address": "0125ec60daa5880cadf3c8f591ed1040bddd0bc572b3c0ae5b67e8",
504+
"blueprint_name": "OraclePlaceholder",
505+
"functions": [
506+
{
507+
"name": "new",
508+
"inputs": [],
509+
"output": {
510+
"type": "Custom",
511+
"name": "ComponentAddress",
512+
"generics": []
513+
}
514+
}
515+
],
516+
"methods": [
517+
{
518+
"name": "set_price",
519+
"mutability": "Mutable",
520+
"inputs": [
521+
{
522+
"type": "Custom",
523+
"name": "Decimal",
524+
"generics": []
525+
}
526+
],
527+
"output": {
528+
"type": "Unit"
529+
}
530+
},
531+
{
532+
"name": "get_price",
533+
"mutability": "Immutable",
534+
"inputs": [],
535+
"output": {
536+
"type": "Custom",
537+
"name": "Decimal",
538+
"generics": []
539+
}
540+
}
541+
]
542+
}"#
543+
}

0 commit comments

Comments
 (0)