Skip to content

Commit 7df8baa

Browse files
yhzhang0128dadepoali-behjati
authored
An example contract for Solana (#72)
* Add example contract for solana Co-authored-by: dadepo <dadepo@gmail.com> Co-authored-by: Ali Behjati <bahjatia@gmail.com>
1 parent 37604d2 commit 7df8baa

File tree

17 files changed

+1393
-0
lines changed

17 files changed

+1393
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ members = [
66
"pyth-sdk-solana/test-contract",
77
"pyth-sdk-cw",
88
"examples/cw-contract",
9+
"examples/sol-contract"
910
]

examples/sol-contract/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build/example_sol_contract.so
2+
build/example_sol_contract-keypair.json
3+
scripts/invoke.js
4+
scripts/invoke.js.map
5+
scripts/node_modules/

examples/sol-contract/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "example-sol-contract"
3+
version = "0.1.0"
4+
authors = ["Pyth Data Foundation"]
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
10+
[dependencies]
11+
borsh = "0.9"
12+
arrayref = "0.3.6"
13+
solana-program = "1.10.40"
14+
pyth-sdk-solana = { path = "../../pyth-sdk-solana", version = "0.6.1" }
15+
16+
[dev-dependencies]
17+
solana-sdk = "1.10.40"
18+
solana-client = "1.10.40"

examples/sol-contract/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Pyth SDK Example Program for Solana
2+
3+
This is an example demonstrating how to read prices from Pyth on Solana.
4+
5+
The program has two instructions: `Init` and `Loan2Value`.
6+
`Init` can *only* be invoked by the program admin and it will initialize some loan information.
7+
`Loan2Value` can be invoked by anyone and it uses the current Pyth price to compare the value of the loan and the value of the collateral.
8+
This is an important functionality in many lending protocols.
9+
10+
The key program logic is in 3 files.
11+
The loan information structure is defined in `src/state.rs`, which also contains the serialization and deserialization code.
12+
The two instructions are implemented in `src/processor.rs`.
13+
An example invocation of these instructions on the Solana devnet can be found in `scripts/invoke.ts`.
14+
15+
## Where and how is the Pyth SDK used?
16+
Pyth SDK is used in the `Loan2Value` instruction in `src/processor.rs`.
17+
For the loan, the code first reads the unit price from the Pyth oracle.
18+
```rust
19+
let feed1 = load_price_feed_from_account_info(pyth_loan_account)?;
20+
let result1 = feed1.get_current_price().ok_or(ProgramError::Custom(3))?;
21+
```
22+
23+
And then calculate the loan value given the quantity of the loan.
24+
```rust
25+
let loan_max_price = result1
26+
.price
27+
.checked_add(result1.conf as i64)
28+
.ok_or(ProgramError::Custom(4))?;
29+
let loan_max_value = loan_max_price
30+
.checked_mul(loan_qty)
31+
.ok_or(ProgramError::Custom(4))?;
32+
```
33+
34+
This code says that, with high confidence, the maximum value of the loan does not exceed `loan_max_value * 10^(result1.expo)` at the time of the query.
35+
In a similar way, the code then calculates the minimum value of the collateral and compare the two.
36+
37+
More on Pyth best practice and price confidence interval can be found [here](https://docs.pyth.network/consume-data/best-practices).
38+
39+
## Run this program
40+
We assume that you have installed `cargo`, `solana`, `npm` and `node`.
41+
42+
```shell
43+
# Enter the root directory of this example
44+
> cd examples/sol-contract
45+
# Build the example contract
46+
> scripts/build.sh
47+
# Config solana CLI and set the url as devnet
48+
> solana config set --url https://api.devnet.solana.com
49+
# Deploy the example contract
50+
> scripts/deploy.sh
51+
# Invoke the example contract
52+
> scripts/invoke.ts
53+
```

examples/sol-contract/build/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This directory holds the output of build-bpf. See scripts/build.sh.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cargo build-bpf --bpf-out-dir ./build
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
echo "Airdropping..."
2+
solana airdrop 1 --url https://api.devnet.solana.com
3+
echo "Deploying the program..."
4+
solana program deploy --program-id build/example_sol_contract-keypair.json build/example_sol_contract.so
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cd scripts; npm install typescript; npm run build; node invoke.js
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
const fs = require('fs');
2+
const web3 = require("@solana/web3.js");
3+
const {struct, b, u8, blob} = require("@solana/buffer-layout");
4+
5+
export const invoke = async (loan: string, collateral: string) => {
6+
/* Obtain the contract keypair */
7+
var contract;
8+
try {
9+
let data = fs.readFileSync(
10+
'../build/example_sol_contract-keypair.json'
11+
);
12+
contract = web3.Keypair.fromSecretKey(
13+
new Uint8Array(JSON.parse(data))
14+
);
15+
console.info("Invoking contract " + contract.publicKey);
16+
} catch (error) {
17+
console.error("Please run scripts/build.sh first.");
18+
return;
19+
}
20+
21+
/* Prepare the payer account */
22+
let conn = new web3.Connection(web3.clusterApiUrl('devnet'));
23+
console.info("Airdropping to the payer account...");
24+
let payer = web3.Keypair.generate();
25+
let airdropSig = await conn.requestAirdrop(
26+
payer.publicKey, web3.LAMPORTS_PER_SOL
27+
);
28+
await conn.confirmTransaction(airdropSig);
29+
30+
/* Prepare the createInst instruction which creates an
31+
* account storing the AdminConfig data for the instructions */
32+
let loanInfoSize = 1 + 32 + 32;
33+
let dataAccount = web3.Keypair.generate();
34+
let dataCost = await conn.getMinimumBalanceForRentExemption(loanInfoSize);
35+
const createInst = web3.SystemProgram.createAccount({
36+
lamports: dataCost,
37+
space: loanInfoSize,
38+
programId: contract.publicKey,
39+
fromPubkey: payer.publicKey,
40+
newAccountPubkey: dataAccount.publicKey,
41+
});
42+
43+
/* Prepare the accounts and instruction data for transactions */
44+
const dataKey = dataAccount.publicKey;
45+
const loanKey = new web3.PublicKey(loan);
46+
const collateralKey = new web3.PublicKey(collateral);
47+
let accounts =
48+
[{pubkey: contract.publicKey, isSigner: true, isWritable: false},
49+
{pubkey: dataKey, isSigner: false, isWritable: false},
50+
{pubkey: loanKey, isSigner: false, isWritable: false},
51+
{pubkey: collateralKey, isSigner: false, isWritable: false},
52+
];
53+
54+
let initLayout = struct([ u8('instruction') ])
55+
let initData = Buffer.alloc(initLayout.span);
56+
let loan2ValueLayout = struct([
57+
u8('instruction'), blob(8, 'loan_qty'), blob(8, 'collateral_qty')
58+
])
59+
let loan2ValueData = Buffer.alloc(loan2ValueLayout.span);
60+
61+
/* Invoke the Init instruction (instruction #0) */
62+
console.log("Creating data account and invoking Init...");
63+
initLayout.encode({instruction: 0}, initData);
64+
let txInit = new web3.Transaction({ feePayer: payer.publicKey });
65+
txInit.add(
66+
createInst, /* Create data account */
67+
new web3.TransactionInstruction({ /* Initialize data account */
68+
data: initData,
69+
keys: accounts,
70+
programId: contract.publicKey
71+
})
72+
);
73+
let txInitSig = await web3.sendAndConfirmTransaction(
74+
conn, txInit, [payer, dataAccount, contract]
75+
);
76+
console.log("TxHash: " + txInitSig);
77+
78+
/* Invoke the Loan2Value instruction (instruction #1) */
79+
console.log("Checking loan to value ratio...");
80+
/* Encode 0x1 in big ending */
81+
let loan_qty = Buffer.from('0100000000000000', 'hex');
82+
/* Encode 0xbb8 (3000) in big ending */
83+
let collateral_qty = Buffer.from('b80b000000000000', 'hex');
84+
loan2ValueLayout.encode(
85+
{instruction: 1,
86+
loan_qty: blob(8).decode(loan_qty),
87+
collateral_qty: blob(8).decode(collateral_qty)}
88+
, loan2ValueData);
89+
90+
let txCheck = new web3.Transaction({ feePayer: payer.publicKey });
91+
txCheck.add(
92+
new web3.TransactionInstruction({
93+
data: loan2ValueData,
94+
keys: accounts,
95+
programId: contract.publicKey
96+
})
97+
);
98+
let txCheckSig = await web3.sendAndConfirmTransaction(
99+
conn, txCheck, [payer, contract]
100+
);
101+
console.log("TxHash: " + txCheckSig);
102+
103+
/* Try to invoke the Init instruction without authority */
104+
console.log("Trying an unauthorized invocation of Init...");
105+
let attacker = web3.Keypair.generate();
106+
accounts[0].pubkey = attacker.publicKey
107+
108+
let attackerDataAccount = web3.Keypair.generate();
109+
const attackerCreateInst = web3.SystemProgram.createAccount({
110+
lamports: dataCost,
111+
space: loanInfoSize,
112+
programId: contract.publicKey,
113+
fromPubkey: payer.publicKey,
114+
newAccountPubkey: attackerDataAccount.publicKey,
115+
});
116+
117+
let txAttacker = new web3.Transaction({ feePayer: payer.publicKey });
118+
txAttacker.add(
119+
attackerCreateInst,
120+
new web3.TransactionInstruction({
121+
data: initData,
122+
keys: accounts,
123+
programId: contract.publicKey
124+
})
125+
);
126+
127+
var attacker_succeed = false, txAttackerSig;
128+
try {
129+
txAttackerSig = await web3.sendAndConfirmTransaction(
130+
conn, txAttacker, [payer, attackerDataAccount, attacker]
131+
);
132+
attacker_succeed = true;
133+
} catch (error) {
134+
console.log("Attacker failed to invoke unauthorized Init.");
135+
}
136+
137+
if (attacker_succeed)
138+
throw new Error("Attacker succeeded! TxHash: " + txAttackerSig);
139+
}
140+
141+
/* Pyth price accounts on the solana devnet */
142+
let ethToUSD = "EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw";
143+
let usdtToUSD = "38xoQ4oeJCBrcVvca2cGk7iV1dAfrmTR1kmhSCJQ8Jto";
144+
invoke(ethToUSD, usdtToUSD);

0 commit comments

Comments
 (0)