From e55efa451ded2ee30d4bb2beb555a4bea4159ab3 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Mon, 22 Jul 2024 19:04:02 -0700 Subject: [PATCH 01/15] feat: define interfaces + initial implementation for escrow_* methods --- contracts/src/apps/transfer.cairo | 1 + contracts/src/apps/transfer/component.cairo | 100 +++++++++++++----- contracts/src/apps/transfer/errors.cairo | 7 ++ contracts/src/apps/transfer/interface.cairo | 54 +++++++++- contracts/src/apps/transfer/types.cairo | 31 +++--- .../transfer.cairo => contract.cairo} | 6 +- contracts/src/lib.cairo | 1 + 7 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 contracts/src/apps/transfer/errors.cairo rename contracts/src/{contracts/transfer.cairo => contract.cairo} (91%) diff --git a/contracts/src/apps/transfer.cairo b/contracts/src/apps/transfer.cairo index ccc935ca..16f4b560 100644 --- a/contracts/src/apps/transfer.cairo +++ b/contracts/src/apps/transfer.cairo @@ -1,3 +1,4 @@ pub mod component; +pub mod errors; pub mod interface; pub mod types; diff --git a/contracts/src/apps/transfer/component.cairo b/contracts/src/apps/transfer/component.cairo index da6ab4dc..4e9b4df9 100644 --- a/contracts/src/apps/transfer/component.cairo +++ b/contracts/src/apps/transfer/component.cairo @@ -1,22 +1,32 @@ #[starknet::component] pub mod ICS20TransferComponent { use core::array::ArrayTrait; + use core::num::traits::zero::Zero; use core::option::OptionTrait; use core::starknet::SyscallResultTrait; use core::traits::TryInto; + use openzeppelin::token::erc20::ERC20Component::{InternalImpl, InternalTrait}; use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::interface::ERC20ABI; + use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use starknet::ClassHash; use starknet::ContractAddress; use starknet::get_caller_address; use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; - use starknet_ibc::apps::transfer::types::{PrefixedCoin, Memo}; + use starknet_ibc::apps::transfer::errors::ICS20Errors; + use starknet_ibc::apps::transfer::interface::{ + ITransfer, ITransferValidationContext, ITransferExecutionContext + }; + use starknet_ibc::apps::transfer::types::{MsgTransfer, PrefixedCoin, Memo, MAXIMUM_MEMO_LENGTH}; use starknet_ibc::core::types::{PortId, ChannelId}; #[storage] struct Storage { salt: felt252, - tokens: LegacyMap::, + governor: ContractAddress, + registered_tokens: LegacyMap::, + minted_tokens: LegacyMap::, } #[event] @@ -30,80 +40,122 @@ pub mod ICS20TransferComponent { sender: ContractAddress, receiver: ContractAddress, amount: u64, - denom: felt252, + denom: ByteArray, memo: Memo, } - #[generate_trait] - pub impl TransferValidationImpl< + #[embeddable_as(Transfer)] + impl TransferImpl< + TContractState, +HasComponent, +Drop + > of ITransfer> { + fn send_transfer(ref self: ComponentState, msg: MsgTransfer) {} + 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); + + 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); + + self.registered_tokens.write(token_name, token_address); + } + } + + #[embeddable_as(TransferValidationImpl)] + impl TransferValidationContext< TContractState, +HasComponent, - impl ERC20MixinImpl: ERC20Component::HasComponent - > of TransferValidationTrait { + +ERC20ABI, + +Drop, + > of ITransferValidationContext> { fn escrow_validate( self: @ComponentState, - from_account: ContractAddress, + from_address: ContractAddress, port_id: PortId, channel_id: ChannelId, coin: PrefixedCoin, memo: Memo, - ) {} + ) { + assert(memo.memo.len() > MAXIMUM_MEMO_LENGTH, ICS20Errors::MAXIMUM_MEMO_LENGTH); + + let token_address = self.registered_tokens.read(coin.denom); + let balance = ERC20ABIDispatcher { contract_address: token_address } + .balance_of(from_address); + assert(balance > coin.amount, ICS20Errors::INSUFFICIENT_BALANCE); + } + fn unescrow_validate( self: @ComponentState, - to_account: ContractAddress, + to_address: ContractAddress, port_id: PortId, channel_id: ChannelId, coin: PrefixedCoin, ) {} + fn mint_validate( - self: @ComponentState, account: ContractAddress, coin: PrefixedCoin, + self: @ComponentState, address: ContractAddress, coin: PrefixedCoin, ) {} + fn burn_validate( self: @ComponentState, - account: ContractAddress, + address: ContractAddress, coin: PrefixedCoin, memo: Memo, ) {} } - #[generate_trait] - pub impl TransferExecutionImpl< + #[embeddable_as(TransferExecutionImpl)] + pub impl TransferExecutionContext< TContractState, +HasComponent, - impl ERC20MixinImpl: ERC20Component::HasComponent - > of TransferExecutionTrait { + +ERC20ABI, + +Drop, + > of ITransferExecutionContext> { fn escrow_execute( ref self: ComponentState, - from_account: ContractAddress, + from_address: ContractAddress, port_id: felt252, channel_id: felt252, coin: PrefixedCoin, memo: ByteArray, - ) {} + ) { + let to_address = get_contract_address(); + let mut contract = self.get_contract_mut(); + contract.transfer_from(from_address, to_address, coin.amount); + } + fn unescrow_execute( ref self: ComponentState, - to_account: ContractAddress, + to_address: ContractAddress, port_id: PortId, channel_id: ChannelId, coin: PrefixedCoin, ) {} + fn mint_execute( - ref self: ComponentState, account: ContractAddress, coin: PrefixedCoin, + ref self: ComponentState, address: ContractAddress, coin: PrefixedCoin, ) {} + fn burn_execute( ref self: ComponentState, - account: ContractAddress, + address: ContractAddress, coin: PrefixedCoin, memo: Memo, ) {} } #[generate_trait] - pub impl InternalImpl< + pub impl TransferInternalImpl< TContractState, +HasComponent, - impl ERC20MixinImpl: ERC20Component::HasComponent - > of InternalTrait { + +ERC20ABI, + +Drop, + > of TransferInternalTrait { 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 new file mode 100644 index 00000000..ba9d4dcd --- /dev/null +++ b/contracts/src/apps/transfer/errors.cairo @@ -0,0 +1,7 @@ +pub mod ICS20Errors { + pub const ALREADY_LISTED_TOKEN: felt252 = 'ICS20: token is already listed'; + pub const ZERO_TOKEN_ADDRESS: felt252 = 'ICS20: token address is 0'; + 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 534fb96f..7342c344 100644 --- a/contracts/src/apps/transfer/interface.cairo +++ b/contracts/src/apps/transfer/interface.cairo @@ -1,8 +1,58 @@ use starknet::ContractAddress; -use starknet_ibc::apps::transfer::types::MsgTransfer; +use starknet_ibc::apps::transfer::types::{MsgTransfer, PrefixedCoin, Memo}; +use starknet_ibc::core::types::{PortId, ChannelId}; #[starknet::interface] pub trait ITransfer { - fn send_transfer(self: @TContractState, msg: MsgTransfer,); + fn send_transfer(ref self: TContractState, msg: MsgTransfer); + fn register_token( + ref self: TContractState, token_name: felt252, token_address: ContractAddress + ); +} + +#[starknet::interface] +pub trait ITransferValidationContext { + fn escrow_validate( + self: @TContractState, + from_address: ContractAddress, + port_id: PortId, + channel_id: ChannelId, + coin: PrefixedCoin, + memo: Memo, + ); + fn unescrow_validate( + self: @TContractState, + to_address: ContractAddress, + port_id: PortId, + channel_id: ChannelId, + coin: PrefixedCoin, + ); + fn mint_validate(self: @TContractState, address: ContractAddress, coin: PrefixedCoin,); + fn burn_validate( + self: @TContractState, address: ContractAddress, coin: PrefixedCoin, memo: Memo, + ); +} + +#[starknet::interface] +pub trait ITransferExecutionContext { + fn escrow_execute( + ref self: TContractState, + from_address: ContractAddress, + port_id: felt252, + channel_id: felt252, + coin: PrefixedCoin, + memo: ByteArray, + ); + fn unescrow_execute( + ref self: TContractState, + to_address: ContractAddress, + port_id: PortId, + channel_id: ChannelId, + coin: PrefixedCoin, + ); + fn mint_execute(ref self: TContractState, address: ContractAddress, coin: PrefixedCoin,); + fn burn_execute( + ref self: TContractState, address: ContractAddress, coin: PrefixedCoin, memo: Memo, + ); } diff --git a/contracts/src/apps/transfer/types.cairo b/contracts/src/apps/transfer/types.cairo index 7a271a38..0ea7725c 100644 --- a/contracts/src/apps/transfer/types.cairo +++ b/contracts/src/apps/transfer/types.cairo @@ -1,28 +1,33 @@ use starknet::ContractAddress; use starknet_ibc::core::types::{PortId, ChannelId}; +/// Maximum memo length allowed for ICS-20 transfers. This bound corresponds to +/// the `MaximumMemoLength` in the `ibc-go`. +pub(crate) const MAXIMUM_MEMO_LENGTH: u32 = 32768; + #[derive(Drop, Serde, Store)] pub struct MsgTransfer { - port_id_on_a: PortId, - chan_id_on_a: ChannelId, - packet_data: PacketData, + pub port_id_on_a: PortId, + pub chan_id_on_a: ChannelId, + pub packet_data: PacketData, } #[derive(Drop, Serde, Store)] -pub struct PrefixedCoin { - denom: ByteArray, - amount: u64, +pub struct PacketData { + pub token: PrefixedCoin, + pub sender: ContractAddress, + pub receiver: ContractAddress, + pub memo: Memo, } #[derive(Drop, Serde, Store)] -pub struct Memo { - memo: ByteArray, +pub struct PrefixedCoin { + pub denom: felt252, + pub amount: u256, } #[derive(Drop, Serde, Store)] -pub struct PacketData { - pub token: PrefixedCoin, - pub sender: ContractAddress, - pub receiver: ContractAddress, - pub memo: Memo, +pub struct Memo { + pub memo: ByteArray, } + diff --git a/contracts/src/contracts/transfer.cairo b/contracts/src/contract.cairo similarity index 91% rename from contracts/src/contracts/transfer.cairo rename to contracts/src/contract.cairo index 9a900d5b..62d4f23f 100644 --- a/contracts/src/contracts/transfer.cairo +++ b/contracts/src/contract.cairo @@ -11,9 +11,11 @@ pub(crate) mod Transfer { impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; impl ERC20InternalImpl = ERC20Component::InternalImpl; + #[abi(embed_v0)] + impl ICS20TransferImpl = ICS20TransferComponent::Transfer; impl TransferValidationImpl = ICS20TransferComponent::TransferValidationImpl; impl TransferExecutionImpl = ICS20TransferComponent::TransferExecutionImpl; - impl TransferInternalImpl = ICS20TransferComponent::InternalImpl; + impl TransferInternalImpl = ICS20TransferComponent::TransferInternalImpl; #[storage] struct Storage { @@ -51,8 +53,8 @@ mod tests { use starknet::ContractAddress; use starknet::contract_address_const; use starknet::syscalls::deploy_syscall; - use starknet_ibc::Transfer; use starknet_ibc::apps::transfer::interface::{ITransferDispatcher, ITransferDispatcherTrait,}; + use super::Transfer; fn deploy() -> (ITransferDispatcher, ContractAddress) { let recipient: ContractAddress = contract_address_const::<'sender'>(); diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index fad72ee8..f7961cf3 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,2 +1,3 @@ pub mod apps; +pub mod contract; pub mod core; From bfc3646961f4ca61d827b6887514dd23a404ab69 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Tue, 23 Jul 2024 19:38:29 -0700 Subject: [PATCH 02/15] 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 From 7b6d5a277f5fe2a8bd97214731359b25d53c1e76 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Thu, 25 Jul 2024 22:29:29 -0700 Subject: [PATCH 03/15] feat: revise contract + implement all methods + successful escrowing test --- .env.example | 5 +- contracts/src/apps/transfer/component.cairo | 389 ++++++++++++------ contracts/src/apps/transfer/errors.cairo | 10 +- contracts/src/apps/transfer/interface.cairo | 62 +-- contracts/src/apps/transfer/types.cairo | 78 +++- contracts/src/core/types.cairo | 20 +- contracts/src/lib.cairo | 2 +- contracts/src/presets.cairo | 8 + contracts/src/presets/erc20.cairo | 50 +++ .../transfer.cairo} | 19 +- contracts/src/tests/transfer.cairo | 85 ++-- contracts/src/tests/utils.cairo | 76 +++- scripts/deploy.sh | 4 +- scripts/invoke.sh | 2 +- 14 files changed, 553 insertions(+), 257 deletions(-) create mode 100644 contracts/src/presets.cairo create mode 100644 contracts/src/presets/erc20.cairo rename contracts/src/{contract.cairo => presets/transfer.cairo} (57%) diff --git a/.env.example b/.env.example index fb2c737e..fbe37ca8 100644 --- a/.env.example +++ b/.env.example @@ -4,5 +4,6 @@ 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 +ERC20_CLASS_HASH="" +ICS20_CLASS_HASH="" +CONTRACT_ADDRESS="" \ No newline at end of file diff --git a/contracts/src/apps/transfer/component.cairo b/contracts/src/apps/transfer/component.cairo index 7d2e8a30..cfc1a79a 100644 --- a/contracts/src/apps/transfer/component.cairo +++ b/contracts/src/apps/transfer/component.cairo @@ -1,126 +1,233 @@ +use core::array::ArrayTrait; +use core::serde::Serde; +use core::to_byte_array::FormatAsByteArray; +use core::traits::TryInto; +use starknet::ContractAddress; +use starknet_ibc::apps::transfer::errors::ICS20Errors; + #[starknet::component] pub mod ICS20TransferComponent { use core::array::ArrayTrait; - use core::num::traits::zero::Zero; + use core::clone::Clone; + use core::num::traits::Zero; use core::option::OptionTrait; use core::starknet::SyscallResultTrait; use core::traits::TryInto; - use openzeppelin::token::erc20::ERC20Component::{InternalImpl, InternalTrait}; - use openzeppelin::token::erc20::ERC20Component; - use openzeppelin::token::erc20::interface::ERC20ABI; use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::utils::serde::SerializedAppend; use starknet::ClassHash; use starknet::ContractAddress; use starknet::get_caller_address; use starknet::get_contract_address; use starknet::syscalls::deploy_syscall; use starknet_ibc::apps::transfer::errors::ICS20Errors; - use starknet_ibc::apps::transfer::interface::{ - ITransfer, ITransferrable, ITransferValidationContext, ITransferExecutionContext + use starknet_ibc::apps::transfer::interface::{ISendTransfer, IRecvPacket, ITransferrable}; + use starknet_ibc::apps::transfer::types::{ + MsgTransfer, Packet, Token, Denom, DenomTrait, Memo, MAXIMUM_MEMO_LENGTH }; - use starknet_ibc::apps::transfer::types::{MsgTransfer, PrefixedCoin, Memo, MAXIMUM_MEMO_LENGTH}; use starknet_ibc::core::types::{PortId, ChannelId}; #[storage] struct Storage { + erc20_class_hash: ClassHash, salt: felt252, governor: ContractAddress, send_capability: bool, receive_capability: bool, - registered_tokens: LegacyMap::, - minted_tokens: LegacyMap::, + minted_token_name_to_address: LegacyMap, + minted_token_address_to_name: LegacyMap, } #[event] - #[derive(Drop, starknet::Event)] + #[derive(Debug, Drop, starknet::Event)] pub enum Event { TransferEvent: TransferEvent, + RecvEvent: RecvEvent, } - #[derive(Drop, Serde, starknet::Event)] + #[derive(Debug, Drop, Serde, starknet::Event)] pub struct TransferEvent { sender: ContractAddress, receiver: ContractAddress, - amount: u64, - denom: ByteArray, + amount: u256, + denom: Denom, memo: Memo, } - #[embeddable_as(Transfer)] - impl TransferImpl< - TContractState, - +HasComponent, - +ERC20ABI, - +Drop - > of ITransfer> { - fn send_transfer(ref self: ComponentState, msg: MsgTransfer) { - self.can_send(); + #[derive(Debug, Drop, Serde, starknet::Event)] + pub struct RecvEvent { + sender: ContractAddress, + receiver: ContractAddress, + denom: Denom, + amount: u256, + memo: Memo, + success: bool, + } + + #[embeddable_as(SendTransfer)] + impl SendTransferImpl< + TContractState, +HasComponent, +Drop + > of ISendTransfer> { + fn send_validate(self: @ComponentState, msg: MsgTransfer) { + self._send_validate(msg); + } - let is_sender_chain_source = self.is_sender_chain_source(msg.packet_data.token.denom); + fn send_execute(ref self: ComponentState, msg: MsgTransfer) { + self._send_execute(msg); + } + } - let is_receiver_chain_source = self - .is_receiver_chain_source(msg.packet_data.token.denom); + #[generate_trait] + impl SendTransferInternalImpl< + TContractState, +HasComponent, +Drop + > of SendTransferInternal { + fn _send_validate(self: @ComponentState, msg: MsgTransfer) -> Denom { + self.can_send(); + assert(msg.packet_data.sender.is_non_zero(), ICS20Errors::INVALID_SENDER); + assert(!msg.packet_data.token.denom.is_zero(), ICS20Errors::INVALID_DENOM); + assert(msg.packet_data.token.amount.is_non_zero(), ICS20Errors::ZERO_AMOUNT); assert( - !is_sender_chain_source && !is_receiver_chain_source, - ICS20Errors::INVALID_TOKEN_NAME + msg.packet_data.memo.memo.len() < MAXIMUM_MEMO_LENGTH, + ICS20Errors::MAXIMUM_MEMO_LENGTH ); - 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(), - ); + match @msg.packet_data.token.denom { + Denom::Native(_) => { + 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(), + ); + }, + Denom::IBC(_) => { + self + .burn_validate( + msg.packet_data.sender.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(), - ); - } + msg.packet_data.token.denom } - fn register_token( - ref self: ComponentState, - token_name: felt252, - token_address: ContractAddress - ) { - let governor = self.governor.read(); + fn _send_execute(ref self: ComponentState, msg: MsgTransfer) -> Denom { + let denom = self._send_validate(msg.clone()); + + match @denom { + Denom::Native(_) => { + 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(), + ); + }, + Denom::IBC(_) => { + self + .burn_execute( + msg.packet_data.sender.clone(), + msg.packet_data.token.clone(), + msg.packet_data.memo.clone(), + ); + } + } - assert(governor == get_caller_address(), ICS20Errors::UNAUTHORIZED_REGISTAR); + self + .emit( + TransferEvent { + sender: msg.packet_data.sender.clone(), + receiver: msg.packet_data.receiver.clone(), + amount: msg.packet_data.token.amount, + denom: denom.clone(), + memo: msg.packet_data.memo.clone(), + } + ); + + denom + } + } - assert(token_name.is_non_zero(), ICS20Errors::ZERO_TOKEN_NAME); + #[embeddable_as(RecvPacket)] + impl RecvPacketImpl< + TContractState, +HasComponent, +Drop + > of IRecvPacket> { + fn recv_validate(self: @ComponentState, packet: Packet) { + self._recv_validate(packet); + } - let registered_token_address: ContractAddress = self.registered_tokens.read(token_name); + fn recv_execute(ref self: ComponentState, packet: Packet) { + self._recv_execute(packet); + } + } - assert(registered_token_address.is_zero(), ICS20Errors::ALREADY_LISTED_TOKEN); + #[generate_trait] + impl RecvPacketInernalImpl< + TContractState, +HasComponent, +Drop + > of RecvPacketInternal { + fn _recv_validate(self: @ComponentState, packet: Packet) -> Denom { + self.can_receive(); + + assert(packet.data.receiver.is_non_zero(), ICS20Errors::INVALID_RECEIVEER); + assert(!packet.data.token.denom.is_zero(), ICS20Errors::INVALID_DENOM); + assert(packet.data.token.amount.is_non_zero(), ICS20Errors::ZERO_AMOUNT); + + match @packet.data.token.denom { + Denom::Native(_) => { + self + .unescrow_validate( + packet.data.receiver.clone(), + packet.port_id_on_a.clone(), + packet.chan_id_on_a.clone(), + packet.data.token.clone(), + ); + }, + Denom::IBC(_) => { + self.mint_validate(packet.data.receiver.clone(), packet.data.token.clone(),); + } + } - assert(token_address.is_non_zero(), ICS20Errors::ZERO_TOKEN_ADDRESS); + packet.data.token.denom + } - self.registered_tokens.write(token_name, token_address); + fn _recv_execute(ref self: ComponentState, packet: Packet) -> Denom { + let denom = self._recv_validate(packet.clone()); + + match @denom { + Denom::Native(_) => { + self + .unescrow_execute( + packet.data.receiver.clone(), + packet.port_id_on_a.clone(), + packet.chan_id_on_a.clone(), + packet.data.token.clone(), + ); + }, + Denom::IBC(_) => { + self.mint_execute(packet.data.receiver.clone(), packet.data.token.clone(),); + } + } + + self + .emit( + RecvEvent { + sender: packet.data.sender.clone(), + receiver: packet.data.receiver.clone(), + denom: denom.clone(), + amount: packet.data.token.amount, + memo: packet.data.memo.clone(), + success: true, + } + ); + + denom } } @@ -138,120 +245,136 @@ pub mod ICS20TransferComponent { } } - - #[embeddable_as(TransferValidationImpl)] - pub impl TransferValidationContext< - TContractState, - +HasComponent, - +ERC20ABI, - +Drop, - > of ITransferValidationContext> { + #[generate_trait] + pub(crate) impl TransferValidationImpl< + TContractState, +HasComponent, + > of TransferValidationTrait { fn escrow_validate( self: @ComponentState, - from_address: ContractAddress, + from_account: ContractAddress, port_id: PortId, channel_id: ChannelId, - coin: PrefixedCoin, + token: Token, memo: Memo, ) { - assert(memo.memo.len() > MAXIMUM_MEMO_LENGTH, ICS20Errors::MAXIMUM_MEMO_LENGTH); - - let token_address = self.registered_tokens.read(coin.denom); - let balance = ERC20ABIDispatcher { contract_address: token_address } - .balance_of(from_address); - assert(balance > coin.amount, ICS20Errors::INSUFFICIENT_BALANCE); + let contract_address = token.denom.native().unwrap(); + let balance = ERC20ABIDispatcher { contract_address }.balance_of(from_account); + assert(balance > token.amount, ICS20Errors::INSUFFICIENT_BALANCE); } fn unescrow_validate( self: @ComponentState, - to_address: ContractAddress, + to_account: ContractAddress, port_id: PortId, channel_id: ChannelId, - coin: PrefixedCoin, - ) {} + token: Token, + ) { + let contract_address = token.denom.native().unwrap(); + let balance = ERC20ABIDispatcher { contract_address } + .balance_of(get_contract_address()); + assert(token.amount > balance, ICS20Errors::INSUFFICIENT_BALANCE); + } fn mint_validate( - self: @ComponentState, address: ContractAddress, coin: PrefixedCoin, + self: @ComponentState, account: ContractAddress, token: Token, ) {} fn burn_validate( self: @ComponentState, - address: ContractAddress, - coin: PrefixedCoin, + account: ContractAddress, + token: Token, memo: Memo, - ) {} + ) { + let contract_address = self + .minted_token_name_to_address + .read(token.denom.ibc().unwrap()); + let balance = ERC20ABIDispatcher { contract_address }.balance_of(account); + assert(token.amount > balance, ICS20Errors::INSUFFICIENT_BALANCE); + } } - #[embeddable_as(TransferExecutionImpl)] - pub impl TransferExecutionContext< - TContractState, - +HasComponent, - +ERC20ABI, - +Drop, - > of ITransferExecutionContext> { + #[generate_trait] + pub(crate) impl TransferExecutionImpl< + TContractState, +HasComponent, +Drop + > of TransferExecutionTrait { fn escrow_execute( ref self: ComponentState, - from_address: ContractAddress, + from_account: ContractAddress, port_id: PortId, channel_id: ChannelId, - coin: PrefixedCoin, + token: Token, memo: Memo, ) { - let to_address = get_contract_address(); - let mut contract = self.get_contract_mut(); - contract.transfer_from(from_address, to_address, coin.amount); + let contract_address = token.denom.native().unwrap(); + ERC20ABIDispatcher { contract_address } + .transfer_from(from_account, get_contract_address(), token.amount); } fn unescrow_execute( ref self: ComponentState, - to_address: ContractAddress, + to_account: ContractAddress, port_id: PortId, channel_id: ChannelId, - coin: PrefixedCoin, + token: Token, ) {} fn mint_execute( - ref self: ComponentState, address: ContractAddress, coin: PrefixedCoin, - ) {} + ref self: ComponentState, account: ContractAddress, token: Token, + ) { + let ibc_denom = token.denom.ibc().unwrap(); + + let contract_address = self.minted_token_name_to_address.read(ibc_denom); + + let contract_address = if contract_address.is_non_zero() { + contract_address + } else { + self.create_token(account, token) + }; + + ERC20ABIDispatcher { contract_address }.transfer(account, token.amount); + } fn burn_execute( ref self: ComponentState, - address: ContractAddress, - coin: PrefixedCoin, + account: ContractAddress, + token: Token, memo: Memo, ) {} } #[generate_trait] pub(crate) impl TransferInternalImpl< - TContractState, - +HasComponent, - +ERC20ABI, - +Drop, + TContractState, +HasComponent, +Drop > of TransferInternalTrait { - fn initializer(ref self: ComponentState) { + fn initializer(ref self: ComponentState, erc20_class_hash: ClassHash) { self.governor.write(get_caller_address()); + self.erc20_class_hash.write(erc20_class_hash); + self.salt.write(0); 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 create_token( + ref self: ComponentState, account: ContractAddress, token: Token + ) -> ContractAddress { + let salt = self.salt.read(); - fn is_receiver_chain_source(self: @ComponentState, denom: felt252) -> bool { - self.minted_tokens.read(denom).is_zero() - } + let mut call_data = array![]; + call_data.append_serde(token.denom.clone()); + call_data + .append_serde(token.denom); // TODO: determine what should be set as symbol here. + call_data.append_serde(token.amount); + call_data.append_serde(account); + call_data.append_serde(get_contract_address()); - fn create_token(ref self: ComponentState) -> ContractAddress { - // unimplemented! > Dummy value to pass the type check - 0.try_into().unwrap() - } - fn get_escrow_address( - self: @ComponentState, port_id: felt252, channel_id: felt252 - ) -> ContractAddress { - // unimplemented! > Dummy value to pass the type check - 0.try_into().unwrap() + let (address, _) = deploy_syscall( + self.erc20_class_hash.read(), salt, call_data.span(), false, + ) + .unwrap_syscall(); + + self.salt.write(salt + 1); + + address } } } diff --git a/contracts/src/apps/transfer/errors.cairo b/contracts/src/apps/transfer/errors.cairo index 9383c10f..ac290fd5 100644 --- a/contracts/src/apps/transfer/errors.cairo +++ b/contracts/src/apps/transfer/errors.cairo @@ -1,11 +1,11 @@ pub mod ICS20Errors { 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 INVALID_SENDER: felt252 = 'ICS20: Invalid sender account'; + pub const INVALID_RECEIVEER: felt252 = 'ICS20: Invalid receiver account'; + pub const ZERO_AMOUNT: felt252 = 'ICS20: transfer amount is 0'; + pub const INVALID_TOKEN_ADDRESS: felt252 = 'ICS20: invalid token address'; + pub const INVALID_DENOM: felt252 = 'ICS20: invalid denom'; 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 8d8ddafa..8b99f8b6 100644 --- a/contracts/src/apps/transfer/interface.cairo +++ b/contracts/src/apps/transfer/interface.cairo @@ -1,64 +1,22 @@ use starknet::ContractAddress; -use starknet_ibc::apps::transfer::types::{MsgTransfer, PrefixedCoin, Memo}; +use starknet_ibc::apps::transfer::types::{MsgTransfer, Packet, Memo}; use starknet_ibc::core::types::{PortId, ChannelId}; #[starknet::interface] -pub trait ITransfer { - fn send_transfer(ref self: TContractState, msg: MsgTransfer); - fn register_token( - ref self: TContractState, token_name: felt252, token_address: ContractAddress - ); +pub trait ISendTransfer { + fn send_validate(self: @TContractState, msg: MsgTransfer); + fn send_execute(ref self: TContractState, msg: MsgTransfer); } #[starknet::interface] -pub trait ITransferrable { - fn can_send(self: @TContractState); - fn can_receive(self: @TContractState); +pub trait IRecvPacket { + fn recv_validate(self: @TContractState, packet: Packet); + fn recv_execute(ref self: TContractState, packet: Packet); } #[starknet::interface] -pub trait ITransferValidationContext { - fn escrow_validate( - self: @TContractState, - from_address: ContractAddress, - port_id: PortId, - channel_id: ChannelId, - coin: PrefixedCoin, - memo: Memo, - ); - fn unescrow_validate( - self: @TContractState, - to_address: ContractAddress, - port_id: PortId, - channel_id: ChannelId, - coin: PrefixedCoin, - ); - fn mint_validate(self: @TContractState, address: ContractAddress, coin: PrefixedCoin,); - fn burn_validate( - self: @TContractState, address: ContractAddress, coin: PrefixedCoin, memo: Memo, - ); -} - -#[starknet::interface] -pub trait ITransferExecutionContext { - fn escrow_execute( - ref self: TContractState, - from_address: ContractAddress, - port_id: PortId, - channel_id: ChannelId, - coin: PrefixedCoin, - memo: Memo, - ); - fn unescrow_execute( - ref self: TContractState, - to_address: ContractAddress, - port_id: PortId, - channel_id: ChannelId, - coin: PrefixedCoin, - ); - fn mint_execute(ref self: TContractState, address: ContractAddress, coin: PrefixedCoin,); - fn burn_execute( - ref self: TContractState, address: ContractAddress, coin: PrefixedCoin, memo: Memo, - ); +pub trait ITransferrable { + fn can_send(self: @TContractState); + fn can_receive(self: @TContractState); } diff --git a/contracts/src/apps/transfer/types.cairo b/contracts/src/apps/transfer/types.cairo index ae1ba5c7..e0f2c493 100644 --- a/contracts/src/apps/transfer/types.cairo +++ b/contracts/src/apps/transfer/types.cairo @@ -1,5 +1,15 @@ +use core::array::ArrayTrait; +use core::byte_array::ByteArrayTrait; +use core::num::traits::zero::Zero; +use core::serde::Serde; +use core::to_byte_array::FormatAsByteArray; +use core::traits::TryInto; +use openzeppelin::utils::selectors; use starknet::ContractAddress; -use starknet_ibc::core::types::{PortId, ChannelId}; +use starknet::Store; +use starknet::contract_address_const; +use starknet::syscalls::call_contract_syscall; +use starknet_ibc::core::types::{PortId, ChannelId, Sequence, Height, Timestamp}; /// Maximum memo length allowed for ICS-20 transfers. This bound corresponds to /// the `MaximumMemoLength` in the `ibc-go`. @@ -12,22 +22,80 @@ pub struct MsgTransfer { pub packet_data: PacketData, } +#[derive(Clone, Debug, Drop, Serde, Store)] +pub struct Packet { + pub seq_on_a: Sequence, + pub port_id_on_a: PortId, + pub chan_id_on_a: ChannelId, + pub port_id_on_b: PortId, + pub chan_id_on_b: ChannelId, + pub data: PacketData, + pub timeout_height_on_b: Height, + pub timeout_timestamp_on_b: Timestamp, +} + #[derive(Clone, Debug, Drop, Serde, Store)] pub struct PacketData { - pub token: PrefixedCoin, + pub token: Token, pub sender: ContractAddress, pub receiver: ContractAddress, pub memo: Memo, } #[derive(Clone, Debug, Drop, Serde, Store)] -pub struct PrefixedCoin { - pub denom: felt252, +pub struct Token { + pub denom: Denom, pub amount: u256, } +#[derive(Clone, Debug, Drop, Serde, Store)] +pub enum Denom { + Native: ContractAddress, + IBC: ByteArray, +} + +pub trait DenomTrait { + fn is_zero(self: @Denom) -> bool; + fn native(self: @Denom) -> Option; + fn ibc(self: @Denom) -> Option; +} + +pub impl DenomImpl of DenomTrait { + fn is_zero(self: @Denom) -> bool { + match self { + Denom::Native(contract_address) => contract_address.is_zero(), + Denom::IBC(byte_array) => byte_array.len() == 0, + } + } + + fn native(self: @Denom) -> Option { + match self { + Denom::Native(contract_address) => Option::Some(*contract_address), + Denom::IBC(_) => Option::None, + } + } + + fn ibc(self: @Denom) -> Option { + match self { + Denom::Native(_) => Option::None, + Denom::IBC(byte_array) => { + // FIXME: This is not the correct way to convert a byte array to a felt252. + let mut serialized_denom: Array = array![]; + byte_array.serialize(ref serialized_denom); + let mut denom_span = serialized_denom.span(); + Serde::::deserialize(ref denom_span) + } + } + } +} + +pub impl ContractAddressIntoDenom of Into { + fn into(self: ContractAddress) -> Denom { + Denom::Native(self) + } +} + #[derive(Clone, Debug, Drop, Serde, Store)] pub struct Memo { pub memo: ByteArray, } - diff --git a/contracts/src/core/types.cairo b/contracts/src/core/types.cairo index e0cac7ef..232f2267 100644 --- a/contracts/src/core/types.cairo +++ b/contracts/src/core/types.cairo @@ -1,12 +1,28 @@ use starknet::ContractAddress; use starknet::Store; +#[derive(Clone, Debug, Drop, Serde, Store)] +pub struct Height { + pub revision_number: u64, + pub revision_height: u64, +} + +#[derive(Clone, Debug, Drop, Serde, Store)] +pub struct Timestamp { + pub timestamp: u64, +} + #[derive(Clone, Debug, Drop, Serde, Store)] pub struct ChannelId { - channel_id: felt252, + pub channel_id: felt252, } #[derive(Clone, Debug, Drop, Serde, Store)] pub struct PortId { - port_id: felt252, + pub port_id: felt252, +} + +#[derive(Clone, Debug, Drop, Serde, Store)] +pub struct Sequence { + pub sequence: u64, } diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index c9c56de0..af8b7732 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,4 +1,4 @@ pub mod apps; -pub mod contract; +pub mod presets; pub mod core; pub mod tests; diff --git a/contracts/src/presets.cairo b/contracts/src/presets.cairo new file mode 100644 index 00000000..82d2aaf2 --- /dev/null +++ b/contracts/src/presets.cairo @@ -0,0 +1,8 @@ +#[cfg(test)] +mod erc20; +mod transfer; + +#[cfg(test)] +pub use erc20::ERC20; +pub use transfer::Transfer; + diff --git a/contracts/src/presets/erc20.cairo b/contracts/src/presets/erc20.cairo new file mode 100644 index 00000000..7eaf585f --- /dev/null +++ b/contracts/src/presets/erc20.cairo @@ -0,0 +1,50 @@ +#[starknet::contract] +pub(crate) mod ERC20 { + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::{ContractAddress, ClassHash}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // Ownable Mixin + #[abi(embed_v0)] + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + // ERC20 Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event, + } + + #[constructor] + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + fixed_supply: u256, + recipient: ContractAddress, + owner: ContractAddress + ) { + self.ownable.initializer(owner); + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, fixed_supply); + } +} diff --git a/contracts/src/contract.cairo b/contracts/src/presets/transfer.cairo similarity index 57% rename from contracts/src/contract.cairo rename to contracts/src/presets/transfer.cairo index aa1fe25a..e6092a36 100644 --- a/contracts/src/contract.cairo +++ b/contracts/src/presets/transfer.cairo @@ -1,27 +1,22 @@ #[starknet::contract] pub(crate) mod Transfer { - use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::{ContractAddress, ClassHash}; use starknet_ibc::apps::transfer::component::ICS20TransferComponent; - component!(path: ERC20Component, storage: erc20, event: ERC20Event); component!(path: ICS20TransferComponent, storage: transfer, event: ICS20TransferEvent); #[abi(embed_v0)] - impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; - impl ERC20InternalImpl = ERC20Component::InternalImpl; - + impl ICS20SendTransferImpl = + ICS20TransferComponent::SendTransfer; #[abi(embed_v0)] - impl ICS20TransferImpl = ICS20TransferComponent::Transfer; - impl TransferreableImpl = ICS20TransferComponent::Transferrable; + impl ICS20RecvPacketImpl = ICS20TransferComponent::RecvPacket; + impl ICS20TransferreableImpl = ICS20TransferComponent::Transferrable; impl TransferValidationImpl = ICS20TransferComponent::TransferValidationImpl; impl TransferExecutionImpl = ICS20TransferComponent::TransferExecutionImpl; impl TransferInternalImpl = ICS20TransferComponent::TransferInternalImpl; #[storage] struct Storage { - #[substorage(v0)] - erc20: ERC20Component::Storage, #[substorage(v0)] transfer: ICS20TransferComponent::Storage, } @@ -29,14 +24,12 @@ pub(crate) mod Transfer { #[event] #[derive(Drop, starknet::Event)] enum Event { - #[flat] - ERC20Event: ERC20Component::Event, #[flat] ICS20TransferEvent: ICS20TransferComponent::Event, } #[constructor] - fn constructor(ref self: ContractState,) { - self.transfer.initializer(); + fn constructor(ref self: ContractState, erc20_class_hash: ClassHash) { + self.transfer.initializer(erc20_class_hash); } } diff --git a/contracts/src/tests/transfer.cairo b/contracts/src/tests/transfer.cairo index c94e76e6..24b12613 100644 --- a/contracts/src/tests/transfer.cairo +++ b/contracts/src/tests/transfer.cairo @@ -1,51 +1,70 @@ use core::starknet::SyscallResultTrait; use core::traits::TryInto; -use openzeppelin::tests::utils::deploy; +use openzeppelin::tests::utils::{deploy, pop_log}; +use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; 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}; +use starknet_ibc::apps::transfer::interface::{ + ISendTransferDispatcher, ISendTransferDispatcherTrait, IRecvPacketDispatcher, + IRecvPacketDispatcherTrait +}; +use starknet_ibc::apps::transfer::types::Denom; +use starknet_ibc::presets::{Transfer, ERC20}; +use starknet_ibc::tests::utils::{ + AMOUNT, SUPPLY, PREFIXED_DENOM, OWNER, RECIPIENT, dummy_erc20_call_data, dummy_msg_transder, + dummy_recv_packet +}; -type ComponentState = ICS20TransferComponent::ComponentState; - -fn component_state() -> ComponentState { - ICS20TransferComponent::component_state_for_testing() +fn setup_erc20() -> (ERC20ABIDispatcher, ContractAddress) { + let contract_address = deploy(ERC20::TEST_CLASS_HASH, dummy_erc20_call_data()); + (ERC20ABIDispatcher { contract_address }, contract_address) } -fn basic_setup() -> ComponentState { - let mut state = component_state(); - testing::set_caller_address(owner()); - state.initializer(); - state -} +fn setup_ics20() -> (ISendTransferDispatcher, IRecvPacketDispatcher, ContractAddress) { + let mut call_data = array![]; + call_data.append_serde(ERC20::TEST_CLASS_HASH); -fn setup() -> ComponentState { - let mut state = basic_setup(); - state.register_token(TOKEN_NAME, pubkey()); - state -} + let contract_address = deploy(Transfer::TEST_CLASS_HASH, call_data); -#[test] -fn test_deploy() { - let contract_address = deploy(Transfer::TEST_CLASS_HASH, array![]); - ITransferDispatcher { contract_address }.register_token(TOKEN_NAME, owner()); + ( + ISendTransferDispatcher { contract_address }, + IRecvPacketDispatcher { contract_address }, + contract_address + ) } #[test] -fn test_register_token() { - setup(); +fn test_escrow() { + // Deploy an ERC20 contract. + let (erc20_dispatcher, token_address) = setup_erc20(); + + // Deploy an ICS20 Token Transfer contract. + let (send_disptacher, _, transfer_address) = setup_ics20(); + + // Owner approves the amount of allowance for the `Transfer` contract. + testing::set_contract_address(OWNER()); + erc20_dispatcher.approve(transfer_address, AMOUNT); + + // Submit a `MsgTransfer` to the `Transfer` contract. + send_disptacher.send_execute(dummy_msg_transder(token_address.into())); + + // Check the balance of the owner. + let balance = erc20_dispatcher.balance_of(OWNER()); + assert_eq!(balance, SUPPLY - AMOUNT); + + // Check the balance of the `Transfer` contract. + let balance = erc20_dispatcher.balance_of(transfer_address); + assert_eq!(balance, AMOUNT); } #[test] -#[should_panic(expected: ('ICS20: token is already listed',))] -fn test_register_token_twice() { - let mut state = setup(); - state.register_token(TOKEN_NAME, pubkey()); +fn test_mint() { + // Deploy an ICS20 Token Transfer contract. + let (_, recv_disptacher, _) = setup_ics20(); + + // Submit a `RecvPacket` to the `Transfer` contract. + recv_disptacher.recv_validate(dummy_recv_packet(Denom::IBC(PREFIXED_DENOM()))); } diff --git a/contracts/src/tests/utils.cairo b/contracts/src/tests/utils.cairo index 37e837c6..6996073d 100644 --- a/contracts/src/tests/utils.cairo +++ b/contracts/src/tests/utils.cairo @@ -1,19 +1,79 @@ use starknet::ContractAddress; use starknet::contract_address_const; +use starknet_ibc::apps::transfer::types::{MsgTransfer, Packet, PacketData, Token, Denom, Memo}; +use starknet_ibc::core::types::{Height, Timestamp, ChannelId, PortId, Sequence}; -pub(crate) const TOKEN_NAME: felt252 = 'ETH'; +pub(crate) const TOKEN_NAME: felt252 = 'NAME'; pub(crate) const DECIMALS: u8 = 18_u8; pub(crate) const SUPPLY: u256 = 2000; +pub(crate) const AMOUNT: u256 = 100; 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 NAME() -> ByteArray { + "NAME" } -pub(crate) fn pubkey() -> ContractAddress { - contract_address_const::() +pub(crate) fn SYMBOL() -> ByteArray { + "SYMBOL" } +pub(crate) fn PREFIXED_DENOM() -> ByteArray { + "transfer/channel-0/uatom" +} + +pub(crate) fn PUBKEY() -> ContractAddress { + contract_address_const::<0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7>() +} + +pub(crate) fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +pub(crate) fn RECIPIENT() -> ContractAddress { + contract_address_const::<'RECIPIENT'>() +} + +pub(crate) fn dummy_erc20_call_data() -> Array { + let mut call_data: Array = array![]; + Serde::serialize(@NAME(), ref call_data); + Serde::serialize(@SYMBOL(), ref call_data); + Serde::serialize(@SUPPLY, ref call_data); + Serde::serialize(@OWNER(), ref call_data); + Serde::serialize(@OWNER(), ref call_data); + call_data +} + +pub(crate) fn dummy_msg_transder(denom: Denom) -> MsgTransfer { + let port_id_on_a = PortId { port_id: 'transfer' }; + + let chan_id_on_a = ChannelId { channel_id: 'channel-0' }; + + let packet_data = PacketData { + token: Token { denom: denom, amount: AMOUNT }, + sender: OWNER(), + receiver: RECIPIENT(), + memo: Memo { memo: "" }, + }; + + MsgTransfer { port_id_on_a, chan_id_on_a, packet_data, } +} + +pub(crate) fn dummy_recv_packet(denom: Denom) -> Packet { + let data = PacketData { + token: Token { denom: denom, amount: AMOUNT }, + sender: OWNER(), + receiver: RECIPIENT(), + memo: Memo { memo: "" }, + }; + + Packet { + seq_on_a: Sequence { sequence: 0 }, + port_id_on_a: PortId { port_id: 'transfer' }, + chan_id_on_a: ChannelId { channel_id: 'channel-0' }, + port_id_on_b: PortId { port_id: 'transfer' }, + chan_id_on_b: ChannelId { channel_id: 'channel-1' }, + data, + timeout_height_on_b: Height { revision_number: 0, revision_height: 1000 }, + timeout_timestamp_on_b: Timestamp { timestamp: 1000 } + } +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index a5908bd5..7742b023 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -54,14 +54,14 @@ declare() { # deploy the contract deploy() { - if [[ $CLASS_HASH == "" ]]; then + if [[ $ICS20_CLASS_HASH == "" ]]; then class_hash=$(declare) else class_hash=$CLASS_HASH fi output=$( - starkli deploy --not-unique \ + starkli deploy --not-unique $ERC20_CLASS_HASH \ --watch $class_hash \ --rpc $RPC_URL \ --account $ACCOUNT_SRC \ diff --git a/scripts/invoke.sh b/scripts/invoke.sh index 0764679d..d704d27e 100755 --- a/scripts/invoke.sh +++ b/scripts/invoke.sh @@ -12,7 +12,7 @@ invoke() { fi output=$( - starkli invoke $address register_token 1 0x4e91934ce777f807d6bc90fd3b06e1fa49e942ab1fb70a072ca1ad61dc2998d \ + starkli invoke $address send_exectue \ --rpc $RPC_URL \ --account $ACCOUNT_SRC \ --keystore $KEYSTORE_SRC \ From f16e3d2e4615a6f3843897775f14c7a0b5738ee6 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Thu, 25 Jul 2024 22:34:17 -0700 Subject: [PATCH 04/15] fix: lint group --- Cargo.toml | 1 - contracts/src/lib.cairo | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4b4a1d8a..69c44ec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ repository = "https://github.com/informalsystems/ibc-starknet" unsafe_code = "forbid" dead_code = "deny" rust_2018_idioms = "deny" -trivial_casts = "deny" unused_import_braces = "deny" unused_variables = "allow" diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index af8b7732..b57d33bb 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,4 +1,4 @@ pub mod apps; -pub mod presets; pub mod core; +pub mod presets; pub mod tests; From 2a83ca43d46e11cec51060c2380ffee59628009d Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Thu, 25 Jul 2024 22:37:57 -0700 Subject: [PATCH 05/15] fix: lint group --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 69c44ec6..1c8484a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ repository = "https://github.com/informalsystems/ibc-starknet" [workspace.lints.rust] unsafe_code = "forbid" dead_code = "deny" -rust_2018_idioms = "deny" +rust_2018_idioms = { level = "deny", priority = -1 } unused_import_braces = "deny" unused_variables = "allow" From 7abc58c2118b6a4667a97cdeded2559615d6a8e8 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Thu, 25 Jul 2024 22:54:09 -0700 Subject: [PATCH 06/15] fix: make clippy happy --- Cargo.lock | 4 ++++ Cargo.toml | 1 + light-client/impls/Cargo.toml | 6 ++++++ light-client/types/Cargo.toml | 6 +++++- light-client/types/src/client_state.rs | 1 + light-client/types/src/consensus_state.rs | 1 + 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 40621740..d11720a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -818,6 +818,7 @@ dependencies = [ "derive_more 0.99.18", "ibc-client-starknet-types", "ibc-core", + "serde", ] [[package]] @@ -833,6 +834,9 @@ dependencies = [ [[package]] name = "ibc-client-starknet-types" version = "0.1.0" +dependencies = [ + "serde", +] [[package]] name = "ibc-client-wasm-types" diff --git a/Cargo.toml b/Cargo.toml index 1c8484a6..eedcf6e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ uninlined_format_args = "deny" [workspace.dependencies] # external dependencies derive_more = { version = "0.99.11", features = [ "from", "try_into" ] } +serde = { version = "1.0", default-features = false } # ibc depedenencies ibc-core = { version = "0.53.0", default-features = false, features = [ "borsh", "schema" ] } diff --git a/light-client/impls/Cargo.toml b/light-client/impls/Cargo.toml index ac623464..d7aa9e20 100644 --- a/light-client/impls/Cargo.toml +++ b/light-client/impls/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] # external dependencies derive_more = { workspace = true } +serde = { workspace = true, optional = true } # ibc dependencies ibc-core = { workspace = true } @@ -28,3 +29,8 @@ std = [ "ibc-core/std", "ibc-client-starknet-types/std", ] +serde = [ + "ibc-core/serde", + "ibc-client-starknet-types/serde", + "dep:serde", +] diff --git a/light-client/types/Cargo.toml b/light-client/types/Cargo.toml index 76960f74..a9d39c68 100644 --- a/light-client/types/Cargo.toml +++ b/light-client/types/Cargo.toml @@ -15,7 +15,11 @@ description = """ workspace = true [dependencies] +serde = { workspace = true, optional = true } [features] default = [ "std" ] -std = [ ] +std = [ ] +serde = [ + "dep:serde", +] diff --git a/light-client/types/src/client_state.rs b/light-client/types/src/client_state.rs index 438159c8..0ff17ebc 100644 --- a/light-client/types/src/client_state.rs +++ b/light-client/types/src/client_state.rs @@ -1,2 +1,3 @@ +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq)] pub struct ClientState {} diff --git a/light-client/types/src/consensus_state.rs b/light-client/types/src/consensus_state.rs index f3b2dcc4..bfcf9243 100644 --- a/light-client/types/src/consensus_state.rs +++ b/light-client/types/src/consensus_state.rs @@ -1,2 +1,3 @@ +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq)] pub struct ConsensusState {} From 4035ef50927ad312fed960e1612bbe85d34a6dc1 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 26 Jul 2024 15:21:27 +0200 Subject: [PATCH 07/15] impl bytes_to_felt252 --- contracts/src/apps/transfer/types.cairo | 99 +++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/contracts/src/apps/transfer/types.cairo b/contracts/src/apps/transfer/types.cairo index e0f2c493..32ac8c85 100644 --- a/contracts/src/apps/transfer/types.cairo +++ b/contracts/src/apps/transfer/types.cairo @@ -78,13 +78,7 @@ pub impl DenomImpl of DenomTrait { fn ibc(self: @Denom) -> Option { match self { Denom::Native(_) => Option::None, - Denom::IBC(byte_array) => { - // FIXME: This is not the correct way to convert a byte array to a felt252. - let mut serialized_denom: Array = array![]; - byte_array.serialize(ref serialized_denom); - let mut denom_span = serialized_denom.span(); - Serde::::deserialize(ref denom_span) - } + Denom::IBC(byte_array) => bytes_to_felt252(byte_array), } } } @@ -99,3 +93,94 @@ pub impl ContractAddressIntoDenom of Into { pub struct Memo { pub memo: ByteArray, } + +fn bytes_to_felt252(bytes: @ByteArray) -> Option { + if bytes.len() == 0 { + return Option::Some(''); + } + + if bytes.len() > 31 { + return Option::None(()); + } + + let mut result: felt252 = 0; + let mut multiplier: felt252 = 1; + + // Iterate through the bytes in reverse order + let mut i = bytes.len(); + loop { + if i == 0 { + break; + } + i -= 1; + + let byte_value = bytes.at(i).unwrap(); + result += byte_value.into() * multiplier; + multiplier *= 0x100; // 256 + }; + + Option::Some(result) +} + + +mod test { + use super::bytes_to_felt252; + + #[test] + fn test_empty_string() { + let bytes = ""; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some(0), "Empty string should convert to 0"); + } + + #[test] + fn test_single_character() { + let bytes = "A"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('A'), "Single character conversion failed"); + } + + #[test] + fn test_multiple_bytes() { + let bytes = "abc"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('abc'), "Multiple byte conversion failed"); + } + + #[test] + fn test_max_bytes() { + let bytes = "abcdefghijklmnopqrstuvwxyz12345"; // 31 characters + let result = bytes_to_felt252(@bytes); + assert!( + result == Option::Some('abcdefghijklmnopqrstuvwxyz12345'), "Max bytes conversion failed" + ); + } + + #[test] + fn test_too_many_bytes() { + let bytes = "abcdefghijklmnopqrstuvwxyz123456"; // 32 characters + let result = bytes_to_felt252(@bytes); + assert!(result == Option::None(()), "More than characters should return None"); + } + + #[test] + fn test_leading_zeros() { + let bytes = "\0\0ab"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('ab'), "Leading zeros not handled correctly"); + } + + #[test] + fn test_all_zeros() { + let bytes = "\0\0\0\0"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some(0), "All zeros should convert to 0"); + } + + #[test] + fn test_special_characters() { + let bytes = "!@#$"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('!@#$'), "Special characters conversion failed"); + } +} From 9204cfd5ee2bbe2d4e998f652adea9b324b71c28 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 26 Jul 2024 16:52:33 +0200 Subject: [PATCH 08/15] mv bytes_to_felt252 to utils --- contracts/src/apps/transfer/types.cairo | 92 +------------------------ contracts/src/lib.cairo | 1 + contracts/src/utils.cairo | 90 ++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 91 deletions(-) create mode 100644 contracts/src/utils.cairo diff --git a/contracts/src/apps/transfer/types.cairo b/contracts/src/apps/transfer/types.cairo index 32ac8c85..88405baf 100644 --- a/contracts/src/apps/transfer/types.cairo +++ b/contracts/src/apps/transfer/types.cairo @@ -10,6 +10,7 @@ use starknet::Store; use starknet::contract_address_const; use starknet::syscalls::call_contract_syscall; use starknet_ibc::core::types::{PortId, ChannelId, Sequence, Height, Timestamp}; +use starknet_ibc::utils::bytes_to_felt252; /// Maximum memo length allowed for ICS-20 transfers. This bound corresponds to /// the `MaximumMemoLength` in the `ibc-go`. @@ -93,94 +94,3 @@ pub impl ContractAddressIntoDenom of Into { pub struct Memo { pub memo: ByteArray, } - -fn bytes_to_felt252(bytes: @ByteArray) -> Option { - if bytes.len() == 0 { - return Option::Some(''); - } - - if bytes.len() > 31 { - return Option::None(()); - } - - let mut result: felt252 = 0; - let mut multiplier: felt252 = 1; - - // Iterate through the bytes in reverse order - let mut i = bytes.len(); - loop { - if i == 0 { - break; - } - i -= 1; - - let byte_value = bytes.at(i).unwrap(); - result += byte_value.into() * multiplier; - multiplier *= 0x100; // 256 - }; - - Option::Some(result) -} - - -mod test { - use super::bytes_to_felt252; - - #[test] - fn test_empty_string() { - let bytes = ""; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some(0), "Empty string should convert to 0"); - } - - #[test] - fn test_single_character() { - let bytes = "A"; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some('A'), "Single character conversion failed"); - } - - #[test] - fn test_multiple_bytes() { - let bytes = "abc"; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some('abc'), "Multiple byte conversion failed"); - } - - #[test] - fn test_max_bytes() { - let bytes = "abcdefghijklmnopqrstuvwxyz12345"; // 31 characters - let result = bytes_to_felt252(@bytes); - assert!( - result == Option::Some('abcdefghijklmnopqrstuvwxyz12345'), "Max bytes conversion failed" - ); - } - - #[test] - fn test_too_many_bytes() { - let bytes = "abcdefghijklmnopqrstuvwxyz123456"; // 32 characters - let result = bytes_to_felt252(@bytes); - assert!(result == Option::None(()), "More than characters should return None"); - } - - #[test] - fn test_leading_zeros() { - let bytes = "\0\0ab"; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some('ab'), "Leading zeros not handled correctly"); - } - - #[test] - fn test_all_zeros() { - let bytes = "\0\0\0\0"; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some(0), "All zeros should convert to 0"); - } - - #[test] - fn test_special_characters() { - let bytes = "!@#$"; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some('!@#$'), "Special characters conversion failed"); - } -} diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index b57d33bb..17622a3a 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -2,3 +2,4 @@ pub mod apps; pub mod core; pub mod presets; pub mod tests; +pub mod utils; diff --git a/contracts/src/utils.cairo b/contracts/src/utils.cairo new file mode 100644 index 00000000..d1564b19 --- /dev/null +++ b/contracts/src/utils.cairo @@ -0,0 +1,90 @@ +pub fn bytes_to_felt252(bytes: @ByteArray) -> Option { + if bytes.len() == 0 { + return Option::Some(''); + } + + if bytes.len() > 31 { + return Option::None(()); + } + + let mut result: felt252 = 0; + let mut multiplier: felt252 = 1; + + // Iterate through the bytes in reverse order + let mut i = bytes.len(); + loop { + if i == 0 { + break; + } + i -= 1; + + let byte_value = bytes.at(i).unwrap(); + result += byte_value.into() * multiplier; + multiplier *= 0x100; // 256 + }; + + Option::Some(result) +} + +#[cfg(test)] +mod test { + use super::bytes_to_felt252; + + #[test] + fn test_empty_string() { + let bytes = ""; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some(0), "Empty string should convert to 0"); + } + + #[test] + fn test_single_character() { + let bytes = "A"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('A'), "Single character conversion failed"); + } + + #[test] + fn test_multiple_bytes() { + let bytes = "abc"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('abc'), "Multiple byte conversion failed"); + } + + #[test] + fn test_max_bytes() { + let bytes = "abcdefghijklmnopqrstuvwxyz12345"; // 31 characters + let result = bytes_to_felt252(@bytes); + assert!( + result == Option::Some('abcdefghijklmnopqrstuvwxyz12345'), "Max bytes conversion failed" + ); + } + + #[test] + fn test_too_many_bytes() { + let bytes = "abcdefghijklmnopqrstuvwxyz123456"; // 32 characters + let result = bytes_to_felt252(@bytes); + assert!(result == Option::None(()), "More than characters should return None"); + } + + #[test] + fn test_leading_zeros() { + let bytes = "\0\0ab"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('ab'), "Leading zeros not handled correctly"); + } + + #[test] + fn test_all_zeros() { + let bytes = "\0\0\0\0"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some(0), "All zeros should convert to 0"); + } + + #[test] + fn test_special_characters() { + let bytes = "!@#$"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('!@#$'), "Special characters conversion failed"); + } +} From 3a3ec6be3baa50e786a2752cc41ddbef3ef433dc Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 26 Jul 2024 22:34:28 +0200 Subject: [PATCH 09/15] add felt252_to_bytes --- contracts/src/utils.cairo | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/contracts/src/utils.cairo b/contracts/src/utils.cairo index d1564b19..9e612435 100644 --- a/contracts/src/utils.cairo +++ b/contracts/src/utils.cairo @@ -26,15 +26,39 @@ pub fn bytes_to_felt252(bytes: @ByteArray) -> Option { Option::Some(result) } +pub fn felt252_to_bytes(value: felt252) -> ByteArray { + if value == '' { + return ""; + } + + let mut result: ByteArray = ""; + let mut remaining: u256 = value.into(); + + loop { + let byte_value = (remaining % 0x100); // 256 + remaining /= 0x100; // 256 + + result.append_byte(byte_value.try_into().unwrap()); + if remaining == 0 { + break; + } + }; + + result.rev() +} + + #[cfg(test)] mod test { - use super::bytes_to_felt252; + use super::{bytes_to_felt252, felt252_to_bytes}; #[test] fn test_empty_string() { let bytes = ""; let result = bytes_to_felt252(@bytes); assert!(result == Option::Some(0), "Empty string should convert to 0"); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(bytes == result_back, "Empty string should convert to 0"); } #[test] @@ -42,6 +66,8 @@ mod test { let bytes = "A"; let result = bytes_to_felt252(@bytes); assert!(result == Option::Some('A'), "Single character conversion failed"); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(bytes == result_back, "Single character conversion failed"); } #[test] @@ -49,6 +75,8 @@ mod test { let bytes = "abc"; let result = bytes_to_felt252(@bytes); assert!(result == Option::Some('abc'), "Multiple byte conversion failed"); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(bytes == result_back, "Multiple byte conversion failed"); } #[test] @@ -58,6 +86,8 @@ mod test { assert!( result == Option::Some('abcdefghijklmnopqrstuvwxyz12345'), "Max bytes conversion failed" ); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(bytes == result_back, "Max bytes conversion failed"); } #[test] @@ -72,6 +102,8 @@ mod test { let bytes = "\0\0ab"; let result = bytes_to_felt252(@bytes); assert!(result == Option::Some('ab'), "Leading zeros not handled correctly"); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(bytes == result_back, "Leading zeros not handled correctly"); } #[test] @@ -79,6 +111,8 @@ mod test { let bytes = "\0\0\0\0"; let result = bytes_to_felt252(@bytes); assert!(result == Option::Some(0), "All zeros should convert to 0"); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(bytes == result_back, "All zeros should convert to 0"); } #[test] @@ -86,5 +120,7 @@ mod test { let bytes = "!@#$"; let result = bytes_to_felt252(@bytes); assert!(result == Option::Some('!@#$'), "Special characters conversion failed"); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(bytes == result_back, "Special characters conversion failed"); } } From b69e4bd1b2cef444a425c4762e9a6007049577b5 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 26 Jul 2024 22:36:57 +0200 Subject: [PATCH 10/15] add comment for null char --- contracts/src/utils.cairo | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/contracts/src/utils.cairo b/contracts/src/utils.cairo index 9e612435..7e00ddd8 100644 --- a/contracts/src/utils.cairo +++ b/contracts/src/utils.cairo @@ -26,6 +26,8 @@ pub fn bytes_to_felt252(bytes: @ByteArray) -> Option { Option::Some(result) } +// felt252_to_bytes(bytes_to_felt252(x).unwrap()) == x +// FIXME: if x has `\0` (null char), it doesn't work. pub fn felt252_to_bytes(value: felt252) -> ByteArray { if value == '' { return ""; @@ -97,24 +99,6 @@ mod test { assert!(result == Option::None(()), "More than characters should return None"); } - #[test] - fn test_leading_zeros() { - let bytes = "\0\0ab"; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some('ab'), "Leading zeros not handled correctly"); - let result_back = felt252_to_bytes(result.unwrap()); - assert!(bytes == result_back, "Leading zeros not handled correctly"); - } - - #[test] - fn test_all_zeros() { - let bytes = "\0\0\0\0"; - let result = bytes_to_felt252(@bytes); - assert!(result == Option::Some(0), "All zeros should convert to 0"); - let result_back = felt252_to_bytes(result.unwrap()); - assert!(bytes == result_back, "All zeros should convert to 0"); - } - #[test] fn test_special_characters() { let bytes = "!@#$"; From 584d7f98da40afca72a03d0e3301399a93d10e82 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 26 Jul 2024 22:37:51 +0200 Subject: [PATCH 11/15] fix typo --- contracts/src/apps/transfer/component.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/apps/transfer/component.cairo b/contracts/src/apps/transfer/component.cairo index cfc1a79a..6525ea19 100644 --- a/contracts/src/apps/transfer/component.cairo +++ b/contracts/src/apps/transfer/component.cairo @@ -169,7 +169,7 @@ pub mod ICS20TransferComponent { } #[generate_trait] - impl RecvPacketInernalImpl< + impl RecvPacketInternalImpl< TContractState, +HasComponent, +Drop > of RecvPacketInternal { fn _recv_validate(self: @ComponentState, packet: Packet) -> Denom { From 3379e4e87054578d204922bc86d365572fac43eb Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 26 Jul 2024 23:34:04 +0200 Subject: [PATCH 12/15] more comments on leading null chars --- contracts/src/utils.cairo | 48 ++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/contracts/src/utils.cairo b/contracts/src/utils.cairo index 7e00ddd8..2d5c440a 100644 --- a/contracts/src/utils.cairo +++ b/contracts/src/utils.cairo @@ -1,3 +1,5 @@ +// Note: if bytes has leading `\0` (null char), `felt252` forgets that information. +// bytes_to_felt252(felt252_to_bytes(x)).unwrap() == x pub fn bytes_to_felt252(bytes: @ByteArray) -> Option { if bytes.len() == 0 { return Option::Some(''); @@ -27,23 +29,24 @@ pub fn bytes_to_felt252(bytes: @ByteArray) -> Option { } // felt252_to_bytes(bytes_to_felt252(x).unwrap()) == x -// FIXME: if x has `\0` (null char), it doesn't work. -pub fn felt252_to_bytes(value: felt252) -> ByteArray { - if value == '' { +pub fn felt252_to_bytes(felt: felt252) -> ByteArray { + if felt == '' { return ""; } let mut result: ByteArray = ""; - let mut remaining: u256 = value.into(); + let mut remaining: u256 = felt.into(); loop { - let byte_value = (remaining % 0x100); // 256 - remaining /= 0x100; // 256 - - result.append_byte(byte_value.try_into().unwrap()); if remaining == 0 { break; } + + let byte_value: u8 = (remaining % 0x100).try_into().unwrap(); // 256 + + result.append_byte(byte_value); + + remaining /= 0x100; // 256 }; result.rev() @@ -60,7 +63,7 @@ mod test { let result = bytes_to_felt252(@bytes); assert!(result == Option::Some(0), "Empty string should convert to 0"); let result_back = felt252_to_bytes(result.unwrap()); - assert!(bytes == result_back, "Empty string should convert to 0"); + assert!(result_back == bytes, "Empty string should convert to 0"); } #[test] @@ -69,7 +72,7 @@ mod test { let result = bytes_to_felt252(@bytes); assert!(result == Option::Some('A'), "Single character conversion failed"); let result_back = felt252_to_bytes(result.unwrap()); - assert!(bytes == result_back, "Single character conversion failed"); + assert!(result_back == bytes, "Single character conversion failed"); } #[test] @@ -78,7 +81,7 @@ mod test { let result = bytes_to_felt252(@bytes); assert!(result == Option::Some('abc'), "Multiple byte conversion failed"); let result_back = felt252_to_bytes(result.unwrap()); - assert!(bytes == result_back, "Multiple byte conversion failed"); + assert!(result_back == bytes, "Multiple byte conversion failed"); } #[test] @@ -89,7 +92,7 @@ mod test { result == Option::Some('abcdefghijklmnopqrstuvwxyz12345'), "Max bytes conversion failed" ); let result_back = felt252_to_bytes(result.unwrap()); - assert!(bytes == result_back, "Max bytes conversion failed"); + assert!(result_back == bytes, "Max bytes conversion failed"); } #[test] @@ -105,6 +108,25 @@ mod test { let result = bytes_to_felt252(@bytes); assert!(result == Option::Some('!@#$'), "Special characters conversion failed"); let result_back = felt252_to_bytes(result.unwrap()); - assert!(bytes == result_back, "Special characters conversion failed"); + assert!(result_back == bytes, "Special characters conversion failed"); + } + + #[test] + fn test_null_character() { + let bytes = "abc\0def\0"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('abc\0def\0'), "Null character conversion failed"); + let result_back = felt252_to_bytes(result.unwrap()); + assert!(result_back == bytes, "Null character conversion failed"); + } + + #[test] + fn test_leading_null_characters() { + let bytes = "\0\0\0abc"; + let result = bytes_to_felt252(@bytes); + assert!(result == Option::Some('\0\0\0abc'), "Leading null character conversion failed"); + let result_back = felt252_to_bytes(result.unwrap()); + // trims the leading null characters + assert!(result_back == "abc", "Leading null character conversion failed"); } } From b8285b3925cb28155dc59c4b72f5d43ed34e9c55 Mon Sep 17 00:00:00 2001 From: Ranadeep Biswas Date: Fri, 26 Jul 2024 23:37:05 +0200 Subject: [PATCH 13/15] refactor --- contracts/src/utils.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/utils.cairo b/contracts/src/utils.cairo index 2d5c440a..721af5d6 100644 --- a/contracts/src/utils.cairo +++ b/contracts/src/utils.cairo @@ -42,9 +42,9 @@ pub fn felt252_to_bytes(felt: felt252) -> ByteArray { break; } - let byte_value: u8 = (remaining % 0x100).try_into().unwrap(); // 256 + let byte_value = remaining % 0x100; // 256 - result.append_byte(byte_value); + result.append_byte(byte_value.try_into().unwrap()); remaining /= 0x100; // 256 }; From 90f33f99c6b635d764d42517237b70479145b006 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Mon, 29 Jul 2024 21:49:52 -0700 Subject: [PATCH 14/15] imp: overhaul implementation + make erc20 mintable + happy transfer tests --- contracts/Scarb.toml | 3 + contracts/src/apps.cairo | 3 + contracts/src/apps/governance.cairo | 2 + contracts/src/apps/governance/component.cairo | 30 ++ contracts/src/apps/governance/interface.cairo | 3 + contracts/src/apps/mintable.cairo | 3 + contracts/src/apps/mintable/component.cairo | 86 ++++ contracts/src/apps/mintable/errors.cairo | 9 + contracts/src/apps/mintable/interface.cairo | 7 + contracts/src/apps/transfer/component.cairo | 458 +++++++++++------- contracts/src/apps/transfer/errors.cairo | 7 +- contracts/src/apps/transfer/interface.cairo | 11 +- contracts/src/apps/transfer/types.cairo | 237 +++++++-- contracts/src/apps/transferrable.cairo | 3 + .../src/apps/transferrable/component.cairo | 40 ++ contracts/src/apps/transferrable/errors.cairo | 4 + .../src/apps/transferrable/interface.cairo | 6 + contracts/src/core.cairo | 4 +- contracts/src/core/channel.cairo | 1 + contracts/src/core/channel/types.cairo | 14 + contracts/src/core/client.cairo | 1 + contracts/src/core/client/types.cairo | 10 + contracts/src/core/host.cairo | 2 + contracts/src/core/host/errors.cairo | 6 + contracts/src/core/host/types.cairo | 146 ++++++ contracts/src/core/types.cairo | 28 -- contracts/src/presets.cairo | 1 - contracts/src/presets/erc20.cairo | 12 + contracts/src/presets/transfer.cairo | 31 +- contracts/src/tests.cairo | 2 + contracts/src/tests/setup.cairo | 97 ++++ contracts/src/tests/transfer.cairo | 143 ++++-- contracts/src/tests/utils.cairo | 78 +-- contracts/src/utils.cairo | 5 + 34 files changed, 1158 insertions(+), 335 deletions(-) create mode 100644 contracts/src/apps/governance.cairo create mode 100644 contracts/src/apps/governance/component.cairo create mode 100644 contracts/src/apps/governance/interface.cairo create mode 100644 contracts/src/apps/mintable.cairo create mode 100644 contracts/src/apps/mintable/component.cairo create mode 100644 contracts/src/apps/mintable/errors.cairo create mode 100644 contracts/src/apps/mintable/interface.cairo create mode 100644 contracts/src/apps/transferrable.cairo create mode 100644 contracts/src/apps/transferrable/component.cairo create mode 100644 contracts/src/apps/transferrable/errors.cairo create mode 100644 contracts/src/apps/transferrable/interface.cairo create mode 100644 contracts/src/core/channel.cairo create mode 100644 contracts/src/core/channel/types.cairo create mode 100644 contracts/src/core/client.cairo create mode 100644 contracts/src/core/client/types.cairo create mode 100644 contracts/src/core/host.cairo create mode 100644 contracts/src/core/host/errors.cairo create mode 100644 contracts/src/core/host/types.cairo delete mode 100644 contracts/src/core/types.cairo create mode 100644 contracts/src/tests/setup.cairo diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index 082ec5ee..1a4527b5 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -19,6 +19,9 @@ snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = " [lib] [[target.starknet-contract]] +allowed-libfuncs-list.name = "experimental" +sierra = true +casm = false [tool.fmt] sort-module-level-items = true diff --git a/contracts/src/apps.cairo b/contracts/src/apps.cairo index 014e52f2..ed5e643a 100644 --- a/contracts/src/apps.cairo +++ b/contracts/src/apps.cairo @@ -1 +1,4 @@ +pub mod governance; +pub mod mintable; pub mod transfer; +pub mod transferrable; diff --git a/contracts/src/apps/governance.cairo b/contracts/src/apps/governance.cairo new file mode 100644 index 00000000..fee8901c --- /dev/null +++ b/contracts/src/apps/governance.cairo @@ -0,0 +1,2 @@ +pub mod component; +pub mod interface; diff --git a/contracts/src/apps/governance/component.cairo b/contracts/src/apps/governance/component.cairo new file mode 100644 index 00000000..7fcfaef8 --- /dev/null +++ b/contracts/src/apps/governance/component.cairo @@ -0,0 +1,30 @@ +#[starknet::component] +pub mod IBCGovernanceComponent { + use starknet::ContractAddress; + use starknet::get_caller_address; + use starknet_ibc::apps::governance::interface::IGovernance; + + #[storage] + struct Storage { + governor: ContractAddress, + } + + #[event] + #[derive(Drop, Debug, starknet::Event)] + pub enum Event {} + + #[embeddable_as(Governance)] + pub impl GovernanceImpl< + TContractState, +HasComponent, +Drop + > of IGovernance> {} + + #[generate_trait] + pub impl GovernanceInternalImpl< + TContractState, +HasComponent, +Drop + > of GovernanceInternalTrait { + fn initializer(ref self: ComponentState) { + self.governor.write(get_caller_address()); + } + } +} + diff --git a/contracts/src/apps/governance/interface.cairo b/contracts/src/apps/governance/interface.cairo new file mode 100644 index 00000000..95ce4d2a --- /dev/null +++ b/contracts/src/apps/governance/interface.cairo @@ -0,0 +1,3 @@ +#[starknet::interface] +pub trait IGovernance {} + diff --git a/contracts/src/apps/mintable.cairo b/contracts/src/apps/mintable.cairo new file mode 100644 index 00000000..3e1ae43c --- /dev/null +++ b/contracts/src/apps/mintable.cairo @@ -0,0 +1,3 @@ +pub mod component; +pub mod errors; +pub mod interface; diff --git a/contracts/src/apps/mintable/component.cairo b/contracts/src/apps/mintable/component.cairo new file mode 100644 index 00000000..04af5ba0 --- /dev/null +++ b/contracts/src/apps/mintable/component.cairo @@ -0,0 +1,86 @@ +#[starknet::component] +pub mod ERC20MintableComponent { + use core::num::traits::Zero; + use openzeppelin::token::erc20::ERC20Component::InternalTrait; + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::erc20::ERC20Component::Transfer; + use starknet::ContractAddress; + use starknet::get_caller_address; + use starknet_ibc::apps::mintable::errors::MintableErrors; + use starknet_ibc::apps::mintable::interface::IERC20Mintable; + + #[storage] + struct Storage { + permission: ContractAddress, + } + + #[event] + #[derive(Drop, Debug, starknet::Event)] + pub enum Event {} + + #[embeddable_as(ERC20Mintable)] + pub impl ERC20MintableImpl< + TContractState, + +HasComponent, + +Drop, + impl ERC20: ERC20Component::HasComponent, + > of IERC20Mintable> { + fn permissioned_mint( + ref self: ComponentState, recipient: ContractAddress, amount: u256 + ) { + let permitted_minter = self.permission.read(); + assert(permitted_minter == get_caller_address(), MintableErrors::UNAUTHORIZED_MINTER); + + self.mint(recipient, amount); + } + + fn permissioned_burn( + ref self: ComponentState, account: ContractAddress, amount: u256 + ) { + let permitted_burner = self.permission.read(); + assert(permitted_burner == get_caller_address(), MintableErrors::UNAUTHORIZED_BURNER); + self.burn(account, amount); + } + } + + #[generate_trait] + pub impl ERC20MintableInternalImpl< + TContractState, + +HasComponent, + +Drop, + impl ERC20: ERC20Component::HasComponent + > of ERC20MintableInternalTrait { + fn initializer(ref self: ComponentState) { + self.permission.write(get_caller_address()); + } + + fn mint( + ref self: ComponentState, recipient: ContractAddress, amount: u256 + ) { + let mut erc20_comp = get_dep_component_mut!(ref self, ERC20); + assert(recipient.is_non_zero(), MintableErrors::MINT_TO_ZERO); + + erc20_comp.ERC20_total_supply.write(erc20_comp.ERC20_total_supply.read() + amount); + erc20_comp + .ERC20_balances + .write(recipient, erc20_comp.ERC20_balances.read(recipient) + amount); + + erc20_comp.emit(Transfer { from: Zero::zero(), to: recipient, value: amount }); + } + + fn burn(ref self: ComponentState, account: ContractAddress, amount: u256) { + let mut erc20_comp = get_dep_component_mut!(ref self, ERC20); + assert(account.is_non_zero(), MintableErrors::BURN_FROM_ZERO); + + let total_supply = erc20_comp.ERC20_total_supply.read(); + assert(total_supply >= amount, MintableErrors::INSUFFICIENT_SUPPLY); + erc20_comp.ERC20_total_supply.write(total_supply - amount); + + let balance = erc20_comp.ERC20_balances.read(account); + assert(balance >= amount, MintableErrors::INSUFFICIENT_BALANCE); + erc20_comp.ERC20_balances.write(account, balance - amount); + + erc20_comp.emit(Transfer { from: account, to: Zero::zero(), value: amount }); + } + } +} diff --git a/contracts/src/apps/mintable/errors.cairo b/contracts/src/apps/mintable/errors.cairo new file mode 100644 index 00000000..835b1726 --- /dev/null +++ b/contracts/src/apps/mintable/errors.cairo @@ -0,0 +1,9 @@ +pub mod MintableErrors { + pub const UNAUTHORIZED_MINTER: felt252 = 'Unauthorized minter'; + pub const UNAUTHORIZED_BURNER: felt252 = 'Unauthorized burner'; + pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; + pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; + pub const INSUFFICIENT_BALANCE: felt252 = 'ERC20: insufficient balance'; + pub const INSUFFICIENT_SUPPLY: felt252 = 'ERC20: insufficient supply'; +} + diff --git a/contracts/src/apps/mintable/interface.cairo b/contracts/src/apps/mintable/interface.cairo new file mode 100644 index 00000000..20ae555c --- /dev/null +++ b/contracts/src/apps/mintable/interface.cairo @@ -0,0 +1,7 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC20Mintable { + fn permissioned_mint(ref self: TContractState, recipient: ContractAddress, amount: u256); + fn permissioned_burn(ref self: TContractState, account: ContractAddress, amount: u256); +} diff --git a/contracts/src/apps/transfer/component.cairo b/contracts/src/apps/transfer/component.cairo index 6525ea19..379c6f09 100644 --- a/contracts/src/apps/transfer/component.cairo +++ b/contracts/src/apps/transfer/component.cairo @@ -1,10 +1,3 @@ -use core::array::ArrayTrait; -use core::serde::Serde; -use core::to_byte_array::FormatAsByteArray; -use core::traits::TryInto; -use starknet::ContractAddress; -use starknet_ibc::apps::transfer::errors::ICS20Errors; - #[starknet::component] pub mod ICS20TransferComponent { use core::array::ArrayTrait; @@ -12,152 +5,131 @@ pub mod ICS20TransferComponent { use core::num::traits::Zero; use core::option::OptionTrait; use core::starknet::SyscallResultTrait; - use core::traits::TryInto; - use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use openzeppelin::utils::serde::SerializedAppend; use starknet::ClassHash; use starknet::ContractAddress; - use starknet::get_caller_address; use starknet::get_contract_address; - use starknet::syscalls::deploy_syscall; - use starknet_ibc::apps::transfer::errors::ICS20Errors; - use starknet_ibc::apps::transfer::interface::{ISendTransfer, IRecvPacket, ITransferrable}; + use starknet_ibc::apps::transfer::errors::TransferErrors; + use starknet_ibc::apps::transfer::interface::{ISendTransfer, IRecvPacket, ITokenAddress}; use starknet_ibc::apps::transfer::types::{ - MsgTransfer, Packet, Token, Denom, DenomTrait, Memo, MAXIMUM_MEMO_LENGTH + MsgTransfer, PrefixedDenom, Denom, DenomTrait, PacketData, TracePrefix, Memo, + TracePrefixTrait, ERC20TokenTrait, ERC20Token, MAXIMUM_MEMO_LENGTH, ValidateBasicTrait, + PrefixedDenomTrait }; - use starknet_ibc::core::types::{PortId, ChannelId}; + use starknet_ibc::apps::transferrable::interface::ITransferrable; + use starknet_ibc::core::channel::types::Packet; + use starknet_ibc::core::host::types::{PortId, ChannelId, ChannelIdTrait}; #[storage] struct Storage { erc20_class_hash: ClassHash, salt: felt252, - governor: ContractAddress, - send_capability: bool, - receive_capability: bool, - minted_token_name_to_address: LegacyMap, - minted_token_address_to_name: LegacyMap, + ibc_token_name_to_address: LegacyMap, + ibc_token_address_to_name: LegacyMap, } #[event] #[derive(Debug, Drop, starknet::Event)] pub enum Event { - TransferEvent: TransferEvent, + SendEvent: SendEvent, RecvEvent: RecvEvent, } #[derive(Debug, Drop, Serde, starknet::Event)] - pub struct TransferEvent { - sender: ContractAddress, - receiver: ContractAddress, - amount: u256, - denom: Denom, - memo: Memo, + pub struct SendEvent { + #[key] + pub sender: ContractAddress, + #[key] + pub receiver: ContractAddress, + #[key] + pub denom: PrefixedDenom, + pub amount: u256, + pub memo: Memo, } #[derive(Debug, Drop, Serde, starknet::Event)] pub struct RecvEvent { - sender: ContractAddress, - receiver: ContractAddress, - denom: Denom, - amount: u256, - memo: Memo, - success: bool, + #[key] + pub sender: ContractAddress, + #[key] + pub receiver: ContractAddress, + #[key] + pub denom: PrefixedDenom, + pub amount: u256, + pub memo: Memo, + pub success: bool, } #[embeddable_as(SendTransfer)] impl SendTransferImpl< - TContractState, +HasComponent, +Drop + TContractState, + +HasComponent, + +ITransferrable, + +Drop > of ISendTransfer> { fn send_validate(self: @ComponentState, msg: MsgTransfer) { - self._send_validate(msg); - } - - fn send_execute(ref self: ComponentState, msg: MsgTransfer) { - self._send_execute(msg); - } - } + self.get_contract().can_send(); - #[generate_trait] - impl SendTransferInternalImpl< - TContractState, +HasComponent, +Drop - > of SendTransferInternal { - fn _send_validate(self: @ComponentState, msg: MsgTransfer) -> Denom { - self.can_send(); - - assert(msg.packet_data.sender.is_non_zero(), ICS20Errors::INVALID_SENDER); - assert(!msg.packet_data.token.denom.is_zero(), ICS20Errors::INVALID_DENOM); - assert(msg.packet_data.token.amount.is_non_zero(), ICS20Errors::ZERO_AMOUNT); - assert( - msg.packet_data.memo.memo.len() < MAXIMUM_MEMO_LENGTH, - ICS20Errors::MAXIMUM_MEMO_LENGTH - ); + msg.validate_basic(); - match @msg.packet_data.token.denom { - Denom::Native(_) => { + match @msg.packet_data.denom.base { + Denom::Native(erc20_token) => { 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(), + erc20_token.clone(), + msg.packet_data.amount, msg.packet_data.memo.clone(), ); }, - Denom::IBC(_) => { + Denom::Hosted(_) => { self .burn_validate( msg.packet_data.sender.clone(), - msg.packet_data.token.clone(), + msg.packet_data.denom.clone(), + msg.packet_data.amount, msg.packet_data.memo.clone(), ); } } - - msg.packet_data.token.denom } - fn _send_execute(ref self: ComponentState, msg: MsgTransfer) -> Denom { - let denom = self._send_validate(msg.clone()); + fn send_execute(ref self: ComponentState, msg: MsgTransfer) { + self.send_validate(msg.clone()); - match @denom { - Denom::Native(_) => { + match @msg.packet_data.denom.base { + Denom::Native(erc20_token) => { 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(), + erc20_token.clone(), + msg.packet_data.amount, msg.packet_data.memo.clone(), ); }, - Denom::IBC(_) => { + Denom::Hosted(_) => { self .burn_execute( msg.packet_data.sender.clone(), - msg.packet_data.token.clone(), + msg.packet_data.denom.clone(), + msg.packet_data.amount, msg.packet_data.memo.clone(), ); } } - self - .emit( - TransferEvent { - sender: msg.packet_data.sender.clone(), - receiver: msg.packet_data.receiver.clone(), - amount: msg.packet_data.token.amount, - denom: denom.clone(), - memo: msg.packet_data.memo.clone(), - } - ); - - denom + self.emit_send_event(msg.packet_data); } } #[embeddable_as(RecvPacket)] impl RecvPacketImpl< - TContractState, +HasComponent, +Drop + TContractState, + +HasComponent, + +ITransferrable, + +Drop > of IRecvPacket> { fn recv_validate(self: @ComponentState, packet: Packet) { self._recv_validate(packet); @@ -170,96 +142,123 @@ pub mod ICS20TransferComponent { #[generate_trait] impl RecvPacketInternalImpl< - TContractState, +HasComponent, +Drop - > of RecvPacketInternal { - fn _recv_validate(self: @ComponentState, packet: Packet) -> Denom { - self.can_receive(); + TContractState, + +HasComponent, + +ITransferrable, + +Drop + > of RecvPacketInternalTrait { + fn _recv_validate(self: @ComponentState, packet: Packet) -> PacketData { + self.get_contract().can_receive(); - assert(packet.data.receiver.is_non_zero(), ICS20Errors::INVALID_RECEIVEER); - assert(!packet.data.token.denom.is_zero(), ICS20Errors::INVALID_DENOM); - assert(packet.data.token.amount.is_non_zero(), ICS20Errors::ZERO_AMOUNT); + let mut pakcet_data_span = packet.data.span(); - match @packet.data.token.denom { - Denom::Native(_) => { + let maybe_packet_data: Option = Serde::deserialize(ref pakcet_data_span); + + assert(maybe_packet_data.is_some(), TransferErrors::INVALID_PACKET_DATA); + + let packet_date = maybe_packet_data.unwrap(); + + packet_date.validate_basic(); + + match @packet_date.denom.base { + Denom::Native(erc20_token) => { self .unescrow_validate( - packet.data.receiver.clone(), + packet_date.receiver.clone(), packet.port_id_on_a.clone(), packet.chan_id_on_a.clone(), - packet.data.token.clone(), + erc20_token.clone(), + packet_date.amount, ); }, - Denom::IBC(_) => { - self.mint_validate(packet.data.receiver.clone(), packet.data.token.clone(),); + Denom::Hosted(_) => { + self + .mint_validate( + packet_date.receiver.clone(), + packet_date.denom.clone(), + packet_date.amount + ); } } - packet.data.token.denom + packet_date } - fn _recv_execute(ref self: ComponentState, packet: Packet) -> Denom { - let denom = self._recv_validate(packet.clone()); + fn _recv_execute(ref self: ComponentState, packet: Packet) -> PacketData { + let mut packet_data = self._recv_validate(packet.clone()); + + let trace_prefix = TracePrefixTrait::new( + packet.port_id_on_a.clone(), packet.chan_id_on_a.clone() + ); + + match @packet_data.denom.base { + Denom::Native(erc20_token) => { + packet_data.denom.remove_prefix(@trace_prefix); - match @denom { - Denom::Native(_) => { self .unescrow_execute( - packet.data.receiver.clone(), + packet_data.receiver.clone(), packet.port_id_on_a.clone(), packet.chan_id_on_a.clone(), - packet.data.token.clone(), - ); + erc20_token.clone(), + packet_data.amount, + ) }, - Denom::IBC(_) => { - self.mint_execute(packet.data.receiver.clone(), packet.data.token.clone(),); + Denom::Hosted(_) => { + packet_data.denom.add_prefix(trace_prefix); + + self + .mint_execute( + packet_data.receiver.clone(), + packet_data.denom.clone(), + packet_data.amount + ) } - } + }; - self - .emit( - RecvEvent { - sender: packet.data.sender.clone(), - receiver: packet.data.receiver.clone(), - denom: denom.clone(), - amount: packet.data.token.amount, - memo: packet.data.memo.clone(), - success: true, - } - ); + self.emit_recv_event(packet_data.clone(), true); - denom + packet_data } } - #[embeddable_as(TransferrableImpl)] - pub impl Transferrable< + #[embeddable_as(IBCTokenAddress)] + impl ITokenAddressImpl< 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); + > of ITokenAddress> { + fn ibc_token_address( + self: @ComponentState, prefixed_denom: PrefixedDenom + ) -> Option { + let denom_key = prefixed_denom.compute_key(); + + let token_address = self.ibc_token_name_to_address.read(denom_key); + + if token_address.is_non_zero() { + Option::Some(token_address) + } else { + Option::None + } } } #[generate_trait] pub(crate) impl TransferValidationImpl< - TContractState, +HasComponent, + TContractState, +HasComponent, +Drop > of TransferValidationTrait { fn escrow_validate( self: @ComponentState, from_account: ContractAddress, port_id: PortId, channel_id: ChannelId, - token: Token, + denom: ERC20Token, + amount: u256, memo: Memo, ) { - let contract_address = token.denom.native().unwrap(); - let balance = ERC20ABIDispatcher { contract_address }.balance_of(from_account); - assert(balance > token.amount, ICS20Errors::INSUFFICIENT_BALANCE); + let balance = denom.balance_of(from_account); + + assert(balance >= amount, TransferErrors::INSUFFICIENT_BALANCE); + + self.assert_non_ibc_token(denom, port_id, channel_id); } fn unescrow_validate( @@ -267,29 +266,41 @@ pub mod ICS20TransferComponent { to_account: ContractAddress, port_id: PortId, channel_id: ChannelId, - token: Token, + denom: ERC20Token, + amount: u256, ) { - let contract_address = token.denom.native().unwrap(); - let balance = ERC20ABIDispatcher { contract_address } - .balance_of(get_contract_address()); - assert(token.amount > balance, ICS20Errors::INSUFFICIENT_BALANCE); + let balance = denom.balance_of(get_contract_address()); + + assert(balance >= amount, TransferErrors::INSUFFICIENT_BALANCE); + + self.assert_non_ibc_token(denom, port_id, channel_id); } fn mint_validate( - self: @ComponentState, account: ContractAddress, token: Token, - ) {} + self: @ComponentState, + account: ContractAddress, + denom: PrefixedDenom, + amount: u256, + ) { // NOTE: Normally, the minting process does not require any checks. + // However, an implementer might choose to incorporate custom + // checks, such as blacklisting. + } fn burn_validate( self: @ComponentState, account: ContractAddress, - token: Token, + denom: PrefixedDenom, + amount: u256, memo: Memo, ) { - let contract_address = self - .minted_token_name_to_address - .read(token.denom.ibc().unwrap()); - let balance = ERC20ABIDispatcher { contract_address }.balance_of(account); - assert(token.amount > balance, ICS20Errors::INSUFFICIENT_BALANCE); + let token_address: ERC20Token = self + .ibc_token_name_to_address + .read(denom.compute_key()) + .into(); + + let balance = token_address.balance_of(account); + + assert(balance >= amount, TransferErrors::INSUFFICIENT_BALANCE); } } @@ -300,14 +311,11 @@ pub mod ICS20TransferComponent { fn escrow_execute( ref self: ComponentState, from_account: ContractAddress, - port_id: PortId, - channel_id: ChannelId, - token: Token, + denom: ERC20Token, + amount: u256, memo: Memo, ) { - let contract_address = token.denom.native().unwrap(); - ERC20ABIDispatcher { contract_address } - .transfer_from(from_account, get_contract_address(), token.amount); + denom.transfer_from(from_account, get_contract_address(), amount); } fn unescrow_execute( @@ -315,31 +323,44 @@ pub mod ICS20TransferComponent { to_account: ContractAddress, port_id: PortId, channel_id: ChannelId, - token: Token, - ) {} + denom: ERC20Token, + amount: u256, + ) { + denom.transfer(to_account, amount); + } fn mint_execute( - ref self: ComponentState, account: ContractAddress, token: Token, + ref self: ComponentState, + account: ContractAddress, + denom: PrefixedDenom, + amount: u256, ) { - let ibc_denom = token.denom.ibc().unwrap(); - - let contract_address = self.minted_token_name_to_address.read(ibc_denom); + let token_address: ERC20Token = self + .ibc_token_name_to_address + .read(denom.compute_key()) + .into(); - let contract_address = if contract_address.is_non_zero() { - contract_address + if token_address.is_non_zero() { + token_address.mint(account, amount); } else { - self.create_token(account, token) - }; - - ERC20ABIDispatcher { contract_address }.transfer(account, token.amount); + self.create_token(account, denom, amount); + } } fn burn_execute( ref self: ComponentState, account: ContractAddress, - token: Token, + denom: PrefixedDenom, + amount: u256, memo: Memo, - ) {} + ) { + let token_address: ERC20Token = self + .ibc_token_name_to_address + .read(denom.compute_key()) + .into(); + + token_address.burn(account, amount); + } } #[generate_trait] @@ -347,34 +368,103 @@ pub mod ICS20TransferComponent { TContractState, +HasComponent, +Drop > of TransferInternalTrait { fn initializer(ref self: ComponentState, erc20_class_hash: ClassHash) { - self.governor.write(get_caller_address()); self.erc20_class_hash.write(erc20_class_hash); self.salt.write(0); - self.send_capability.write(true); - self.receive_capability.write(true); } fn create_token( - ref self: ComponentState, account: ContractAddress, token: Token + ref self: ComponentState, + account: ContractAddress, + denom: PrefixedDenom, + amount: u256, ) -> ContractAddress { let salt = self.salt.read(); - let mut call_data = array![]; - call_data.append_serde(token.denom.clone()); - call_data - .append_serde(token.denom); // TODO: determine what should be set as symbol here. - call_data.append_serde(token.amount); - call_data.append_serde(account); - call_data.append_serde(get_contract_address()); + let name = denom.base.hosted().unwrap(); - let (address, _) = deploy_syscall( - self.erc20_class_hash.read(), salt, call_data.span(), false, - ) - .unwrap_syscall(); + let erc20_token = ERC20TokenTrait::create( + self.erc20_class_hash.read(), + salt, + name.clone(), + name, // TODO: Detemine the symbol + amount, + account, + get_contract_address() + ); + + self.record_ibc_token(denom, erc20_token.address); self.salt.write(salt + 1); - address + erc20_token.address + } + + fn record_ibc_token( + ref self: ComponentState, + denom: PrefixedDenom, + token_address: ContractAddress, + ) { + let denom_key = denom.compute_key(); + + self.ibc_token_name_to_address.write(denom_key, token_address); + + self.ibc_token_address_to_name.write(token_address, denom_key); + } + + fn assert_non_ibc_token( + self: @ComponentState, + denom: ERC20Token, + port_id: PortId, + channel_id: ChannelId, + ) { + let token_key = self.ibc_token_address_to_name.read(denom.address); + + if token_key.is_non_zero() { + let trace_prefix = TracePrefixTrait::new(port_id, channel_id); + + let denom = PrefixedDenom { + trace_path: array![trace_prefix], base: Denom::Native(denom), + }; + + // Checks if the token is an IBC-created token. If so, it cannot + // be transferred back to the source by escrowing. A prefixed + // denom should be passed to burn instead. + assert(token_key == denom.compute_key(), TransferErrors::INVALID_DENOM); + } + } + } + + #[generate_trait] + pub(crate) impl TransferEventImpl< + TContractState, +HasComponent, +Drop + > of TransferEventTrait { + fn emit_send_event(ref self: ComponentState, packet_date: PacketData,) { + self + .emit( + SendEvent { + sender: packet_date.sender, + receiver: packet_date.receiver, + denom: packet_date.denom, + amount: packet_date.amount, + memo: packet_date.memo, + } + ); + } + + fn emit_recv_event( + ref self: ComponentState, packet_date: PacketData, success: bool, + ) { + self + .emit( + RecvEvent { + sender: packet_date.sender, + receiver: packet_date.receiver, + denom: packet_date.denom, + amount: packet_date.amount, + memo: packet_date.memo, + success, + } + ); } } } diff --git a/contracts/src/apps/transfer/errors.cairo b/contracts/src/apps/transfer/errors.cairo index ac290fd5..3f715725 100644 --- a/contracts/src/apps/transfer/errors.cairo +++ b/contracts/src/apps/transfer/errors.cairo @@ -1,11 +1,10 @@ -pub mod ICS20Errors { - pub const NO_SEND_CAPABILITY: felt252 = 'ICS20: No send capability'; - pub const NO_RECEIVE_CAPABILITY: felt252 = 'ICS20: No receive capability'; +pub mod TransferErrors { pub const INVALID_SENDER: felt252 = 'ICS20: Invalid sender account'; - pub const INVALID_RECEIVEER: felt252 = 'ICS20: Invalid receiver account'; + pub const INVALID_RECEIVER: felt252 = 'ICS20: Invalid receiver account'; pub const ZERO_AMOUNT: felt252 = 'ICS20: transfer amount is 0'; pub const INVALID_TOKEN_ADDRESS: felt252 = 'ICS20: invalid token address'; pub const INVALID_DENOM: felt252 = 'ICS20: invalid denom'; + pub const INVALID_PACKET_DATA: felt252 = 'ICS20: invalid packet data'; 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 8b99f8b6..94b1102c 100644 --- a/contracts/src/apps/transfer/interface.cairo +++ b/contracts/src/apps/transfer/interface.cairo @@ -1,6 +1,6 @@ use starknet::ContractAddress; -use starknet_ibc::apps::transfer::types::{MsgTransfer, Packet, Memo}; -use starknet_ibc::core::types::{PortId, ChannelId}; +use starknet_ibc::apps::transfer::types::{MsgTransfer, PrefixedDenom}; +use starknet_ibc::core::channel::types::Packet; #[starknet::interface] pub trait ISendTransfer { @@ -15,8 +15,9 @@ pub trait IRecvPacket { } #[starknet::interface] -pub trait ITransferrable { - fn can_send(self: @TContractState); - fn can_receive(self: @TContractState); +pub trait ITokenAddress { + fn ibc_token_address( + self: @TContractState, prefixed_denom: PrefixedDenom + ) -> Option; } diff --git a/contracts/src/apps/transfer/types.cairo b/contracts/src/apps/transfer/types.cairo index 88405baf..f73e8bb0 100644 --- a/contracts/src/apps/transfer/types.cairo +++ b/contracts/src/apps/transfer/types.cairo @@ -1,92 +1,251 @@ -use core::array::ArrayTrait; -use core::byte_array::ByteArrayTrait; -use core::num::traits::zero::Zero; +use core::num::traits::Zero; use core::serde::Serde; +use core::starknet::SyscallResultTrait; use core::to_byte_array::FormatAsByteArray; +use core::traits::Into; use core::traits::TryInto; -use openzeppelin::utils::selectors; +use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ClassHash; use starknet::ContractAddress; use starknet::Store; -use starknet::contract_address_const; -use starknet::syscalls::call_contract_syscall; -use starknet_ibc::core::types::{PortId, ChannelId, Sequence, Height, Timestamp}; -use starknet_ibc::utils::bytes_to_felt252; +use starknet::syscalls::deploy_syscall; +use starknet_ibc::apps::mintable::interface::{ + IERC20MintableDispatcher, IERC20MintableDispatcherTrait +}; +use starknet_ibc::apps::transfer::errors::TransferErrors; +use starknet_ibc::core::client::types::{Height, Timestamp}; +use starknet_ibc::core::host::types::{PortId, PortIdTrait, ChannelId, ChannelIdTrait, Sequence}; /// Maximum memo length allowed for ICS-20 transfers. This bound corresponds to /// the `MaximumMemoLength` in the `ibc-go`. pub(crate) const MAXIMUM_MEMO_LENGTH: u32 = 32768; +/// Message used to build an ICS20 token transfer packet. #[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(Clone, Debug, Drop, Serde, Store)] -pub struct Packet { - pub seq_on_a: Sequence, - pub port_id_on_a: PortId, - pub chan_id_on_a: ChannelId, - pub port_id_on_b: PortId, - pub chan_id_on_b: ChannelId, - pub data: PacketData, pub timeout_height_on_b: Height, pub timeout_timestamp_on_b: Timestamp, } +impl MsgTransferValidateBasicImpl of ValidateBasicTrait { + fn validate_basic(self: @MsgTransfer) { + // self.port_id_on_a.validate(); + // self.chan_id_on_a.validate(); + self.packet_data.validate_basic(); + } +} + #[derive(Clone, Debug, Drop, Serde, Store)] pub struct PacketData { - pub token: Token, + pub denom: PrefixedDenom, + pub amount: u256, pub sender: ContractAddress, pub receiver: ContractAddress, pub memo: Memo, } +impl PacketDataValidateBasicImpl of ValidateBasicTrait { + fn validate_basic(self: @PacketData) { + assert(self.sender.is_non_zero(), TransferErrors::INVALID_SENDER); + assert(self.receiver.is_non_zero(), TransferErrors::INVALID_RECEIVER); + assert(self.denom.base.is_non_zero(), TransferErrors::INVALID_DENOM); + assert(self.amount.is_non_zero(), TransferErrors::ZERO_AMOUNT); + self.memo.validate_basic(); + } +} + #[derive(Clone, Debug, Drop, Serde, Store)] -pub struct Token { - pub denom: Denom, - pub amount: u256, +pub struct PrefixedDenom { + pub trace_path: Array, + pub base: Denom, +} + +pub trait PrefixedDenomTrait { + fn starts_with(self: @PrefixedDenom, prefix: @TracePrefix) -> bool; + fn add_prefix(ref self: PrefixedDenom, prefix: TracePrefix); + fn remove_prefix(ref self: PrefixedDenom, prefix: @TracePrefix); + fn compute_key(self: @PrefixedDenom) -> felt252; +} + +impl PrefixedDenomImpl of PrefixedDenomTrait { + fn starts_with(self: @PrefixedDenom, prefix: @TracePrefix) -> bool { + self.trace_path.at(0) == prefix + } + + fn add_prefix(ref self: PrefixedDenom, prefix: TracePrefix) { + self.trace_path.append(prefix); + } + + fn remove_prefix(ref self: PrefixedDenom, prefix: @TracePrefix) { + if self.starts_with(prefix) { + self.trace_path.pop_front().unwrap(); + } + } + + fn compute_key(self: @PrefixedDenom) -> felt252 { + 1 + } +} + +#[derive(Clone, Debug, Drop, PartialEq, Eq, Serde, Store)] +pub struct TracePrefix { + pub port_id: PortId, + pub channel_id: ChannelId, +} + +pub trait TracePrefixTrait { + fn new(port_id: PortId, channel_id: ChannelId) -> TracePrefix; + fn shorthand(self: @TracePrefix) -> ByteArray; +} + +impl TracePrefixImpl of TracePrefixTrait { + fn new(port_id: PortId, channel_id: ChannelId) -> TracePrefix { + TracePrefix { port_id: port_id, channel_id: channel_id, } + } + + fn shorthand(self: @TracePrefix) -> ByteArray { + assert(self.port_id == @PortIdTrait::transfer(), 'port_id must be transfer'); + let mut shorthand: ByteArray = ""; + shorthand.append(@"tr"); + shorthand.append(@"/"); + shorthand.append(@self.channel_id.index().format_as_byte_array(10)); + shorthand + } } #[derive(Clone, Debug, Drop, Serde, Store)] pub enum Denom { - Native: ContractAddress, - IBC: ByteArray, + Native: ERC20Token, + Hosted: ByteArray, +} + +#[derive(Clone, Debug, Drop, Serde, Store)] +pub struct ERC20Token { + pub address: ContractAddress, +} + +impl ContractAddressIntoTokenAddr of Into { + fn into(self: ContractAddress) -> ERC20Token { + ERC20Token { address: self } + } +} + +impl ERC20TokenIntoFelt252 of Into { + fn into(self: ERC20Token) -> felt252 { + self.address.into() + } +} + +pub trait ERC20TokenTrait { + fn is_non_zero(self: @ERC20Token) -> bool; + fn create( + class_hash: ClassHash, + salt: felt252, + name: ByteArray, + symbol: ByteArray, + amount: u256, + recipient: ContractAddress, + owner: ContractAddress + ) -> ERC20Token; + fn transfer(self: @ERC20Token, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + self: @ERC20Token, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn mint(self: @ERC20Token, recipient: ContractAddress, amount: u256); + fn burn(self: @ERC20Token, account: ContractAddress, amount: u256); + fn balance_of(self: @ERC20Token, from_account: ContractAddress) -> u256; +} + +impl ERC20TokenImpl of ERC20TokenTrait { + fn is_non_zero(self: @ERC20Token) -> bool { + self.address.is_non_zero() + } + + fn create( + class_hash: ClassHash, + salt: felt252, + name: ByteArray, + symbol: ByteArray, + amount: u256, + recipient: ContractAddress, + owner: ContractAddress + ) -> ERC20Token { + let mut call_data = array![]; + + call_data.append_serde(name); + call_data.append_serde(symbol); + call_data.append_serde(amount); + call_data.append_serde(recipient); + call_data.append_serde(owner); + + let (address, _) = deploy_syscall(class_hash, salt, call_data.span(), false,) + .unwrap_syscall(); + + ERC20Token { address } + } + + fn transfer(self: @ERC20Token, recipient: ContractAddress, amount: u256) -> bool { + ERC20ABIDispatcher { contract_address: *self.address }.transfer(recipient, amount) + } + + fn transfer_from( + self: @ERC20Token, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool { + ERC20ABIDispatcher { contract_address: *self.address } + .transfer_from(sender, recipient, amount) + } + + fn mint(self: @ERC20Token, recipient: ContractAddress, amount: u256) { + IERC20MintableDispatcher { contract_address: *self.address } + .permissioned_mint(recipient, amount) + } + + fn burn(self: @ERC20Token, account: ContractAddress, amount: u256) { + IERC20MintableDispatcher { contract_address: *self.address } + .permissioned_burn(account, amount) + } + + fn balance_of(self: @ERC20Token, from_account: ContractAddress) -> u256 { + ERC20ABIDispatcher { contract_address: *self.address }.balance_of(from_account) + } } pub trait DenomTrait { - fn is_zero(self: @Denom) -> bool; + fn is_non_zero(self: @Denom) -> bool; fn native(self: @Denom) -> Option; - fn ibc(self: @Denom) -> Option; + fn hosted(self: @Denom) -> Option; } pub impl DenomImpl of DenomTrait { - fn is_zero(self: @Denom) -> bool { + fn is_non_zero(self: @Denom) -> bool { match self { - Denom::Native(contract_address) => contract_address.is_zero(), - Denom::IBC(byte_array) => byte_array.len() == 0, + Denom::Native(token_addr) => token_addr.is_non_zero(), + Denom::Hosted(byte_array) => byte_array.len() != 0, } } fn native(self: @Denom) -> Option { match self { - Denom::Native(contract_address) => Option::Some(*contract_address), - Denom::IBC(_) => Option::None, + Denom::Native(contract_address) => Option::Some(*contract_address.address), + Denom::Hosted(_) => Option::None, } } - fn ibc(self: @Denom) -> Option { + fn hosted(self: @Denom) -> Option { match self { Denom::Native(_) => Option::None, - Denom::IBC(byte_array) => bytes_to_felt252(byte_array), + Denom::Hosted(base) => Option::Some(base.clone()) } } } pub impl ContractAddressIntoDenom of Into { fn into(self: ContractAddress) -> Denom { - Denom::Native(self) + Denom::Native(ERC20Token { address: self }) } } @@ -94,3 +253,13 @@ pub impl ContractAddressIntoDenom of Into { pub struct Memo { pub memo: ByteArray, } + +impl MemoValidateBasicImpl of ValidateBasicTrait { + fn validate_basic(self: @Memo) { + assert(self.memo.len() <= MAXIMUM_MEMO_LENGTH, TransferErrors::MAXIMUM_MEMO_LENGTH); + } +} + +pub trait ValidateBasicTrait { + fn validate_basic(self: @T); +} diff --git a/contracts/src/apps/transferrable.cairo b/contracts/src/apps/transferrable.cairo new file mode 100644 index 00000000..3e1ae43c --- /dev/null +++ b/contracts/src/apps/transferrable.cairo @@ -0,0 +1,3 @@ +pub mod component; +pub mod errors; +pub mod interface; diff --git a/contracts/src/apps/transferrable/component.cairo b/contracts/src/apps/transferrable/component.cairo new file mode 100644 index 00000000..74e51c7f --- /dev/null +++ b/contracts/src/apps/transferrable/component.cairo @@ -0,0 +1,40 @@ +#[starknet::component] +pub mod TransferrableComponent { + use starknet_ibc::apps::transferrable::errors::TransferrableErrors; + use starknet_ibc::apps::transferrable::interface::ITransferrable; + + #[storage] + struct Storage { + send_capability: bool, + receive_capability: bool, + } + + #[event] + #[derive(Drop, Debug, starknet::Event)] + pub enum Event {} + + #[embeddable_as(Transferrable)] + pub impl TransferrableImpl< + TContractState, +HasComponent, +Drop + > of ITransferrable> { + fn can_send(self: @ComponentState) { + let send_capability = self.send_capability.read(); + assert(send_capability, TransferrableErrors::NO_SEND_CAPABILITY); + } + fn can_receive(self: @ComponentState) { + let receive_capability = self.receive_capability.read(); + assert(receive_capability, TransferrableErrors::NO_RECEIVE_CAPABILITY); + } + } + + #[generate_trait] + pub impl TransferrableInternalImpl< + TContractState, +HasComponent, +Drop + > of TransferrableInternalTrait { + fn initializer(ref self: ComponentState) { + self.send_capability.write(true); + self.receive_capability.write(true); + } + } +} + diff --git a/contracts/src/apps/transferrable/errors.cairo b/contracts/src/apps/transferrable/errors.cairo new file mode 100644 index 00000000..0cf9b542 --- /dev/null +++ b/contracts/src/apps/transferrable/errors.cairo @@ -0,0 +1,4 @@ +pub mod TransferrableErrors { + pub const NO_SEND_CAPABILITY: felt252 = 'ICS20: No send capability'; + pub const NO_RECEIVE_CAPABILITY: felt252 = 'ICS20: No receive capability'; +} diff --git a/contracts/src/apps/transferrable/interface.cairo b/contracts/src/apps/transferrable/interface.cairo new file mode 100644 index 00000000..0a62332c --- /dev/null +++ b/contracts/src/apps/transferrable/interface.cairo @@ -0,0 +1,6 @@ +#[starknet::interface] +pub trait ITransferrable { + fn can_send(self: @TContractState); + fn can_receive(self: @TContractState); +} + diff --git a/contracts/src/core.cairo b/contracts/src/core.cairo index cd408564..ac81ab6e 100644 --- a/contracts/src/core.cairo +++ b/contracts/src/core.cairo @@ -1 +1,3 @@ -pub mod types; +pub mod channel; +pub mod client; +pub mod host; diff --git a/contracts/src/core/channel.cairo b/contracts/src/core/channel.cairo new file mode 100644 index 00000000..cd408564 --- /dev/null +++ b/contracts/src/core/channel.cairo @@ -0,0 +1 @@ +pub mod types; diff --git a/contracts/src/core/channel/types.cairo b/contracts/src/core/channel/types.cairo new file mode 100644 index 00000000..43acd353 --- /dev/null +++ b/contracts/src/core/channel/types.cairo @@ -0,0 +1,14 @@ +use starknet_ibc::core::client::types::{Height, Timestamp}; +use starknet_ibc::core::host::types::{ChannelId, PortId, Sequence}; + +#[derive(Clone, Debug, Drop, Serde, Store)] +pub struct Packet { + pub seq_on_a: Sequence, + pub port_id_on_a: PortId, + pub chan_id_on_a: ChannelId, + pub port_id_on_b: PortId, + pub chan_id_on_b: ChannelId, + pub data: Array, + pub timeout_height_on_b: Height, + pub timeout_timestamp_on_b: Timestamp, +} diff --git a/contracts/src/core/client.cairo b/contracts/src/core/client.cairo new file mode 100644 index 00000000..cd408564 --- /dev/null +++ b/contracts/src/core/client.cairo @@ -0,0 +1 @@ +pub mod types; diff --git a/contracts/src/core/client/types.cairo b/contracts/src/core/client/types.cairo new file mode 100644 index 00000000..f6cbca37 --- /dev/null +++ b/contracts/src/core/client/types.cairo @@ -0,0 +1,10 @@ +#[derive(Clone, Debug, Drop, PartialEq, Eq, Serde, Store)] +pub struct Height { + pub revision_number: u64, + pub revision_height: u64, +} + +#[derive(Clone, Debug, Drop, PartialEq, Eq, Serde, Store)] +pub struct Timestamp { + pub timestamp: u64, +} diff --git a/contracts/src/core/host.cairo b/contracts/src/core/host.cairo new file mode 100644 index 00000000..422d6694 --- /dev/null +++ b/contracts/src/core/host.cairo @@ -0,0 +1,2 @@ +pub mod errors; +pub mod types; diff --git a/contracts/src/core/host/errors.cairo b/contracts/src/core/host/errors.cairo new file mode 100644 index 00000000..bcf5ad81 --- /dev/null +++ b/contracts/src/core/host/errors.cairo @@ -0,0 +1,6 @@ +pub mod HostErrors { + pub const INVALID_IDENTIFIER_LENGTH: felt252 = 'ICS24: invalid ID length'; + pub const INVALID_IDENTIFIER_PREFIX: felt252 = 'ICS24: invalid ID prefix'; + pub const INVALID_IDENTIFIER_INDEX: felt252 = 'ICS24: invalid ID index'; + pub const INVALID_IDENTIFIER_CHAR: felt252 = 'ICS24: invalid ID character'; +} diff --git a/contracts/src/core/host/types.cairo b/contracts/src/core/host/types.cairo new file mode 100644 index 00000000..c54bef89 --- /dev/null +++ b/contracts/src/core/host/types.cairo @@ -0,0 +1,146 @@ +use core::byte_array::ByteArrayTrait; +use core::to_byte_array::FormatAsByteArray; +use core::traits::TryInto; +use starknet::ContractAddress; +use starknet::Store; +use starknet_ibc::core::host::errors::HostErrors; +use starknet_ibc::utils::{ToFelt252Trait, bytes_to_felt252}; + +#[derive(Clone, Debug, Drop, PartialEq, Eq, Serde, Store)] +pub struct ChannelId { + pub channel_id: ByteArray, +} + +pub trait ChannelIdTrait { + fn new(index: u64) -> ChannelId; + fn index(self: @ChannelId) -> u64; + fn validate(self: @ChannelId); + fn abbr(self: @ChannelId) -> ByteArray; +} + +pub impl ChannelIdImpl of ChannelIdTrait { + fn new(index: u64) -> ChannelId { + let mut channel_id: ByteArray = ""; + channel_id.append(@"channel-"); + channel_id.append(@index.format_as_byte_array(10)); + ChannelId { channel_id } + } + + fn index(self: @ChannelId) -> u64 { + let mut i = self.channel_id.len() - 1; + let mut j: u64 = 0; + let mut index: u64 = 0; + + loop { + if i == 8 { + break; + } + let char_byte = self.channel_id.at(i).unwrap(); + index += j * 10 * (char_byte - 48).into(); + i -= 1; + j += 1; + }; + + index + } + + fn validate(self: @ChannelId) { + let channel_id_len = self.channel_id.len(); + + assert(channel_id_len > 8, HostErrors::INVALID_IDENTIFIER_LENGTH); + assert(channel_id_len < 32, HostErrors::INVALID_IDENTIFIER_LENGTH); + + let prefix: ByteArray = "channel-"; + + let mut i = 0; + + loop { + if i == channel_id_len - 1 { + break; + } + + let char_byte = self.channel_id.at(i).unwrap(); + + validate_char(char_byte); + + if i <= 7 { + assert(char_byte == prefix.at(i).unwrap(), HostErrors::INVALID_IDENTIFIER_PREFIX); + } else { + /// Checks if the index starts with 0 it does not contain any leading zeros. + if i == 8 && char_byte == 48 { + assert(i == channel_id_len - 1, HostErrors::INVALID_IDENTIFIER_INDEX); + } + assert_numeric(char_byte); + } + } + } + + fn abbr(self: @ChannelId) -> ByteArray { + let mut abbr: ByteArray = ""; + abbr.append(@"ch"); + abbr.append(@self.index().format_as_byte_array(10)); + abbr + } +} + +impl ChannelIdToFelt252 of ToFelt252Trait { + fn try_to_felt252(self: @ChannelId) -> Option { + bytes_to_felt252(self.channel_id) + } +} + +#[derive(Clone, Debug, Drop, PartialEq, Eq, Serde, Store)] +pub struct PortId { + pub port_id: ByteArray, +} + +pub trait PortIdTrait { + fn validate(self: @PortId); + fn transfer() -> PortId; +} + +pub impl PortIdImpl of PortIdTrait { + fn validate(self: @PortId) { + let port_id_len = self.port_id.len(); + + assert(port_id_len > 2, HostErrors::INVALID_IDENTIFIER_LENGTH); + assert(port_id_len < 32, HostErrors::INVALID_IDENTIFIER_LENGTH); + + let mut i = 0; + + loop { + if i == port_id_len - 1 { + break; + } + + let char_byte = self.port_id.at(i).unwrap(); + + validate_char(char_byte); + } + } + + fn transfer() -> PortId { + PortId { port_id: "transfer", } + } +} + +impl PortIdToFelt252 of ToFelt252Trait { + fn try_to_felt252(self: @PortId) -> Option { + bytes_to_felt252(self.port_id) + } +} + +#[derive(Clone, Debug, Drop, PartialEq, Eq, Serde, Store)] +pub struct Sequence { + pub sequence: u64, +} + +/// Validates if the given byte is a valid identifier character. +pub(crate) fn validate_char(char_bytes: u8) { + assert(char_bytes != 47, HostErrors::INVALID_IDENTIFIER_CHAR); // '/' + assert(char_bytes >= 33, HostErrors::INVALID_IDENTIFIER_CHAR); // Non-printable ASCII characters +} + +pub(crate) fn assert_numeric(char_bytes: u8) { + assert(char_bytes >= 48 && char_bytes <= 57, HostErrors::INVALID_IDENTIFIER_CHAR); +} diff --git a/contracts/src/core/types.cairo b/contracts/src/core/types.cairo deleted file mode 100644 index 232f2267..00000000 --- a/contracts/src/core/types.cairo +++ /dev/null @@ -1,28 +0,0 @@ -use starknet::ContractAddress; -use starknet::Store; - -#[derive(Clone, Debug, Drop, Serde, Store)] -pub struct Height { - pub revision_number: u64, - pub revision_height: u64, -} - -#[derive(Clone, Debug, Drop, Serde, Store)] -pub struct Timestamp { - pub timestamp: u64, -} - -#[derive(Clone, Debug, Drop, Serde, Store)] -pub struct ChannelId { - pub channel_id: felt252, -} - -#[derive(Clone, Debug, Drop, Serde, Store)] -pub struct PortId { - pub port_id: felt252, -} - -#[derive(Clone, Debug, Drop, Serde, Store)] -pub struct Sequence { - pub sequence: u64, -} diff --git a/contracts/src/presets.cairo b/contracts/src/presets.cairo index 82d2aaf2..214f2d13 100644 --- a/contracts/src/presets.cairo +++ b/contracts/src/presets.cairo @@ -1,4 +1,3 @@ -#[cfg(test)] mod erc20; mod transfer; diff --git a/contracts/src/presets/erc20.cairo b/contracts/src/presets/erc20.cairo index 7eaf585f..43ba780e 100644 --- a/contracts/src/presets/erc20.cairo +++ b/contracts/src/presets/erc20.cairo @@ -3,8 +3,11 @@ pub(crate) mod ERC20 { use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::{ContractAddress, ClassHash}; + use starknet_ibc::apps::mintable::component::ERC20MintableComponent::ERC20MintableInternalTrait; + use starknet_ibc::apps::mintable::component::ERC20MintableComponent; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: ERC20MintableComponent, storage: mintable, event: MintableEvent); component!(path: ERC20Component, storage: erc20, event: ERC20Event); // Ownable Mixin @@ -12,6 +15,10 @@ pub(crate) mod ERC20 { impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl; impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // ERC20 Mintable + #[abi(embed_v0)] + impl ERC20MintableImpl = ERC20MintableComponent::ERC20Mintable; + // ERC20 Mixin #[abi(embed_v0)] impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; @@ -22,6 +29,8 @@ pub(crate) mod ERC20 { #[substorage(v0)] ownable: OwnableComponent::Storage, #[substorage(v0)] + mintable: ERC20MintableComponent::Storage, + #[substorage(v0)] erc20: ERC20Component::Storage, } @@ -31,6 +40,8 @@ pub(crate) mod ERC20 { #[flat] OwnableEvent: OwnableComponent::Event, #[flat] + MintableEvent: ERC20MintableComponent::Event, + #[flat] ERC20Event: ERC20Component::Event, } @@ -44,6 +55,7 @@ pub(crate) mod ERC20 { owner: ContractAddress ) { self.ownable.initializer(owner); + self.mintable.initializer(); self.erc20.initializer(name, symbol); self.erc20.mint(recipient, fixed_supply); } diff --git a/contracts/src/presets/transfer.cairo b/contracts/src/presets/transfer.cairo index e6092a36..4c2bf219 100644 --- a/contracts/src/presets/transfer.cairo +++ b/contracts/src/presets/transfer.cairo @@ -1,35 +1,58 @@ #[starknet::contract] pub(crate) mod Transfer { - use starknet::{ContractAddress, ClassHash}; + use starknet::ClassHash; + use starknet_ibc::apps::governance::component::IBCGovernanceComponent::GovernanceInternalTrait; + use starknet_ibc::apps::governance::component::IBCGovernanceComponent; use starknet_ibc::apps::transfer::component::ICS20TransferComponent; + use starknet_ibc::apps::transferrable::component::TransferrableComponent::TransferrableInternalTrait; + use starknet_ibc::apps::transferrable::component::TransferrableComponent; + component!(path: IBCGovernanceComponent, storage: governance, event: IBCGovernanceEvent); + component!(path: TransferrableComponent, storage: transferrable, event: TransferrableEvent); component!(path: ICS20TransferComponent, storage: transfer, event: ICS20TransferEvent); + #[abi(embed_v0)] + impl IBCGovernanceImpl = IBCGovernanceComponent::Governance; + #[abi(embed_v0)] + impl ICS20TransferreableImpl = + TransferrableComponent::Transferrable; #[abi(embed_v0)] impl ICS20SendTransferImpl = ICS20TransferComponent::SendTransfer; #[abi(embed_v0)] impl ICS20RecvPacketImpl = ICS20TransferComponent::RecvPacket; - impl ICS20TransferreableImpl = ICS20TransferComponent::Transferrable; + #[abi(embed_v0)] + impl ICS20TokenAddressImpl = + ICS20TransferComponent::IBCTokenAddress; impl TransferValidationImpl = ICS20TransferComponent::TransferValidationImpl; impl TransferExecutionImpl = ICS20TransferComponent::TransferExecutionImpl; impl TransferInternalImpl = ICS20TransferComponent::TransferInternalImpl; #[storage] struct Storage { + #[substorage(v0)] + governance: IBCGovernanceComponent::Storage, + #[substorage(v0)] + transferrable: TransferrableComponent::Storage, #[substorage(v0)] transfer: ICS20TransferComponent::Storage, } #[event] - #[derive(Drop, starknet::Event)] - enum Event { + #[derive(Debug, Drop, starknet::Event)] + pub enum Event { + #[flat] + IBCGovernanceEvent: IBCGovernanceComponent::Event, + #[flat] + TransferrableEvent: TransferrableComponent::Event, #[flat] ICS20TransferEvent: ICS20TransferComponent::Event, } #[constructor] fn constructor(ref self: ContractState, erc20_class_hash: ClassHash) { + self.governance.initializer(); + self.transferrable.initializer(); self.transfer.initializer(erc20_class_hash); } } diff --git a/contracts/src/tests.cairo b/contracts/src/tests.cairo index 652d8355..8a28d1f0 100644 --- a/contracts/src/tests.cairo +++ b/contracts/src/tests.cairo @@ -1,4 +1,6 @@ #[cfg(test)] +mod setup; +#[cfg(test)] mod transfer; mod utils; diff --git a/contracts/src/tests/setup.cairo b/contracts/src/tests/setup.cairo new file mode 100644 index 00000000..8c2a745f --- /dev/null +++ b/contracts/src/tests/setup.cairo @@ -0,0 +1,97 @@ +use core::option::OptionTrait; +use openzeppelin::tests::utils::{deploy, pop_log}; +use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use starknet::ContractAddress; +use starknet::testing; +use starknet_ibc::apps::transfer::component::ICS20TransferComponent::{ + Event as TransferEvent, SendEvent, RecvEvent +}; +use starknet_ibc::apps::transfer::interface::{ + ISendTransferDispatcher, IRecvPacketDispatcher, ITokenAddressDispatcher, +}; +use starknet_ibc::presets::{Transfer, ERC20}; +use starknet_ibc::tests::utils::dummy_erc20_call_data; + +#[derive(Clone, Debug, Drop, PartialEq, Eq)] +pub struct ICS20TransferContract { + pub contract_address: ContractAddress, +} + +pub trait ICS20TransferContractTrait { + fn setup() -> ICS20TransferContract; + fn send_dispatcher(self: @ICS20TransferContract) -> ISendTransferDispatcher; + fn recv_dispatcher(self: @ICS20TransferContract) -> IRecvPacketDispatcher; + fn addr_dispatcher(self: @ICS20TransferContract) -> ITokenAddressDispatcher; + fn pop_event(self: @ICS20TransferContract) -> Option; + fn assert_send_event(self: @ICS20TransferContract) -> SendEvent; + fn assert_recv_event(self: @ICS20TransferContract) -> RecvEvent; +} + +pub impl ICS20TransferContractImpl of ICS20TransferContractTrait { + fn setup() -> ICS20TransferContract { + let mut call_data = array![]; + call_data.append_serde(ERC20::TEST_CLASS_HASH); + + let contract_address = deploy(Transfer::TEST_CLASS_HASH, call_data); + + ICS20TransferContract { contract_address } + } + + fn send_dispatcher(self: @ICS20TransferContract) -> ISendTransferDispatcher { + ISendTransferDispatcher { contract_address: *self.contract_address } + } + + fn recv_dispatcher(self: @ICS20TransferContract) -> IRecvPacketDispatcher { + IRecvPacketDispatcher { contract_address: *self.contract_address } + } + + fn addr_dispatcher(self: @ICS20TransferContract) -> ITokenAddressDispatcher { + ITokenAddressDispatcher { contract_address: *self.contract_address } + } + + fn pop_event(self: @ICS20TransferContract) -> Option { + pop_log(*self.contract_address) + } + + fn assert_send_event(self: @ICS20TransferContract) -> SendEvent { + match self.pop_event().expect('no event') { + TransferEvent::SendEvent(e) => e, + _ => panic!("unexpected event"), + } + } + + fn assert_recv_event(self: @ICS20TransferContract) -> RecvEvent { + match self.pop_event().expect('no event') { + TransferEvent::RecvEvent(e) => e, + _ => panic!("unexpected event"), + } + } +} + +#[derive(Clone, Debug, Drop, PartialEq, Eq)] +pub struct ERC20Contract { + pub contract_address: ContractAddress, +} + +pub trait ERC20ContractTrait { + fn setup() -> ERC20Contract; + fn setup_with_addr(contract_address: ContractAddress) -> ERC20Contract; + fn dispatcher(self: @ERC20Contract) -> ERC20ABIDispatcher; +} + +pub impl ERC20ContractImpl of ERC20ContractTrait { + fn setup() -> ERC20Contract { + let contract_address = deploy(ERC20::TEST_CLASS_HASH, dummy_erc20_call_data()); + + ERC20Contract { contract_address } + } + + fn setup_with_addr(contract_address: ContractAddress) -> ERC20Contract { + ERC20Contract { contract_address } + } + + fn dispatcher(self: @ERC20Contract) -> ERC20ABIDispatcher { + ERC20ABIDispatcher { contract_address: *self.contract_address } + } +} diff --git a/contracts/src/tests/transfer.cairo b/contracts/src/tests/transfer.cairo index 24b12613..de93753a 100644 --- a/contracts/src/tests/transfer.cairo +++ b/contracts/src/tests/transfer.cairo @@ -1,70 +1,123 @@ -use core::starknet::SyscallResultTrait; -use core::traits::TryInto; -use openzeppelin::tests::utils::{deploy, pop_log}; -use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; -use openzeppelin::utils::serde::SerializedAppend; +use openzeppelin::token::erc20::ERC20ABIDispatcherTrait; use starknet::ContractAddress; use starknet::testing; -use starknet_ibc::apps::transfer::component::ICS20TransferComponent; +use starknet_ibc::apps::transfer::component::ICS20TransferComponent::Event as TransferEvent; use starknet_ibc::apps::transfer::interface::{ - ISendTransferDispatcher, ISendTransferDispatcherTrait, IRecvPacketDispatcher, - IRecvPacketDispatcherTrait + ISendTransferDispatcherTrait, IRecvPacketDispatcherTrait, ITokenAddressDispatcherTrait }; -use starknet_ibc::apps::transfer::types::Denom; -use starknet_ibc::presets::{Transfer, ERC20}; +use starknet_ibc::tests::setup::{ERC20ContractTrait, ICS20TransferContractTrait}; use starknet_ibc::tests::utils::{ - AMOUNT, SUPPLY, PREFIXED_DENOM, OWNER, RECIPIENT, dummy_erc20_call_data, dummy_msg_transder, - dummy_recv_packet + AMOUNT, SUPPLY, HOSTED_PREFIXED_DENOM, OWNER, RECIPIENT, dummy_msg_transder, dummy_recv_packet, + NATIVE_PREFIXED_DENOM, BARE_DENOM }; -fn setup_erc20() -> (ERC20ABIDispatcher, ContractAddress) { - let contract_address = deploy(ERC20::TEST_CLASS_HASH, dummy_erc20_call_data()); - (ERC20ABIDispatcher { contract_address }, contract_address) -} - -fn setup_ics20() -> (ISendTransferDispatcher, IRecvPacketDispatcher, ContractAddress) { - let mut call_data = array![]; - call_data.append_serde(ERC20::TEST_CLASS_HASH); - - let contract_address = deploy(Transfer::TEST_CLASS_HASH, call_data); - - ( - ISendTransferDispatcher { contract_address }, - IRecvPacketDispatcher { contract_address }, - contract_address - ) -} - #[test] -fn test_escrow() { +fn test_escrow_unescrow_roundtrip() { + // ----------------------------------------------------------- + // Setup Contracts + // ----------------------------------------------------------- + // Deploy an ERC20 contract. - let (erc20_dispatcher, token_address) = setup_erc20(); + let erc20 = ERC20ContractTrait::setup(); // Deploy an ICS20 Token Transfer contract. - let (send_disptacher, _, transfer_address) = setup_ics20(); + let ics20 = ICS20TransferContractTrait::setup(); + + // ----------------------------------------------------------- + // Escrow + // ----------------------------------------------------------- // Owner approves the amount of allowance for the `Transfer` contract. testing::set_contract_address(OWNER()); - erc20_dispatcher.approve(transfer_address, AMOUNT); + erc20.dispatcher().approve(ics20.contract_address, AMOUNT); // Submit a `MsgTransfer` to the `Transfer` contract. - send_disptacher.send_execute(dummy_msg_transder(token_address.into())); + ics20 + .send_dispatcher() + .send_execute(dummy_msg_transder(BARE_DENOM(erc20.contract_address), OWNER(), RECIPIENT())); - // Check the balance of the owner. - let balance = erc20_dispatcher.balance_of(OWNER()); - assert_eq!(balance, SUPPLY - AMOUNT); + // Assert the `SendEvent` emitted. + let event = ics20.assert_send_event(); + + // Check the balance of the sender. + let sender_balance = erc20.dispatcher().balance_of(event.sender); + assert_eq!(sender_balance, SUPPLY - AMOUNT); // Check the balance of the `Transfer` contract. - let balance = erc20_dispatcher.balance_of(transfer_address); - assert_eq!(balance, AMOUNT); + let contract_balance = erc20.dispatcher().balance_of(ics20.contract_address); + assert_eq!(contract_balance, AMOUNT); + + // ----------------------------------------------------------- + // Unescrow + // ----------------------------------------------------------- + + // Submit a `RecvPacket` to the `Transfer` contract. + ics20 + .recv_dispatcher() + .recv_execute( + dummy_recv_packet(NATIVE_PREFIXED_DENOM(erc20.contract_address), OWNER(), RECIPIENT()) + ); + + // Assert the `RecvEvent` emitted. + let event = ics20.assert_recv_event(); + + let contract_balance = erc20.dispatcher().balance_of(ics20.contract_address); + assert_eq!(contract_balance, 0); + + // Check the balance of the recipient. + let receiver_balance = erc20.dispatcher().balance_of(event.receiver); + assert_eq!(receiver_balance, AMOUNT); } #[test] -fn test_mint() { +fn test_mint_burn_roundtrip() { + // ----------------------------------------------------------- + // Setup Contracts + // ----------------------------------------------------------- + // Deploy an ICS20 Token Transfer contract. - let (_, recv_disptacher, _) = setup_ics20(); + let ics20 = ICS20TransferContractTrait::setup(); - // Submit a `RecvPacket` to the `Transfer` contract. - recv_disptacher.recv_validate(dummy_recv_packet(Denom::IBC(PREFIXED_DENOM()))); -} + // ----------------------------------------------------------- + // Mint + // ----------------------------------------------------------- + + // Submit a `RecvPacket`, which will create a new ERC20 contract. + ics20.recv_dispatcher().recv_execute(dummy_recv_packet(HOSTED_PREFIXED_DENOM(), OWNER(), RECIPIENT())); + + // Assert the `RecvEvent` emitted. + ics20.assert_recv_event(); + + // Submit another `RecvPacket`, which will mint the amount of tokens. + ics20.recv_dispatcher().recv_execute(dummy_recv_packet(HOSTED_PREFIXED_DENOM(), OWNER(), RECIPIENT())); + + // Assert the `RecvEvent` emitted. + let event = ics20.assert_recv_event(); + + // Check the balance of the receiver. + let token_address = ics20.addr_dispatcher().ibc_token_address(HOSTED_PREFIXED_DENOM()).unwrap(); + + let erc20 = ERC20ContractTrait::setup_with_addr(token_address); + let receiver_balance = erc20.dispatcher().balance_of(event.receiver); + assert_eq!(receiver_balance, AMOUNT * 2); + + // ----------------------------------------------------------- + // Burn + // ----------------------------------------------------------- + + // Owner approves the amount of allowance for the `Transfer` contract. + ics20 + .send_dispatcher() + .send_execute(dummy_msg_transder(HOSTED_PREFIXED_DENOM(), RECIPIENT(), OWNER())); + + // Assert the `SendEvent` emitted. + let event = ics20.assert_send_event(); + + // Check the balance of the sender. + let sender_balance = erc20.dispatcher().balance_of(event.sender); + assert_eq!(sender_balance, AMOUNT); + + let contract_balance = erc20.dispatcher().balance_of(ics20.contract_address); + assert_eq!(contract_balance, 0); +} diff --git a/contracts/src/tests/utils.cairo b/contracts/src/tests/utils.cairo index 6996073d..921a33c7 100644 --- a/contracts/src/tests/utils.cairo +++ b/contracts/src/tests/utils.cairo @@ -1,7 +1,12 @@ +use core::serde::Serde; use starknet::ContractAddress; use starknet::contract_address_const; -use starknet_ibc::apps::transfer::types::{MsgTransfer, Packet, PacketData, Token, Denom, Memo}; -use starknet_ibc::core::types::{Height, Timestamp, ChannelId, PortId, Sequence}; +use starknet_ibc::apps::transfer::types::{ + MsgTransfer, PacketData, PrefixedDenom, Denom, Memo, TracePrefixTrait +}; +use starknet_ibc::core::channel::types::Packet; +use starknet_ibc::core::client::types::{Height, Timestamp}; +use starknet_ibc::core::host::types::{ChannelId, PortId, Sequence}; pub(crate) const TOKEN_NAME: felt252 = 'NAME'; pub(crate) const DECIMALS: u8 = 18_u8; @@ -17,8 +22,22 @@ pub(crate) fn SYMBOL() -> ByteArray { "SYMBOL" } -pub(crate) fn PREFIXED_DENOM() -> ByteArray { - "transfer/channel-0/uatom" +pub(crate) fn BARE_DENOM(contract_address: ContractAddress) -> PrefixedDenom { + PrefixedDenom { trace_path: array![], base: Denom::Native(contract_address.into()) } +} + +pub(crate) fn NATIVE_PREFIXED_DENOM(contract_address: ContractAddress) -> PrefixedDenom { + let trace_prefix = TracePrefixTrait::new( + PortId { port_id: "transfer" }, ChannelId { channel_id: "channel-0" } + ); + PrefixedDenom { trace_path: array![trace_prefix], base: Denom::Native(contract_address.into()) } +} + +pub(crate) fn HOSTED_PREFIXED_DENOM() -> PrefixedDenom { + let trace_prefix = TracePrefixTrait::new( + PortId { port_id: "transfer" }, ChannelId { channel_id: "channel-0" } + ); + PrefixedDenom { trace_path: array![trace_prefix], base: Denom::Hosted("uatom") } } pub(crate) fn PUBKEY() -> ContractAddress { @@ -43,37 +62,38 @@ pub(crate) fn dummy_erc20_call_data() -> Array { call_data } -pub(crate) fn dummy_msg_transder(denom: Denom) -> MsgTransfer { - let port_id_on_a = PortId { port_id: 'transfer' }; - - let chan_id_on_a = ChannelId { channel_id: 'channel-0' }; - - let packet_data = PacketData { - token: Token { denom: denom, amount: AMOUNT }, - sender: OWNER(), - receiver: RECIPIENT(), - memo: Memo { memo: "" }, - }; - - MsgTransfer { port_id_on_a, chan_id_on_a, packet_data, } +pub(crate) fn dummy_msg_transder( + denom: PrefixedDenom, sender: ContractAddress, receiver: ContractAddress +) -> MsgTransfer { + MsgTransfer { + port_id_on_a: PortId { port_id: "transfer" }, + chan_id_on_a: ChannelId { channel_id: "channel-0" }, + packet_data: dummy_packet_data(denom, sender, receiver), + timeout_height_on_b: Height { revision_number: 0, revision_height: 1000 }, + timeout_timestamp_on_b: Timestamp { timestamp: 1000 } + } } -pub(crate) fn dummy_recv_packet(denom: Denom) -> Packet { - let data = PacketData { - token: Token { denom: denom, amount: AMOUNT }, - sender: OWNER(), - receiver: RECIPIENT(), - memo: Memo { memo: "" }, - }; +pub(crate) fn dummy_recv_packet( + denom: PrefixedDenom, sender: ContractAddress, receiver: ContractAddress +) -> Packet { + let mut serialized_data = array![]; + Serde::serialize(@dummy_packet_data(denom, sender, receiver), ref serialized_data); Packet { seq_on_a: Sequence { sequence: 0 }, - port_id_on_a: PortId { port_id: 'transfer' }, - chan_id_on_a: ChannelId { channel_id: 'channel-0' }, - port_id_on_b: PortId { port_id: 'transfer' }, - chan_id_on_b: ChannelId { channel_id: 'channel-1' }, - data, + port_id_on_a: PortId { port_id: "transfer" }, + chan_id_on_a: ChannelId { channel_id: "channel-0" }, + port_id_on_b: PortId { port_id: "transfer" }, + chan_id_on_b: ChannelId { channel_id: "channel-1" }, + data: serialized_data, timeout_height_on_b: Height { revision_number: 0, revision_height: 1000 }, timeout_timestamp_on_b: Timestamp { timestamp: 1000 } } } + +pub(crate) fn dummy_packet_data( + denom: PrefixedDenom, sender: ContractAddress, receiver: ContractAddress +) -> PacketData { + PacketData { denom, amount: AMOUNT, sender, receiver, memo: Memo { memo: "" }, } +} diff --git a/contracts/src/utils.cairo b/contracts/src/utils.cairo index 721af5d6..59065198 100644 --- a/contracts/src/utils.cairo +++ b/contracts/src/utils.cairo @@ -1,3 +1,8 @@ +/// Converts a generic type 'T' to a felt252. +pub trait ToFelt252Trait { + fn try_to_felt252(self: @T) -> Option; +} + // Note: if bytes has leading `\0` (null char), `felt252` forgets that information. // bytes_to_felt252(felt252_to_bytes(x)).unwrap() == x pub fn bytes_to_felt252(bytes: @ByteArray) -> Option { From 6d6b5c8ae77988191cdc07d3b0b5200d9b6638d0 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Mon, 29 Jul 2024 21:52:00 -0700 Subject: [PATCH 15/15] fix: taplo fmt --- contracts/Scarb.toml | 4 ++-- contracts/src/tests/transfer.cairo | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index 1a4527b5..d9bed03b 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -20,8 +20,8 @@ snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = " [[target.starknet-contract]] allowed-libfuncs-list.name = "experimental" -sierra = true -casm = false +sierra = true +casm = false [tool.fmt] sort-module-level-items = true diff --git a/contracts/src/tests/transfer.cairo b/contracts/src/tests/transfer.cairo index de93753a..c9a161f5 100644 --- a/contracts/src/tests/transfer.cairo +++ b/contracts/src/tests/transfer.cairo @@ -83,13 +83,17 @@ fn test_mint_burn_roundtrip() { // ----------------------------------------------------------- // Submit a `RecvPacket`, which will create a new ERC20 contract. - ics20.recv_dispatcher().recv_execute(dummy_recv_packet(HOSTED_PREFIXED_DENOM(), OWNER(), RECIPIENT())); + ics20 + .recv_dispatcher() + .recv_execute(dummy_recv_packet(HOSTED_PREFIXED_DENOM(), OWNER(), RECIPIENT())); // Assert the `RecvEvent` emitted. ics20.assert_recv_event(); // Submit another `RecvPacket`, which will mint the amount of tokens. - ics20.recv_dispatcher().recv_execute(dummy_recv_packet(HOSTED_PREFIXED_DENOM(), OWNER(), RECIPIENT())); + ics20 + .recv_dispatcher() + .recv_execute(dummy_recv_packet(HOSTED_PREFIXED_DENOM(), OWNER(), RECIPIENT())); // Assert the `RecvEvent` emitted. let event = ics20.assert_recv_event();