diff --git a/README.md b/README.md index ad56d42..7039700 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,13 @@ The tutorial is divided into the following steps: - At this point the user should be able to send STRK and ETH to the contract through our UI and see the events logged at the bottom. - [View changes from step-1 to step-2](https://github.com/Scaffold-Stark/basecamp/compare/step-1...step-2) -**Step 3: Full zklend Integration** ([branch: step-3](https://github.com/Scaffold-Stark/basecamp/tree/step-3)) +**Step 3: Full Vesu Integration** ([branch: step-3](https://github.com/Scaffold-Stark/basecamp/tree/step-3)) -- Updates [`YourContract.cairo`](https://github.com/Scaffold-Stark/basecamp/blob/step-3/packages/snfoundry/contracts/src/YourContract.cairo) with [zklend](https://app.zklend.com/markets) integration -- All the STRK and ETH deposits are now sent to zklend for yield farming -- Introduces development on mainnet fork. Can follow scaffold-stark [docs](https://scaffoldstark.com/docs/recipes/DevelopingOnFork) to run and interact with a local fork of Starknet mainnet. +- Introduces development on mainnet fork. Run mainnet fork: `yarn chain --fork-network https://starknet-mainnet.public.blastapi.io/rpc/v0_9`. Can follow scaffold-stark [docs](https://scaffoldstark.com/docs/recipes/DevelopingOnFork) to run and interact with a local fork of Starknet mainnet. +- To deposit or withdraw a Vesu token, you will need to interact with a "vToken", we will take vSTRK to interact +- Use `Configure Contracts` tool to to download Vesu vStrk configures at address `0x0147ae3337b168ac9abe80a7214f0cb9e874b25c3db530a8e04beb98a134e07a` and name vStrk - Minor `page.tsx` and `scaffold.config.ts` updates to support mainnetFork testing - Includes mainnet deployment steps -- Users can send STRK or ETH along with a greeting, these deposits will generate yield from first second onwards, owner can withdraw the yield anytime. User can connect with `burner wallet` to interact with the contract on Starknet mainnet fork. - [View changes from step-2 to step-3](https://github.com/Scaffold-Stark/basecamp/compare/step-2...step-3) Each step builds upon the previous one, introducing new concepts and features while maintaining a clean, production-ready codebase. diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index 45eceb4..a783b2f 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -53,7 +53,7 @@ const Home = () => { const { data: events } = useScaffoldEventHistory({ contractName: "YourContract", eventName: "GreetingChanged", - fromBlock: 1028886n, // NOTE : For now keep this block number as it is. When running local mainnet fork, use this block number as the starting block number. For example: `yarn chain --fork-network https://starknet-mainnet.public.blastapi.io/rpc/v0_7 --fork-block 1028886` + fromBlock: lastBlock ? (lastBlock > 50n ? BigInt(lastBlock - 50) : 0n) : 0n, watch: true, }); diff --git a/packages/nextjs/app/vesu/page.tsx b/packages/nextjs/app/vesu/page.tsx new file mode 100644 index 0000000..66d76b4 --- /dev/null +++ b/packages/nextjs/app/vesu/page.tsx @@ -0,0 +1,272 @@ +"use client"; + +import { ConnectedAddress } from "~~/components/ConnectedAddress"; +import { useState } from "react"; +import { useScaffoldReadContract } from "~~/hooks/scaffold-stark/useScaffoldReadContract"; +import { useScaffoldMultiWriteContract } from "~~/hooks/scaffold-stark/useScaffoldMultiWriteContract"; +import { useScaffoldEventHistory } from "~~/hooks/scaffold-stark/useScaffoldEventHistory"; +import { useDeployedContractInfo } from "~~/hooks/scaffold-stark/useDeployedContractInfo"; +import { useScaffoldWriteContract } from "~~/hooks/scaffold-stark/useScaffoldWriteContract"; +import { useTargetNetwork } from "~~/hooks/scaffold-stark/useTargetNetwork"; +import { useAccount } from "@starknet-react/core"; +import useScaffoldStrkBalance from "~~/hooks/scaffold-stark/useScaffoldStrkBalance"; + +const Home = () => { + const [displayAmount, setDisplayAmount] = useState(""); + const [amountWei, setAmountWei] = useState(0n); + + const { targetNetwork } = useTargetNetwork(); + + const { address: connectedAddress } = useAccount(); + + // vSTRK contract info + const { data: vStrk } = useDeployedContractInfo("vStrk"); + + const toHex = (v: unknown) => + typeof v === "bigint" ? `0x${v.toString(16)}` : (v as string); + + // Deposited amount = user's vSTRK balance (shares) + const { data: vStrkBalance } = useScaffoldReadContract({ + contractName: "vStrk", + functionName: "balance_of", + args: [connectedAddress], + }); + + // Available STRK balance of user + const { value: availableStrk } = useScaffoldStrkBalance({ + address: connectedAddress || undefined, + }); + + // Redeem (withdraw) all vSTRK shares to STRK + const { sendAsync: redeemAll, isPending: isRedeeming } = + useScaffoldWriteContract({ + contractName: "vStrk", + functionName: "redeem", + args: [vStrkBalance as any, connectedAddress, connectedAddress], + }); + + // STRK-only deposit flow: approve STRK to vSTRK, then deposit assets + const { sendAsync: depositStrk, isPending: isDepositing } = + useScaffoldMultiWriteContract({ + calls: [ + { + contractName: "Strk", + functionName: "approve", + args: [vStrk?.address, amountWei], + }, + { + contractName: "vStrk", + functionName: "deposit", + args: [amountWei, connectedAddress], + }, + ], + }); + + // Event history for vSTRK Deposit and Withdraw + const { data: depositEvents } = useScaffoldEventHistory({ + contractName: "vStrk", + eventName: "Deposit", + fromBlock: 1540452n, // NOTE : For now keep this block number as it is. When running local mainnet fork, use this block number as the starting block number. For example: `yarn chain --fork-network https://starknet-mainnet.public.blastapi.io/rpc/v0_9 --fork-block 1540452` + watch: true, + enabled: !!vStrk, + }); + const { data: withdrawEvents } = useScaffoldEventHistory({ + contractName: "vStrk", + eventName: "Withdraw", + fromBlock: 1540452n, + watch: true, + enabled: !!vStrk, + }); + + return ( +
+
+

+ + Scaffold-Stark Workshop + +
+ + {targetNetwork.name} + +
+

+ +
+ {/* Contract Status removed for Vesu demo */} +
+
+

+ Contract Management +

+ +
+
+
+ Withdraw all your vSTRK shares back to STRK. +
+
+
+ + Deposited vSTRK (shares) + + + {vStrkBalance + ? (Number(vStrkBalance) / 10 ** 18).toFixed(6) + : "0.000000"}{" "} + vSTRK + +
+
+ + Available STRK + + + {availableStrk + ? (Number(availableStrk) / 10 ** 18).toFixed(6) + : "0.000000"}{" "} + STRK + +
+
+
+
+
+

Deposit

+
+
+ +
+
+
+ { + const value = e.target.value; + setDisplayAmount(value); + if (value && value !== "") { + try { + const amountInWei = BigInt( + Math.floor(Number(value) * 10 ** 18), + ); + setAmountWei(amountInWei); + } catch (error) { + console.error("Invalid number input:", error); + setAmountWei(0n); + } + } else { + setAmountWei(0n); + } + }} + placeholder="Enter amount" + autoComplete="off" + /> +
+
+
+
+ +
+
+
+

+ vSTRK Activity +

+
+
+

Deposits

+
+ {depositEvents?.length ? ( + depositEvents.map((evt, idx) => ( +
+

+ Owner: {toHex(evt.args.owner)} +

+

+ Assets:{" "} + {(Number(evt.args.assets) / 10 ** 18).toFixed(6)} STRK + · Shares:{" "} + {(Number(evt.args.shares) / 10 ** 18).toFixed(6)}{" "} + vSTRK +

+
+ )) + ) : ( + No deposit events + )} +
+
+
+

Withdrawals

+
+ {withdrawEvents?.length ? ( + withdrawEvents.map((evt, idx) => ( +
+

+ Owner: {toHex(evt.args.owner)} +

+

+ Assets:{" "} + {(Number(evt.args.assets) / 10 ** 18).toFixed(6)} STRK + · Shares:{" "} + {(Number(evt.args.shares) / 10 ** 18).toFixed(6)}{" "} + vSTRK +

+
+ )) + ) : ( + No withdraw events + )} +
+
+
+
+
+
+
+ ); +}; +export default Home; diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 7c8c8c1..87a7d6f 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -30,6 +30,13 @@ export const menuLinks: HeaderMenuLink[] = [ href: "/debug", icon: , }, + { + label: "Vesu", + href: "/vesu", + icon: ( + Vesu + ), + }, ]; export const HeaderMenuLinks = () => { diff --git a/packages/nextjs/public/vesu-symbol-icon.svg b/packages/nextjs/public/vesu-symbol-icon.svg new file mode 100644 index 0000000..d909c31 --- /dev/null +++ b/packages/nextjs/public/vesu-symbol-icon.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/services/web3/connectors.tsx b/packages/nextjs/services/web3/connectors.tsx index 7dc7205..251d77b 100644 --- a/packages/nextjs/services/web3/connectors.tsx +++ b/packages/nextjs/services/web3/connectors.tsx @@ -4,7 +4,6 @@ import { BurnerConnector } from "@scaffold-stark/stark-burner"; import scaffoldConfig from "~~/scaffold.config"; import { LAST_CONNECTED_TIME_LOCALSTORAGE_KEY } from "~~/utils/Constants"; import { KeplrConnector } from "./keplr"; -import { supportedChains } from "~~/supportedChains"; const targetNetworks = getTargetNetworks(); @@ -34,7 +33,8 @@ function getConnectors() { connectors.push(new KeplrConnector()); } else { const burnerConnector = new BurnerConnector(); - burnerConnector.chain = supportedChains.devnet; + // burnerConnector's should be initialized with dynamic network instead of hardcoded devnet to support mainnetFork + burnerConnector.chain = targetNetworks[0]; connectors.push(burnerConnector as unknown as InjectedConnector); } diff --git a/packages/nextjs/supportedChains.ts b/packages/nextjs/supportedChains.ts index 6d10bed..fe9148a 100644 --- a/packages/nextjs/supportedChains.ts +++ b/packages/nextjs/supportedChains.ts @@ -4,7 +4,7 @@ const rpcUrlDevnet = process.env.NEXT_PUBLIC_DEVNET_PROVIDER_URL || "http://127.0.0.1:5050"; // devnet with mainnet network ID const mainnetFork = { - id: BigInt("0x534e5f4d41494e"), + id: chains.mainnet.id, network: "devnet", name: "Starknet Devnet", nativeCurrency: { @@ -24,8 +24,8 @@ const mainnetFork = { }, }, paymasterRpcUrls: { - default: { - http: [], + avnu: { + http: [rpcUrlDevnet], }, }, } as chains.Chain; diff --git a/packages/snfoundry/contracts/Scarb.toml b/packages/snfoundry/contracts/Scarb.toml index dbdf5d0..c081319 100644 --- a/packages/snfoundry/contracts/Scarb.toml +++ b/packages/snfoundry/contracts/Scarb.toml @@ -29,7 +29,7 @@ sort-module-level-items = true [[tool.snforge.fork]] name = "MAINNET_LATEST" url = "https://starknet-mainnet.public.blastapi.io/rpc/v0_9" -block_id.number = "1028886" +block_id.number = "1540452" block_id.tag = "latest" [tool] diff --git a/packages/snfoundry/contracts/src/your_contract.cairo b/packages/snfoundry/contracts/src/your_contract.cairo index c4a3ae9..5f57eb6 100644 --- a/packages/snfoundry/contracts/src/your_contract.cairo +++ b/packages/snfoundry/contracts/src/your_contract.cairo @@ -14,12 +14,6 @@ pub trait IYourContract { fn token_deposits(self: @TContractState, token: ContractAddress) -> u256; } -#[starknet::interface] -pub trait IZklendMarket { - fn deposit(ref self: TContractState, asset: ContractAddress, amount: felt252); - fn withdraw_all(ref self: TContractState, asset: ContractAddress); -} - #[starknet::contract] pub mod YourContract { use openzeppelin_access::ownable::OwnableComponent; @@ -29,7 +23,7 @@ pub mod YourContract { StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_caller_address, get_contract_address}; - use super::{IYourContract, IZklendMarketDispatcher, IZklendMarketDispatcherTrait}; + use super::IYourContract; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); @@ -41,8 +35,6 @@ pub mod YourContract { 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; pub const FELT_STRK_CONTRACT: felt252 = 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d; - const ZKLEND_MARKET: felt252 = - 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; #[event] #[derive(Drop, starknet::Event)] @@ -69,7 +61,6 @@ pub mod YourContract { premium: bool, total_counter: u256, user_greeting_counter: Map, - zklend_dispatcher: IZklendMarketDispatcher, token_deposits: Map, #[substorage(v0)] ownable: OwnableComponent::Storage, @@ -79,11 +70,6 @@ pub mod YourContract { fn constructor(ref self: ContractState, owner: ContractAddress) { self.greeting.write("Building Unstoppable Apps!!!"); self.ownable.initializer(owner); - - // setup dispatchers - self - .zklend_dispatcher - .write(IZklendMarketDispatcher { contract_address: ZKLEND_MARKET.try_into().unwrap() }); } #[abi(embed_v0)] @@ -113,7 +99,6 @@ pub mod YourContract { ._get_token_dispatcher(token) .transfer_from(get_caller_address(), get_contract_address(), amount); self.premium.write(true); - self._deposit_all_tokens_to_zklend(); self.token_deposits.write(token, self.token_deposits.read(token) + amount); self .emit( @@ -157,7 +142,6 @@ pub mod YourContract { fn withdraw(ref self: ContractState) { self.ownable.assert_only_owner(); - self._withdraw_all_tokens_from_zklend(); let eth_contract_address = FELT_ETH_CONTRACT.try_into().unwrap(); let strk_contract_address = FELT_STRK_CONTRACT.try_into().unwrap(); @@ -169,6 +153,9 @@ pub mod YourContract { eth_dispatcher.transfer(self.ownable.owner(), eth_balance); strk_dispatcher.transfer(self.ownable.owner(), strk_balance); + + self.token_deposits.write(eth_contract_address, 0); + self.token_deposits.write(strk_contract_address, 0); } } // internal @@ -180,44 +167,6 @@ pub mod YourContract { return IERC20Dispatcher { contract_address: token }; } - fn _deposit_all_tokens_to_zklend(ref self: ContractState) { - let eth_dispatcher = self._get_token_dispatcher(FELT_ETH_CONTRACT.try_into().unwrap()); - let strk_dispatcher = self - ._get_token_dispatcher(FELT_STRK_CONTRACT.try_into().unwrap()); - - let eth_balance = eth_dispatcher.balance_of(get_contract_address()); - let strk_balance = strk_dispatcher.balance_of(get_contract_address()); - - let eth_balance_felt: felt252 = eth_balance.try_into().expect('Amount too large'); - let strk_balance_felt: felt252 = strk_balance.try_into().expect('Amount too large'); - - if eth_balance > 0 { - eth_dispatcher.approve(ZKLEND_MARKET.try_into().unwrap(), eth_balance); - self - .zklend_dispatcher - .read() - .deposit(eth_dispatcher.contract_address, eth_balance_felt); - } - if strk_balance > 0 { - strk_dispatcher.approve(ZKLEND_MARKET.try_into().unwrap(), strk_balance); - self - .zklend_dispatcher - .read() - .deposit(strk_dispatcher.contract_address, strk_balance_felt); - } - } - - fn _withdraw_all_tokens_from_zklend(ref self: ContractState) { - if self.token_deposits.read(FELT_STRK_CONTRACT.try_into().unwrap()) > 0 { - self.zklend_dispatcher.read().withdraw_all(FELT_STRK_CONTRACT.try_into().unwrap()); - self.token_deposits.write(FELT_STRK_CONTRACT.try_into().unwrap(), 0); - } - if self.token_deposits.read(FELT_ETH_CONTRACT.try_into().unwrap()) > 0 { - self.zklend_dispatcher.read().withdraw_all(FELT_ETH_CONTRACT.try_into().unwrap()); - self.token_deposits.write(FELT_ETH_CONTRACT.try_into().unwrap(), 0); - } - } - fn _require_supported_token( ref self: ContractState, option_token: Option, ) { diff --git a/packages/snfoundry/contracts/tests/test_contract.cairo b/packages/snfoundry/contracts/tests/test_contract.cairo index c18d871..adb082b 100644 --- a/packages/snfoundry/contracts/tests/test_contract.cairo +++ b/packages/snfoundry/contracts/tests/test_contract.cairo @@ -6,8 +6,8 @@ use openzeppelin_utils::serde::SerializedAppend; use snforge_std::{CheatSpan, cheat_caller_address}; use starknet::ContractAddress; -// Real wallet address deployed on Mainnet -const OWNER: ContractAddress = 0x00c853dC4D9141DC6192FeCc999cC7333348Ab4fDCEf97B760ECb63A7FDE1c0e +// Real wallet address deployed on Sepolia +const OWNER: ContractAddress = 0x02dA5254690b46B9C4059C25366D1778839BE63C142d899F0306fd5c312A5918 .try_into() .unwrap(); @@ -31,14 +31,12 @@ fn test_set_greetings() { let new_greeting: ByteArray = "Learn Scaffold-Stark 2! :)"; dispatcher - .set_greeting( - new_greeting.clone(), Option::None, Option::None, - ); // we dont transfer any fri/wei + .set_greeting(new_greeting.clone(), Option::None, Option::None); // we dont transfer any eth assert(dispatcher.greeting() == new_greeting, 'Should allow set new message'); } #[test] -#[fork("MAINNET_LATEST")] +#[fork("SEPOLIA_LATEST")] fn test_transfer_eth() { let user: ContractAddress = OWNER.try_into().unwrap(); let your_contract_address = deploy_contract("YourContract"); @@ -66,7 +64,7 @@ fn test_transfer_eth() { } #[test] -#[fork("MAINNET_LATEST")] +#[fork("SEPOLIA_LATEST")] fn test_transfer_strk() { let user: ContractAddress = OWNER.try_into().unwrap(); let your_contract_address = deploy_contract("YourContract"); @@ -88,6 +86,6 @@ fn test_transfer_strk() { new_greeting.clone(), Option::Some(amount_to_transfer), Option::Some(STRK_TOKEN_CONTRACT_ADDRESS), - ); // we transfer 500 fri + ); // we transfer 500 fri/wei assert(your_contract_dispatcher.greeting() == new_greeting, 'Should allow set new message'); }