Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions price_feeds/starknet/send_usd/contract/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
4 changes: 4 additions & 0 deletions price_feeds/starknet/send_usd/contract/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
scarb 2.5.4
starknet-foundry 0.21.0
dojo 0.6.0
starkli 0.2.8
29 changes: 29 additions & 0 deletions price_feeds/starknet/send_usd/contract/Scarb.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "openzeppelin"
version = "0.10.0"
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3"

[[package]]
name = "pyth"
version = "0.1.0"
source = "git+https://github.com/pyth-network/pyth-crosschain.git?rev=ecc3a2f1#ecc3a2f17d470c8d59586ecd4897c4a63ece282a"
dependencies = [
"openzeppelin",
"snforge_std",
]

[[package]]
name = "send_usd"
version = "0.1.0"
dependencies = [
"openzeppelin",
"pyth",
]

[[package]]
name = "snforge_std"
version = "0.21.0"
source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.21.0#2996b8c1dd66b2715fc67e69578089f278a46790"
13 changes: 13 additions & 0 deletions price_feeds/starknet/send_usd/contract/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "send_usd"
version = "0.1.0"
edition = "2023_11"

[dependencies]
starknet = ">=2.5.4"
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" }

# TODO: replace with tag after release
pyth = { git = "https://github.com/pyth-network/pyth-crosschain.git", rev = "ecc3a2f1" }

[[target.starknet-contract]]
72 changes: 72 additions & 0 deletions price_feeds/starknet/send_usd/contract/deploy/local_deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash

set -o errexit
set -o nounset
set -o pipefail
set -x

starkli --version
scarb --version

export STARKNET_ACCOUNT=katana-0
export STARKNET_RPC=http://0.0.0.0:5050
sleep="sleep 0.3"

cd "$(dirname "$0")/.."
scarb build

# predeployed fee token contract in katana
fee_token_address=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7

if [ -z ${PYTH_ADDRESS+x} ]; then
echo "Missing PYTH_ADDRESS env var"
exit 1
fi

send_usd_hash=$(starkli declare --watch target/dev/send_usd_send_usd.contract_class.json)
${sleep}

# ETH/USD
price_feed_id=0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace

price_feed_u128_low=$(
python3 -c "print(${price_feed_id} % (1<<128))"
)
price_feed_u128_high=$(
python3 -c "print(${price_feed_id} // (1<<128))"
)

send_usd_address=$(starkli deploy --watch "${send_usd_hash}" \
"${PYTH_ADDRESS}" \
"${fee_token_address}" \
"${price_feed_u128_low}" "${price_feed_u128_high}" \
)
${sleep}

update_data=$(
curl --request "GET" \
"https://hermes.pyth.network/v2/updates/price/latest?ids%5B%5D=${price_feed_id}" \
--header "accept: application/json" \
| jq --raw-output '.binary.data[0]' \
| cargo run --manifest-path "../../../../../pyth-crosschain/target_chains/starknet/tools/test_vaas/Cargo.toml" --bin hex_to_cairo_calldata
)

starkli invoke --watch "${fee_token_address}" approve "${send_usd_address}" 1000000000000000000000 0
${sleep}

destination=0x1001

echo "Old destination balance:"
starkli balance "${destination}"

starkli invoke --watch --log-traffic "${send_usd_address}" send_usd \
"${destination}" \
10 0 `# amount_in_usd` \
${update_data} \

${sleep}

echo "New destination balance:"
starkli balance "${destination}"

echo send_usd contract has been successfully deployed at "${send_usd_address}"
90 changes: 90 additions & 0 deletions price_feeds/starknet/send_usd/contract/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use starknet::ContractAddress;
use pyth::ByteArray;

#[starknet::interface]
pub trait ISendUsd<T> {
/// Sends ETH from the caller to the destination. The amount of ETH will be equivalent
/// to the specified amount of USD, converted using the last available ETH/USD price from Pyth.
/// `price_update` should be the latest available price update for the ETH/USD price feed.
/// The caller needs to set up sufficient allowance for this contract.
fn send_usd(
ref self: T, destination: ContractAddress, amount_in_usd: u256, price_update: ByteArray
);
}

#[starknet::contract]
mod send_usd {
use core::panic_with_felt252;
use starknet::{ContractAddress, get_caller_address, get_contract_address};
use pyth::{ByteArray, IPythDispatcher, IPythDispatcherTrait, exp10, UnwrapWithFelt252};
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};

const MAX_PRICE_AGE: u64 = 3600; // 1 hour
const WEI_PER_ETH: u256 = 1000000000000000000;

#[storage]
struct Storage {
pyth_address: ContractAddress,
eth_erc20_address: ContractAddress,
eth_usd_price_id: u256,
}

/// Initializes the contract.
/// `pyth_address` is the address of the deployed Pyth account.
/// `eth_erc20_address` is the address of the ERC20 token account for the ETH token.
/// `eth_usd_price_id` is the ID of Pyth's price feed for ETH/USD.
#[constructor]
fn constructor(
ref self: ContractState,
pyth_address: ContractAddress,
eth_erc20_address: ContractAddress,
eth_usd_price_id: u256,
) {
self.pyth_address.write(pyth_address);
self.eth_erc20_address.write(eth_erc20_address);
self.eth_usd_price_id.write(eth_usd_price_id);
}

#[abi(embed_v0)]
impl SendUsd of super::ISendUsd<ContractState> {
fn send_usd(
ref self: ContractState,
destination: ContractAddress,
amount_in_usd: u256,
price_update: ByteArray
) {
let pyth = IPythDispatcher { contract_address: self.pyth_address.read() };
let eth_erc20 = IERC20CamelDispatcher {
contract_address: self.eth_erc20_address.read()
};
let caller = get_caller_address();
let contract = get_contract_address();

let pyth_fee = pyth.get_update_fee(price_update.clone());
if !eth_erc20.transferFrom(caller, contract, pyth_fee) {
panic_with_felt252('insufficient allowance for fee');
}
if !eth_erc20.approve(pyth.contract_address, pyth_fee) {
panic_with_felt252('approve failed');
}

pyth.update_price_feeds(price_update);

let price = pyth
.get_price_no_older_than(self.eth_usd_price_id.read(), MAX_PRICE_AGE)
.unwrap_with_felt252();

let price_u64: u64 = price.price.try_into().unwrap();
let amount_in_wei = WEI_PER_ETH
* exp10((-price.expo).try_into().unwrap())
* amount_in_usd
/ price_u64.into();

let transfer_ok = eth_erc20
.transferFrom(caller, destination, amount_in_wei);
if !transfer_ok {
panic_with_felt252('insufficient allowance');
}
}
}
}