From bfc3646961f4ca61d827b6887514dd23a404ab69 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Tue, 23 Jul 2024 19:38:29 -0700 Subject: [PATCH] imp: implmement register_token(), enhance tests, write scripts, add tests job --- .env.example | 8 ++ .github/workflows/tests.yml | 30 ++++++ .gitignore | 2 + contracts/src/apps/transfer/component.cairo | 111 +++++++++++++++++--- contracts/src/apps/transfer/errors.cairo | 6 +- contracts/src/apps/transfer/interface.cairo | 12 ++- contracts/src/apps/transfer/types.cairo | 8 +- contracts/src/contract.cairo | 38 +------ contracts/src/core/types.cairo | 4 +- contracts/src/lib.cairo | 1 + contracts/src/tests.cairo | 4 + contracts/src/tests/transfer.cairo | 51 +++++++++ contracts/src/tests/utils.cairo | 19 ++++ scripts/deploy.sh | 83 +++++++++++++++ scripts/invoke.sh | 31 ++++++ 15 files changed, 351 insertions(+), 57 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/tests.yml create mode 100644 contracts/src/tests.cairo create mode 100644 contracts/src/tests/transfer.cairo create mode 100644 contracts/src/tests/utils.cairo create mode 100755 scripts/deploy.sh create mode 100755 scripts/invoke.sh diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..fb2c737e --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +CONTRACT_SRC=${CONTRACT_SRC:-$(pwd)/contracts/target/dev/starknet_ibc_Transfer.contract_class.json} +RPC_URL=https://starknet-sepolia.public.blastapi.io/rpc/v0_7 +ACCOUNT_SRC="${HOME}/.starkli-wallets/deployer/account.json" +KEYSTORE_SRC="${HOME}/.starkli-wallets/deployer/keystore.json" +KEYSTORE_PASS= + +CONTRACT_ADDRESS="" +CLASS_HASH="" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..e524a1fa --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Tests +on: + pull_request: + paths: + - .github/workflows/tests.yaml + - contracts/** + - justfile + + push: + tags: + - v[0-9]+.* + branches: + - "release/*" + - main + +jobs: + test-contracts: + name: Test Cairo Contracts + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - name: Install Scarb + uses: software-mansion/setup-scarb@v1 + with: + scarb-version: "2.6.5" + - name: Install Just + uses: extractions/setup-just@v1 + - name: Run Tests + run: just test-contracts diff --git a/.gitignore b/.gitignore index 3dc05158..2f45da14 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ corelib/ target/ +.env + # vscode .vscode/ \ No newline at end of file diff --git a/contracts/src/apps/transfer/component.cairo b/contracts/src/apps/transfer/component.cairo index 4e9b4df9..7d2e8a30 100644 --- a/contracts/src/apps/transfer/component.cairo +++ b/contracts/src/apps/transfer/component.cairo @@ -16,7 +16,7 @@ pub mod ICS20TransferComponent { use starknet::syscalls::deploy_syscall; use starknet_ibc::apps::transfer::errors::ICS20Errors; use starknet_ibc::apps::transfer::interface::{ - ITransfer, ITransferValidationContext, ITransferExecutionContext + ITransfer, ITransferrable, ITransferValidationContext, ITransferExecutionContext }; use starknet_ibc::apps::transfer::types::{MsgTransfer, PrefixedCoin, Memo, MAXIMUM_MEMO_LENGTH}; use starknet_ibc::core::types::{PortId, ChannelId}; @@ -25,6 +25,8 @@ pub mod ICS20TransferComponent { struct Storage { salt: felt252, governor: ContractAddress, + send_capability: bool, + receive_capability: bool, registered_tokens: LegacyMap::, minted_tokens: LegacyMap::, } @@ -46,28 +48,99 @@ pub mod ICS20TransferComponent { #[embeddable_as(Transfer)] impl TransferImpl< - TContractState, +HasComponent, +Drop + TContractState, + +HasComponent, + +ERC20ABI, + +Drop > of ITransfer> { - fn send_transfer(ref self: ComponentState, msg: MsgTransfer) {} + fn send_transfer(ref self: ComponentState, msg: MsgTransfer) { + self.can_send(); + + let is_sender_chain_source = self.is_sender_chain_source(msg.packet_data.token.denom); + + let is_receiver_chain_source = self + .is_receiver_chain_source(msg.packet_data.token.denom); + + assert( + !is_sender_chain_source && !is_receiver_chain_source, + ICS20Errors::INVALID_TOKEN_NAME + ); + + if is_sender_chain_source { + self + .escrow_validate( + msg.packet_data.sender.clone(), + msg.port_id_on_a.clone(), + msg.chan_id_on_a.clone(), + msg.packet_data.token.clone(), + msg.packet_data.memo.clone(), + ); + + self + .escrow_execute( + msg.packet_data.sender.clone(), + msg.port_id_on_a.clone(), + msg.chan_id_on_a.clone(), + msg.packet_data.token.clone(), + msg.packet_data.memo.clone(), + ); + } + + if is_receiver_chain_source { + self + .burn_validate( + msg.packet_data.sender.clone(), + msg.packet_data.token.clone(), + msg.packet_data.memo.clone(), + ); + + self + .burn_execute( + msg.packet_data.sender.clone(), + msg.packet_data.token.clone(), + msg.packet_data.memo.clone(), + ); + } + } + fn register_token( ref self: ComponentState, token_name: felt252, token_address: ContractAddress ) { let governor = self.governor.read(); - let maybe_governor = get_caller_address(); - assert(maybe_governor == governor, ICS20Errors::UNAUTHORIZED_REGISTAR); + + assert(governor == get_caller_address(), ICS20Errors::UNAUTHORIZED_REGISTAR); + + assert(token_name.is_non_zero(), ICS20Errors::ZERO_TOKEN_NAME); let registered_token_address: ContractAddress = self.registered_tokens.read(token_name); - assert(registered_token_address.is_non_zero(), ICS20Errors::ALREADY_LISTED_TOKEN); - assert(registered_token_address == token_address, ICS20Errors::ALREADY_LISTED_TOKEN); + + assert(registered_token_address.is_zero(), ICS20Errors::ALREADY_LISTED_TOKEN); + + assert(token_address.is_non_zero(), ICS20Errors::ZERO_TOKEN_ADDRESS); self.registered_tokens.write(token_name, token_address); } } + #[embeddable_as(TransferrableImpl)] + pub impl Transferrable< + TContractState, +HasComponent, +Drop + > of ITransferrable> { + fn can_send(self: @ComponentState) { + let send_capability = self.send_capability.read(); + assert(send_capability, ICS20Errors::NO_SEND_CAPABILITY); + } + fn can_receive(self: @ComponentState) { + let receive_capability = self.receive_capability.read(); + assert(receive_capability, ICS20Errors::NO_RECEIVE_CAPABILITY); + } + } + + #[embeddable_as(TransferValidationImpl)] - impl TransferValidationContext< + pub impl TransferValidationContext< TContractState, +HasComponent, +ERC20ABI, @@ -119,10 +192,10 @@ pub mod ICS20TransferComponent { fn escrow_execute( ref self: ComponentState, from_address: ContractAddress, - port_id: felt252, - channel_id: felt252, + port_id: PortId, + channel_id: ChannelId, coin: PrefixedCoin, - memo: ByteArray, + memo: Memo, ) { let to_address = get_contract_address(); let mut contract = self.get_contract_mut(); @@ -150,12 +223,26 @@ pub mod ICS20TransferComponent { } #[generate_trait] - pub impl TransferInternalImpl< + pub(crate) impl TransferInternalImpl< TContractState, +HasComponent, +ERC20ABI, +Drop, > of TransferInternalTrait { + fn initializer(ref self: ComponentState) { + self.governor.write(get_caller_address()); + self.send_capability.write(true); + self.receive_capability.write(true); + } + + fn is_sender_chain_source(self: @ComponentState, denom: felt252) -> bool { + self.registered_tokens.read(denom).is_zero() + } + + fn is_receiver_chain_source(self: @ComponentState, denom: felt252) -> bool { + self.minted_tokens.read(denom).is_zero() + } + fn create_token(ref self: ComponentState) -> ContractAddress { // unimplemented! > Dummy value to pass the type check 0.try_into().unwrap() diff --git a/contracts/src/apps/transfer/errors.cairo b/contracts/src/apps/transfer/errors.cairo index ba9d4dcd..9383c10f 100644 --- a/contracts/src/apps/transfer/errors.cairo +++ b/contracts/src/apps/transfer/errors.cairo @@ -1,6 +1,10 @@ pub mod ICS20Errors { - pub const ALREADY_LISTED_TOKEN: felt252 = 'ICS20: token is already listed'; + pub const NO_SEND_CAPABILITY: felt252 = 'ICS20: No send capability'; + pub const NO_RECEIVE_CAPABILITY: felt252 = 'ICS20: No receive capability'; + pub const ZERO_TOKEN_NAME: felt252 = 'ICS20: token name is 0'; pub const ZERO_TOKEN_ADDRESS: felt252 = 'ICS20: token address is 0'; + pub const ALREADY_LISTED_TOKEN: felt252 = 'ICS20: token is already listed'; + pub const INVALID_TOKEN_NAME: felt252 = 'ICS20: token name is invalid'; pub const UNAUTHORIZED_REGISTAR: felt252 = 'ICS20: unauthorized registrar'; pub const MAXIMUM_MEMO_LENGTH: felt252 = 'ICS20: memo exceeds max length'; pub const INSUFFICIENT_BALANCE: felt252 = 'ICS20: insufficient balance'; diff --git a/contracts/src/apps/transfer/interface.cairo b/contracts/src/apps/transfer/interface.cairo index 7342c344..8d8ddafa 100644 --- a/contracts/src/apps/transfer/interface.cairo +++ b/contracts/src/apps/transfer/interface.cairo @@ -10,6 +10,12 @@ pub trait ITransfer { ); } +#[starknet::interface] +pub trait ITransferrable { + fn can_send(self: @TContractState); + fn can_receive(self: @TContractState); +} + #[starknet::interface] pub trait ITransferValidationContext { fn escrow_validate( @@ -38,10 +44,10 @@ pub trait ITransferExecutionContext { fn escrow_execute( ref self: TContractState, from_address: ContractAddress, - port_id: felt252, - channel_id: felt252, + port_id: PortId, + channel_id: ChannelId, coin: PrefixedCoin, - memo: ByteArray, + memo: Memo, ); fn unescrow_execute( ref self: TContractState, diff --git a/contracts/src/apps/transfer/types.cairo b/contracts/src/apps/transfer/types.cairo index 0ea7725c..ae1ba5c7 100644 --- a/contracts/src/apps/transfer/types.cairo +++ b/contracts/src/apps/transfer/types.cairo @@ -5,14 +5,14 @@ use starknet_ibc::core::types::{PortId, ChannelId}; /// the `MaximumMemoLength` in the `ibc-go`. pub(crate) const MAXIMUM_MEMO_LENGTH: u32 = 32768; -#[derive(Drop, Serde, Store)] +#[derive(Clone, Debug, Drop, Serde, Store)] pub struct MsgTransfer { pub port_id_on_a: PortId, pub chan_id_on_a: ChannelId, pub packet_data: PacketData, } -#[derive(Drop, Serde, Store)] +#[derive(Clone, Debug, Drop, Serde, Store)] pub struct PacketData { pub token: PrefixedCoin, pub sender: ContractAddress, @@ -20,13 +20,13 @@ pub struct PacketData { pub memo: Memo, } -#[derive(Drop, Serde, Store)] +#[derive(Clone, Debug, Drop, Serde, Store)] pub struct PrefixedCoin { pub denom: felt252, pub amount: u256, } -#[derive(Drop, Serde, Store)] +#[derive(Clone, Debug, Drop, Serde, Store)] pub struct Memo { pub memo: ByteArray, } diff --git a/contracts/src/contract.cairo b/contracts/src/contract.cairo index 62d4f23f..aa1fe25a 100644 --- a/contracts/src/contract.cairo +++ b/contracts/src/contract.cairo @@ -13,6 +13,7 @@ pub(crate) mod Transfer { #[abi(embed_v0)] impl ICS20TransferImpl = ICS20TransferComponent::Transfer; + impl TransferreableImpl = ICS20TransferComponent::Transferrable; impl TransferValidationImpl = ICS20TransferComponent::TransferValidationImpl; impl TransferExecutionImpl = ICS20TransferComponent::TransferExecutionImpl; impl TransferInternalImpl = ICS20TransferComponent::TransferInternalImpl; @@ -35,40 +36,7 @@ pub(crate) mod Transfer { } #[constructor] - fn constructor( - ref self: ContractState, - name: ByteArray, - symbol: ByteArray, - fixed_supply: u256, - recipient: ContractAddress, - owner: ContractAddress - ) { - self.erc20.initializer(name, symbol); - } -} - -#[cfg(test)] -mod tests { - use core::starknet::SyscallResultTrait; - use starknet::ContractAddress; - use starknet::contract_address_const; - use starknet::syscalls::deploy_syscall; - use starknet_ibc::apps::transfer::interface::{ITransferDispatcher, ITransferDispatcherTrait,}; - use super::Transfer; - - fn deploy() -> (ITransferDispatcher, ContractAddress) { - let recipient: ContractAddress = contract_address_const::<'sender'>(); - - let (contract_address, _) = deploy_syscall( - Transfer::TEST_CLASS_HASH.try_into().unwrap(), recipient.into(), array![0].span(), false - ) - .unwrap_syscall(); - - (ITransferDispatcher { contract_address }, contract_address) - } - - #[test] - fn test_transfer() { - deploy(); + fn constructor(ref self: ContractState,) { + self.transfer.initializer(); } } diff --git a/contracts/src/core/types.cairo b/contracts/src/core/types.cairo index edfe12cc..e0cac7ef 100644 --- a/contracts/src/core/types.cairo +++ b/contracts/src/core/types.cairo @@ -1,12 +1,12 @@ use starknet::ContractAddress; use starknet::Store; -#[derive(Drop, Serde, Store)] +#[derive(Clone, Debug, Drop, Serde, Store)] pub struct ChannelId { channel_id: felt252, } -#[derive(Drop, Serde, Store)] +#[derive(Clone, Debug, Drop, Serde, Store)] pub struct PortId { port_id: felt252, } diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index f7961cf3..c9c56de0 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,3 +1,4 @@ pub mod apps; pub mod contract; pub mod core; +pub mod tests; diff --git a/contracts/src/tests.cairo b/contracts/src/tests.cairo new file mode 100644 index 00000000..652d8355 --- /dev/null +++ b/contracts/src/tests.cairo @@ -0,0 +1,4 @@ +#[cfg(test)] +mod transfer; + +mod utils; diff --git a/contracts/src/tests/transfer.cairo b/contracts/src/tests/transfer.cairo new file mode 100644 index 00000000..c94e76e6 --- /dev/null +++ b/contracts/src/tests/transfer.cairo @@ -0,0 +1,51 @@ +use core::starknet::SyscallResultTrait; +use core::traits::TryInto; +use openzeppelin::tests::utils::deploy; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::syscalls::deploy_syscall; +use starknet::testing; +use starknet_ibc::apps::transfer::component::ICS20TransferComponent::TransferInternalTrait; +use starknet_ibc::apps::transfer::component::ICS20TransferComponent; +use starknet_ibc::apps::transfer::interface::ITransfer; +use starknet_ibc::apps::transfer::interface::{ITransferDispatcher, ITransferDispatcherTrait}; +use starknet_ibc::contract::Transfer; +use starknet_ibc::tests::utils::{PUBKEY, TOKEN_NAME, SALT, OWNER, pubkey, owner}; + +type ComponentState = ICS20TransferComponent::ComponentState; + +fn component_state() -> ComponentState { + ICS20TransferComponent::component_state_for_testing() +} + +fn basic_setup() -> ComponentState { + let mut state = component_state(); + testing::set_caller_address(owner()); + state.initializer(); + state +} + +fn setup() -> ComponentState { + let mut state = basic_setup(); + state.register_token(TOKEN_NAME, pubkey()); + state +} + +#[test] +fn test_deploy() { + let contract_address = deploy(Transfer::TEST_CLASS_HASH, array![]); + ITransferDispatcher { contract_address }.register_token(TOKEN_NAME, owner()); +} + +#[test] +fn test_register_token() { + setup(); +} + +#[test] +#[should_panic(expected: ('ICS20: token is already listed',))] +fn test_register_token_twice() { + let mut state = setup(); + state.register_token(TOKEN_NAME, pubkey()); +} + diff --git a/contracts/src/tests/utils.cairo b/contracts/src/tests/utils.cairo new file mode 100644 index 00000000..37e837c6 --- /dev/null +++ b/contracts/src/tests/utils.cairo @@ -0,0 +1,19 @@ +use starknet::ContractAddress; +use starknet::contract_address_const; + +pub(crate) const TOKEN_NAME: felt252 = 'ETH'; +pub(crate) const DECIMALS: u8 = 18_u8; +pub(crate) const SUPPLY: u256 = 2000; +pub(crate) const SALT: felt252 = 'SALT'; +pub(crate) const OWNER: felt252 = 'OWNER'; +pub(crate) const PUBKEY: felt252 = + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; + +pub(crate) fn owner() -> ContractAddress { + contract_address_const::() +} + +pub(crate) fn pubkey() -> ContractAddress { + contract_address_const::() +} + diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 00000000..a5908bd5 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,83 @@ +#!/bin/bash +set -euo pipefail + +source .env + +# use ggrep for macOS, and grep for Linux +case "$OSTYPE" in + darwin*) GREP="ggrep" ;; + linux-gnu*) GREP="grep" ;; + *) echo "Unknown OS: $OSTYPE" && exit 1 ;; +esac + +version() { + starkli --version 1>&2 + scarb --version 1>&2 +} + +# build the contract +build() { + version + + cd "$(dirname "$0")/../contracts" + + output=$(scarb build 2>&1) + + if [[ $output == *"Error"* ]]; then + echo "Error: $output" + exit 1 + fi +} + +# declare the contract +declare() { + build + + output=$( + starkli declare --watch $CONTRACT_SRC \ + --rpc $RPC_URL \ + --account $ACCOUNT_SRC \ + --keystore $KEYSTORE_SRC \ + --keystore-password $KEYSTORE_PASS \ + 2>&1 | tee /dev/tty + ) + + if [[ $output == *"Error"* ]]; then + echo "Error: $output" + exit 1 + fi + + address=$(echo -e "$output" | "$GREP" -oP '0x[0-9a-fA-F]+' | tail -n 1) + + echo $address +} + +# deploy the contract +deploy() { + if [[ $CLASS_HASH == "" ]]; then + class_hash=$(declare) + else + class_hash=$CLASS_HASH + fi + + output=$( + starkli deploy --not-unique \ + --watch $class_hash \ + --rpc $RPC_URL \ + --account $ACCOUNT_SRC \ + --keystore $KEYSTORE_SRC \ + --keystore-password $KEYSTORE_PASS \ + 2>&1 | tee /dev/tty + ) + + if [[ $output == *"Error"* ]]; then + echo "Error: $output" + exit 1 + fi + + address=$(echo -e "$output" | "$GREP" -oP '0x[0-9a-fA-F]+' | tail -n 1) + + echo $address +} + +deploy diff --git a/scripts/invoke.sh b/scripts/invoke.sh new file mode 100755 index 00000000..0764679d --- /dev/null +++ b/scripts/invoke.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -euo pipefail + +source ./scripts/deploy.sh + +# invoke the contract +invoke() { + if [[ $CONTRACT_ADDRESS == "" ]]; then + address=$(deploy) + else + address=$CONTRACT_ADDRESS + fi + + output=$( + starkli invoke $address register_token 1 0x4e91934ce777f807d6bc90fd3b06e1fa49e942ab1fb70a072ca1ad61dc2998d \ + --rpc $RPC_URL \ + --account $ACCOUNT_SRC \ + --keystore $KEYSTORE_SRC \ + --keystore-password $KEYSTORE_PASS \ + 2>&1 | tee /dev/tty + ) + + if [[ $output == *"Error"* ]]; then + echo "Error: $output" + exit 1 + fi + + echo $output +} + +invoke