Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate to standard ERC20 for dynamic supply with separate token creation scope #338

Merged
merged 8 commits into from
Mar 4, 2025
12 changes: 6 additions & 6 deletions cairo-contracts/packages/apps/src/tests/transfer.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,20 @@ fn test_mint_ok() {

let token_address = ics20.ibc_token_address(prefixed_denom.key());

let erc20: ERC20Contract = token_address.into();

// Assert the `CreateTokenEvent` emitted.
spy
.assert_create_token_event(
ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, token_address, cfg.amount,
);
spy.assert_create_token_event(ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, token_address);

// Assert if ICS20 performs the mint.
spy.assert_transfer_event(erc20.address, ics20.address, SN_USER(), cfg.amount);

// Assert the `RecvEvent` emitted.
spy
.assert_recv_event(
ics20.address, CS_USER(), SN_USER(), prefixed_denom.clone(), cfg.amount, true,
);

let erc20: ERC20Contract = token_address.into();

// Assert if the transfer happens from the ICS20 address.
spy.assert_transfer_event(erc20.address, ics20.address, SN_USER(), cfg.amount);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ pub mod TokenTransferComponent {
pub decimals: u8,
#[key]
pub address: ContractAddress,
pub initial_supply: u256,
}

// -----------------------------------------------------------
Expand Down Expand Up @@ -714,15 +713,14 @@ pub mod TokenTransferComponent {
amount: u256,
) {
let mut token = self.get_token(denom.key());
if token.is_non_zero() {
token.mint(get_contract_address(), amount);
} else {
if token.is_zero() {
let name = denom.base.hosted().unwrap();

token = self.create_token(name, amount);

self.record_ibc_token(denom, token.address);
}
token.mint(amount);
token.transfer(account, amount);
}

Expand All @@ -737,7 +735,7 @@ pub mod TokenTransferComponent {

token.transfer_from(account, get_contract_address(), amount);

token.burn(get_contract_address(), amount);
token.burn(amount);
}

fn refund_execute(
Expand Down Expand Up @@ -813,14 +811,12 @@ pub mod TokenTransferComponent {
name.clone(),
symbol.clone(),
decimals,
amount.clone(),
get_contract_address(),
get_contract_address(),
);

self.write_salt(salt + 1);

self.emit_create_token_event(name, symbol, decimals, erc20_token.address, amount);
self.emit_create_token_event(name, symbol, decimals, erc20_token.address);

erc20_token
}
Expand Down Expand Up @@ -1015,9 +1011,8 @@ pub mod TokenTransferComponent {
symbol: ByteArray,
decimals: u8,
address: ContractAddress,
initial_supply: u256,
) {
let event = CreateTokenEvent { name, symbol, decimals, address, initial_supply };
let event = CreateTokenEvent { name, symbol, decimals, address };
self.emit(event);
}
}
Expand Down
23 changes: 10 additions & 13 deletions cairo-contracts/packages/apps/src/transfer/erc20_call.cairo
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use core::num::traits::Zero;
use core::starknet::SyscallResultTrait;
use openzeppelin_token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait};
use starknet::syscalls::deploy_syscall;
use starknet::syscalls::{call_contract_syscall, deploy_syscall};
use starknet::{ClassHash, ContractAddress, contract_address_const};
use starknet_ibc_utils::mintable::{IERC20MintableDispatcher, IERC20MintableDispatcherTrait};

#[derive(Copy, Debug, Drop, Serde)]
pub struct ERC20Contract {
Expand All @@ -28,23 +27,17 @@ pub impl ERC20ContractImpl of ERC20ContractTrait {
ERC20ABIDispatcher { contract_address: *self.address }
}

fn mintable_dispatcher(self: @ERC20Contract) -> IERC20MintableDispatcher {
IERC20MintableDispatcher { contract_address: *self.address }
}

fn create(
class_hash: ClassHash,
salt: felt252,
name: ByteArray,
symbol: ByteArray,
decimals: u8,
amount: u256,
recipient: ContractAddress,
owner: ContractAddress,
) -> ERC20Contract {
let mut call_data = array![];

(name, symbol, decimals, amount, recipient, owner).serialize(ref call_data);
(name, symbol, decimals, owner).serialize(ref call_data);

let (address, _) = deploy_syscall(class_hash, salt, call_data.span(), false)
.unwrap_syscall();
Expand All @@ -62,12 +55,16 @@ pub impl ERC20ContractImpl of ERC20ContractTrait {
self.dispatcher().transfer_from(sender, recipient, amount)
}

fn mint(self: @ERC20Contract, recipient: ContractAddress, amount: u256) {
self.mintable_dispatcher().permissioned_mint(recipient, amount)
fn mint(self: @ERC20Contract, amount: u256) {
let mut calldata = array![];
amount.serialize(ref calldata);
call_contract_syscall(*self.address, selector!("mint"), calldata.span()).unwrap_syscall();
}

fn burn(self: @ERC20Contract, account: ContractAddress, amount: u256) {
self.mintable_dispatcher().permissioned_burn(account, amount)
fn burn(self: @ERC20Contract, amount: u256) {
let mut calldata = array![];
amount.serialize(ref calldata);
call_contract_syscall(*self.address, selector!("burn"), calldata.span()).unwrap_syscall();
}

fn balance_of(self: @ERC20Contract, from_account: ContractAddress) -> u256 {
Expand Down
36 changes: 20 additions & 16 deletions cairo-contracts/packages/contracts/src/erc20.cairo
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
#[starknet::contract]
pub mod ERC20Mintable {
use core::num::traits::Zero;
use openzeppelin_access::ownable::OwnableComponent;
use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl, interface::IERC20Metadata};
use starknet::ContractAddress;
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet_ibc_utils::mintable::ERC20MintableComponent;
use starknet_ibc_utils::mintable::ERC20MintableComponent::ERC20MintableInternalTrait;
use starknet::{ContractAddress, get_caller_address};

component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
component!(path: ERC20MintableComponent, storage: mintable, event: MintableEvent);
component!(path: ERC20Component, storage: erc20, event: ERC20Event);

// Ownable Mixin
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

// ERC20 Mintable
#[abi(embed_v0)]
impl ERC20MintableImpl = ERC20MintableComponent::ERC20Mintable<ContractState>;

#[abi(embed_v0)]
impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
#[abi(embed_v0)]
Expand All @@ -31,8 +25,6 @@ pub mod ERC20Mintable {
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
mintable: ERC20MintableComponent::Storage,
#[substorage(v0)]
erc20: ERC20Component::Storage,
// The decimals value is stored locally in the contract.
// ref: https://docs.openzeppelin.com/contracts-cairo/0.20.0/erc20#the_storage_approach
Expand All @@ -45,8 +37,6 @@ pub mod ERC20Mintable {
#[flat]
OwnableEvent: OwnableComponent::Event,
#[flat]
MintableEvent: ERC20MintableComponent::Event,
#[flat]
ERC20Event: ERC20Component::Event,
}

Expand All @@ -56,14 +46,10 @@ pub mod ERC20Mintable {
name: ByteArray,
symbol: ByteArray,
decimals: u8,
initial_supply: u256,
recipient: ContractAddress,
owner: ContractAddress,
) {
self.ownable.initializer(owner);
self.mintable.initializer();
self.erc20.initializer(name, symbol);
self.erc20.mint(recipient, initial_supply);

self._set_decimals(decimals);
}
Expand All @@ -84,6 +70,24 @@ pub mod ERC20Mintable {
}
}

#[generate_trait]
#[abi(per_item)]
impl DynamicSupplyImpl of DynamicSupplyTrait {
#[external(v0)]
fn burn(ref self: ContractState, amount: u256) {
self.ownable.assert_only_owner();
assert(amount.is_non_zero(), 'ERC20: burning amount is zero');
self.erc20.burn(get_caller_address(), amount);
}

#[external(v0)]
fn mint(ref self: ContractState, amount: u256) {
self.ownable.assert_only_owner();
assert(amount.is_non_zero(), 'ERC20: minting amount is zero');
self.erc20.mint(get_caller_address(), amount);
}
}

#[generate_trait]
impl InternalImpl of InternalTrait {
fn _set_decimals(ref self: ContractState, decimals: u8) {
Expand Down
1 change: 1 addition & 0 deletions cairo-contracts/packages/contracts/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ mod tests {
pub(crate) mod channel;
pub(crate) mod client;
pub(crate) mod connection;
pub(crate) mod erc20;
pub(crate) mod transfer;
}
83 changes: 83 additions & 0 deletions cairo-contracts/packages/contracts/src/tests/erc20.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use snforge_std::{EventSpy, spy_events, start_cheat_caller_address};
use starknet_ibc_apps::transfer::{ERC20Contract, ERC20ContractTrait};
use starknet_ibc_testkit::dummies::{AMOUNT, OWNER, SN_USER, ZERO};
use starknet_ibc_testkit::event_spy::{ERC20EventSpyExt, ERC20EventSpyExtImpl};
use starknet_ibc_testkit::handles::ERC20Handle;
use starknet_ibc_testkit::setup::SetupImpl;

fn setup() -> (ERC20Contract, EventSpy) {
let setup = SetupImpl::default();
let erc20 = SetupImpl::deploy_erc20(@setup, OWNER());
let spy = spy_events();
(erc20, spy)
}

#[test]
fn test_deploy_erc20_ok() {
setup();
}

#[test]
fn test_erc20_mint_ok() {
let (mut erc20, mut spy) = setup();
start_cheat_caller_address(erc20.address, OWNER());
erc20.mint(AMOUNT);
spy.assert_transfer_event(erc20.address, ZERO(), OWNER(), AMOUNT);
erc20.assert_balance(OWNER(), AMOUNT);
erc20.assert_total_supply(AMOUNT);
}

#[test]
fn test_erc20_burn_ok() {
let (mut erc20, mut spy) = setup();
start_cheat_caller_address(erc20.address, OWNER());
erc20.mint(AMOUNT);
erc20.burn(AMOUNT);
spy.assert_transfer_event(erc20.address, OWNER(), ZERO(), AMOUNT);
erc20.assert_balance(OWNER(), 0);
erc20.assert_total_supply(0);
}

#[test]
#[should_panic(expected: 'Caller is not the owner')]
fn test_erc20_unauthorized_mint() {
let (mut erc20, _) = setup();
erc20.mint(AMOUNT);
}

#[test]
#[should_panic(expected: 'Caller is not the owner')]
fn test_erc20_unauthorized_burn() {
let (mut erc20, _) = setup();
start_cheat_caller_address(erc20.address, OWNER());
erc20.mint(AMOUNT);
start_cheat_caller_address(erc20.address, SN_USER());
erc20.burn(AMOUNT);
}

#[test]
#[should_panic(expected: 'ERC20: insufficient allowance')]
fn test_erc20_transfer_without_user_approval() {
let (mut erc20, _) = setup();
start_cheat_caller_address(erc20.address, OWNER());
erc20.mint(AMOUNT);
erc20.transfer_from(OWNER(), SN_USER(), AMOUNT);
erc20.transfer_from(SN_USER(), OWNER(), AMOUNT);
}

#[test]
#[should_panic(expected: 'ERC20: minting amount is zero')]
fn test_erc20_mint_zero_amount() {
let (mut erc20, _) = setup();
start_cheat_caller_address(erc20.address, OWNER());
erc20.mint(0);
}

#[test]
#[should_panic(expected: 'ERC20: burning amount is zero')]
fn test_erc20_burn_zero_amount() {
let (mut erc20, _) = setup();
start_cheat_caller_address(erc20.address, OWNER());
erc20.mint(AMOUNT);
erc20.burn(0);
}
16 changes: 8 additions & 8 deletions cairo-contracts/packages/contracts/src/tests/transfer.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,20 @@ fn test_mint_burn_roundtrip() {
// Fetch the token address.
let token_address = ics20.ibc_token_address(prefixed_denom.key());

let mut erc20: ERC20Contract = token_address.into();

// Assert the `CreateTokenEvent` emitted.
spy
.assert_create_token_event(
ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, token_address, transfer_cfg.amount,
);
spy.assert_create_token_event(ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, token_address);

// Assert if ICS20 performs the mint.
spy.assert_transfer_event(erc20.address, ics20.address, SN_USER(), transfer_cfg.amount);

// Assert the `RecvEvent` emitted.
spy
.assert_recv_event(
ics20.address, CS_USER(), SN_USER(), prefixed_denom.clone(), transfer_cfg.amount, true,
);

let mut erc20: ERC20Contract = token_address.into();

// Assert if the transfer happens from the ICS20 address.
spy.assert_transfer_event(erc20.address, ics20.address, SN_USER(), transfer_cfg.amount);

Expand Down Expand Up @@ -191,7 +191,7 @@ fn test_create_ibc_token_ok() {
let (_, ics20, _, _, _, transfer_cfg, mut spy) = setup(Mode::WithChannel);
let prefixed_denom = transfer_cfg.prefix_hosted_denom();
let address = ics20.create_ibc_token(prefixed_denom.clone());
spy.assert_create_token_event(ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, address, 0);
spy.assert_create_token_event(ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, address);
let queried = ics20.ibc_token_address(prefixed_denom.key());
assert_eq!(address, queried);
}
Expand All @@ -208,7 +208,7 @@ fn test_create_ibc_token_with_multihop() {
let prefixed_denom = transfer_cfg.prefix_hosted_denom();

let address = ics20.create_ibc_token(prefixed_denom.clone());
spy.assert_create_token_event(ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, address, 0);
spy.assert_create_token_event(ics20.address, NAME(), SYMBOL(), DECIMAL_ZERO, address);
let queried = ics20.ibc_token_address(prefixed_denom.key());
assert_eq!(address, queried);
}
Expand Down
4 changes: 4 additions & 0 deletions cairo-contracts/packages/testkit/src/dummies/transfer.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub fn CLASS_HASH() -> ClassHash {
class_hash_const::<'ERC20Mintable'>()
}

pub fn ZERO() -> ContractAddress {
contract_address_const::<0>()
}

pub fn ERC20() -> ERC20Contract {
contract_address_const::<0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7>()
.into()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ pub impl TransferEventSpyExtImpl of TransferEventSpyExt {
symbol: ByteArray,
decimals: u8,
address: ContractAddress,
initial_supply: u256,
) {
let expected = Event::CreateTokenEvent(
CreateTokenEvent { name, symbol, decimals, address, initial_supply },
CreateTokenEvent { name, symbol, decimals, address },
);
self.assert_emitted_single(contract_address, expected);
}
Expand Down
Loading
Loading