From 7b6d5a277f5fe2a8bd97214731359b25d53c1e76 Mon Sep 17 00:00:00 2001 From: Farhad Shabani Date: Thu, 25 Jul 2024 22:29:29 -0700 Subject: [PATCH] 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 \