Skip to content

Commit 264789c

Browse files
authored
feat(price_feeds/starknet): add send_usd example (#12)
1 parent 4ed83d8 commit 264789c

File tree

6 files changed

+209
-0
lines changed

6 files changed

+209
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
scarb 2.5.4
2+
starknet-foundry 0.21.0
3+
dojo 0.6.0
4+
starkli 0.2.8
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Code generated by scarb DO NOT EDIT.
2+
version = 1
3+
4+
[[package]]
5+
name = "openzeppelin"
6+
version = "0.10.0"
7+
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3"
8+
9+
[[package]]
10+
name = "pyth"
11+
version = "0.1.0"
12+
source = "git+https://github.com/pyth-network/pyth-crosschain.git?rev=ecc3a2f1#ecc3a2f17d470c8d59586ecd4897c4a63ece282a"
13+
dependencies = [
14+
"openzeppelin",
15+
"snforge_std",
16+
]
17+
18+
[[package]]
19+
name = "send_usd"
20+
version = "0.1.0"
21+
dependencies = [
22+
"openzeppelin",
23+
"pyth",
24+
]
25+
26+
[[package]]
27+
name = "snforge_std"
28+
version = "0.21.0"
29+
source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.21.0#2996b8c1dd66b2715fc67e69578089f278a46790"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "send_usd"
3+
version = "0.1.0"
4+
edition = "2023_11"
5+
6+
[dependencies]
7+
starknet = ">=2.5.4"
8+
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" }
9+
10+
# TODO: replace with tag after release
11+
pyth = { git = "https://github.com/pyth-network/pyth-crosschain.git", rev = "ecc3a2f1" }
12+
13+
[[target.starknet-contract]]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/bash
2+
3+
set -o errexit
4+
set -o nounset
5+
set -o pipefail
6+
set -x
7+
8+
starkli --version
9+
scarb --version
10+
11+
export STARKNET_ACCOUNT=katana-0
12+
export STARKNET_RPC=http://0.0.0.0:5050
13+
sleep="sleep 0.3"
14+
15+
cd "$(dirname "$0")/.."
16+
scarb build
17+
18+
# predeployed fee token contract in katana
19+
fee_token_address=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
20+
21+
if [ -z ${PYTH_ADDRESS+x} ]; then
22+
echo "Missing PYTH_ADDRESS env var"
23+
exit 1
24+
fi
25+
26+
send_usd_hash=$(starkli declare --watch target/dev/send_usd_send_usd.contract_class.json)
27+
${sleep}
28+
29+
# ETH/USD
30+
price_feed_id=0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
31+
32+
price_feed_u128_low=$(
33+
python3 -c "print(${price_feed_id} % (1<<128))"
34+
)
35+
price_feed_u128_high=$(
36+
python3 -c "print(${price_feed_id} // (1<<128))"
37+
)
38+
39+
send_usd_address=$(starkli deploy --watch "${send_usd_hash}" \
40+
"${PYTH_ADDRESS}" \
41+
"${fee_token_address}" \
42+
"${price_feed_u128_low}" "${price_feed_u128_high}" \
43+
)
44+
${sleep}
45+
46+
update_data=$(
47+
curl --request "GET" \
48+
"https://hermes.pyth.network/v2/updates/price/latest?ids%5B%5D=${price_feed_id}" \
49+
--header "accept: application/json" \
50+
| jq --raw-output '.binary.data[0]' \
51+
| cargo run --manifest-path "../../../../../pyth-crosschain/target_chains/starknet/tools/test_vaas/Cargo.toml" --bin hex_to_cairo_calldata
52+
)
53+
54+
starkli invoke --watch "${fee_token_address}" approve "${send_usd_address}" 1000000000000000000000 0
55+
${sleep}
56+
57+
destination=0x1001
58+
59+
echo "Old destination balance:"
60+
starkli balance "${destination}"
61+
62+
starkli invoke --watch --log-traffic "${send_usd_address}" send_usd \
63+
"${destination}" \
64+
10 0 `# amount_in_usd` \
65+
${update_data} \
66+
67+
${sleep}
68+
69+
echo "New destination balance:"
70+
starkli balance "${destination}"
71+
72+
echo send_usd contract has been successfully deployed at "${send_usd_address}"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use starknet::ContractAddress;
2+
use pyth::ByteArray;
3+
4+
#[starknet::interface]
5+
pub trait ISendUsd<T> {
6+
/// Sends ETH from the caller to the destination. The amount of ETH will be equivalent
7+
/// to the specified amount of USD, converted using the last available ETH/USD price from Pyth.
8+
/// `price_update` should be the latest available price update for the ETH/USD price feed.
9+
/// The caller needs to set up sufficient allowance for this contract.
10+
fn send_usd(
11+
ref self: T, destination: ContractAddress, amount_in_usd: u256, price_update: ByteArray
12+
);
13+
}
14+
15+
#[starknet::contract]
16+
mod send_usd {
17+
use core::panic_with_felt252;
18+
use starknet::{ContractAddress, get_caller_address, get_contract_address};
19+
use pyth::{ByteArray, IPythDispatcher, IPythDispatcherTrait, exp10, UnwrapWithFelt252};
20+
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};
21+
22+
const MAX_PRICE_AGE: u64 = 3600; // 1 hour
23+
const WEI_PER_ETH: u256 = 1000000000000000000;
24+
25+
#[storage]
26+
struct Storage {
27+
pyth_address: ContractAddress,
28+
eth_erc20_address: ContractAddress,
29+
eth_usd_price_id: u256,
30+
}
31+
32+
/// Initializes the contract.
33+
/// `pyth_address` is the address of the deployed Pyth account.
34+
/// `eth_erc20_address` is the address of the ERC20 token account for the ETH token.
35+
/// `eth_usd_price_id` is the ID of Pyth's price feed for ETH/USD.
36+
#[constructor]
37+
fn constructor(
38+
ref self: ContractState,
39+
pyth_address: ContractAddress,
40+
eth_erc20_address: ContractAddress,
41+
eth_usd_price_id: u256,
42+
) {
43+
self.pyth_address.write(pyth_address);
44+
self.eth_erc20_address.write(eth_erc20_address);
45+
self.eth_usd_price_id.write(eth_usd_price_id);
46+
}
47+
48+
#[abi(embed_v0)]
49+
impl SendUsd of super::ISendUsd<ContractState> {
50+
fn send_usd(
51+
ref self: ContractState,
52+
destination: ContractAddress,
53+
amount_in_usd: u256,
54+
price_update: ByteArray
55+
) {
56+
let pyth = IPythDispatcher { contract_address: self.pyth_address.read() };
57+
let eth_erc20 = IERC20CamelDispatcher {
58+
contract_address: self.eth_erc20_address.read()
59+
};
60+
let caller = get_caller_address();
61+
let contract = get_contract_address();
62+
63+
let pyth_fee = pyth.get_update_fee(price_update.clone());
64+
if !eth_erc20.transferFrom(caller, contract, pyth_fee) {
65+
panic_with_felt252('insufficient allowance for fee');
66+
}
67+
if !eth_erc20.approve(pyth.contract_address, pyth_fee) {
68+
panic_with_felt252('approve failed');
69+
}
70+
71+
pyth.update_price_feeds(price_update);
72+
73+
let price = pyth
74+
.get_price_no_older_than(self.eth_usd_price_id.read(), MAX_PRICE_AGE)
75+
.unwrap_with_felt252();
76+
77+
let price_u64: u64 = price.price.try_into().unwrap();
78+
let amount_in_wei = WEI_PER_ETH
79+
* exp10((-price.expo).try_into().unwrap())
80+
* amount_in_usd
81+
/ price_u64.into();
82+
83+
let transfer_ok = eth_erc20
84+
.transferFrom(caller, destination, amount_in_wei);
85+
if !transfer_ok {
86+
panic_with_felt252('insufficient allowance');
87+
}
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)