IOTA Contract ClubMem2025

unofficial site

- return to IOTA Rebased Useful Links

ClubMem2025 Smart Contract

This is an IOTA Rebased smart contract which implements a simple membership system with NFT membership card. Membership is 0.02 IOTA. The contract processes membership payments to a Treasury. It is live on Mainnet though for demonstration purposes only. It uses the IOTA CLI to both publish and access the contract. Information here lets you view everything done on IOTA Explorer.

IMPORTANT The following is not meant to be secure or perfectly written - it is a first working draft intended only for my own use but that may aid others to get started.

Contract Implementation

The contract is implemented in two files. The Move.toml file is at top level and the clubmem2025.move file is in a 'sources' folder. Lower down the page the commands to build and publish the contract are shown, as well as how to call it.

Move.toml Configuration

[package]
name = "clubmem2025"
edition = "2024.beta"

[dependencies]
Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "mainnet" }  

[addresses]
clubmem2025 = "0x0"

Contract Source (clubmem2025.move)

 
module clubmem2025::membership {
    use iota::coin::{Self, Coin};
    use iota::iota::IOTA;
    use iota::tx_context::TxContext;
    use iota::event;
    use iota::transfer;
    use iota::object::{Self, UID};
    use iota::display;
    use iota::package;
    use std::string::{Self, String};

    // -----------------------------------------------------------------------
    // Structs
    // -----------------------------------------------------------------------
    
    /// One-time witness for claiming Publisher
    public struct MEMBERSHIP has drop {}

    /// Admin capability for managing the contract
    public struct AdminCap has key, store {
        id: UID,
    }

    /// Membership Card NFT
    public struct MembershipCard has key, store {
        id: UID,
        member_number: u64,
        member: address,
        issued_at: u64,
    }

    /// Configuration object (shared, managed by admin)
    public struct MembershipConfig has key {
        id: UID,
        treasury: address,
        membership_fee: u64,
        paused: bool,
        next_member_number: u64,
    }

    // -----------------------------------------------------------------------
    // Events
    // -----------------------------------------------------------------------
    
    public struct MembershipAccepted has copy, drop {
        from: address,
        amount: u64,
        member_number: u64,
        card_id: address,
    }

    public struct MembershipRejected has copy, drop {
        from: address,
        amount: u64,
        reason: vector
    }

    public struct ConfigUpdated has copy, drop {
        treasury: address,
        membership_fee: u64,
    }

    public struct ContractPaused has copy, drop {
        paused: bool,
    }

    // -----------------------------------------------------------------------
    // Error codes
    // -----------------------------------------------------------------------
    
    const E_CONTRACT_PAUSED: u64 = 1;
    const E_NOT_AUTHORIZED: u64 = 2;

    // -----------------------------------------------------------------------
    // Initialization
    // -----------------------------------------------------------------------
    
    fun init(otw: MEMBERSHIP, ctx: &mut TxContext) {
        let sender = ctx.sender();
        
        // Create and transfer admin capability
        let admin_cap = AdminCap {
            id: object::new(ctx),
        };
        transfer::transfer(admin_cap, sender);

        // Create display for NFTs
        let keys = vector[
            string::utf8(b"name"),
            string::utf8(b"description"),
            string::utf8(b"image_url"),
            string::utf8(b"member_number"),
            string::utf8(b"member"),
            string::utf8(b"project_url"),
        ];

        let values = vector[
            string::utf8(b"Club Membership #{member_number}"),
            string::utf8(b"Official Club Membership Card - Member #{member_number}"),
            string::utf8(b"https://iotarebased.com/clubnfts/member_{member_number}.jpg"),
            string::utf8(b"{member_number}"),
            string::utf8(b"{member}"),
            string::utf8(b"https://iotarebased.com"),
        ];

        let publisher = package::claim(otw, ctx);
        let mut display = display::new_with_fields(
            &publisher, keys, values, ctx
        );
        display::update_version(&mut display);
        transfer::public_transfer(publisher, sender);
        transfer::public_transfer(display, sender);

        // Create and share configuration
        let config = MembershipConfig {
            id: object::new(ctx),
            treasury: @0xda5f052f5d98d7d6bdbeaff2df40acd1ff7e7e48368de591870d542e2e8eaf48,
            membership_fee: 20_000_000,  // 0.02 IOTA
            paused: false,
            next_member_number: 1,
        };
        transfer::share_object(config);
    }

    // -----------------------------------------------------------------------
    // Public entry functions
    // -----------------------------------------------------------------------
    
    /// Join the club by paying membership fee
    public entry fun join(
        config: &mut MembershipConfig,
        coin: Coin,
        ctx: &mut TxContext
    ) {
        assert!(!config.paused, E_CONTRACT_PAUSED);

        let amount = coin::value(&coin);
        let sender = ctx.sender();

        // Check if amount matches membership fee
        if (amount == config.membership_fee) {
            
            let member_number = config.next_member_number;
            config.next_member_number = member_number + 1;

            // Create membership card NFT
            let card = MembershipCard {
                id: object::new(ctx),
                member_number,
                member: sender,
                issued_at: ctx.epoch_timestamp_ms(),
            };

            let card_id = object::uid_to_address(&card.id);

            // Transfer payment to treasury
            transfer::public_transfer(coin, config.treasury);

            // Transfer membership card to user
            transfer::public_transfer(card, sender);

            // Emit acceptance event
            event::emit(MembershipAccepted {
                from: sender,
                amount,
                member_number,
                card_id,
            });
        }
        else {
            // Refund invalid payment
            transfer::public_transfer(coin, sender);
            
            event::emit(MembershipRejected {
                from: sender,
                amount,
                reason: b"Incorrect payment amount. Full amount refunded."
            });
        }
    }

    // -----------------------------------------------------------------------
    // Admin functions
    // -----------------------------------------------------------------------
    
    /// Update treasury address
    public entry fun set_treasury(
        _: &AdminCap,
        config: &mut MembershipConfig,
        new_treasury: address,
    ) {
        config.treasury = new_treasury;
        emit_config_update(config);
    }

    /// Update membership fee
    public entry fun set_membership_fee(
        _: &AdminCap,
        config: &mut MembershipConfig,
        new_fee: u64,
    ) {
        config.membership_fee = new_fee;
        emit_config_update(config);
    }

    /// Pause or unpause the contract
    public entry fun set_paused(
        _: &AdminCap,
        config: &mut MembershipConfig,
        paused: bool,
    ) {
        config.paused = paused;
        event::emit(ContractPaused { paused });
    }

    /// Emergency withdrawal (only if contract is paused)
    public entry fun emergency_withdraw(
        _: &AdminCap,
        config: &MembershipConfig,
        coin: Coin,
        recipient: address,
    ) {
        assert!(config.paused, E_NOT_AUTHORIZED);
        transfer::public_transfer(coin, recipient);
    }

    // -----------------------------------------------------------------------
    // View functions
    // -----------------------------------------------------------------------
    
    /// Get member number from card
    public fun get_member_number(card: &MembershipCard): u64 {
        card.member_number
    }

    /// Get member address from card
    public fun get_member(card: &MembershipCard): address {
        card.member
    }

    /// Get when membership was issued
    public fun get_issued_at(card: &MembershipCard): u64 {
        card.issued_at
    }

    /// Check current membership fee
    public fun get_membership_fee(config: &MembershipConfig): u64 {
        config.membership_fee
    }

    /// Get next member number to be issued
    public fun get_next_member_number(config: &MembershipConfig): u64 {
        config.next_member_number
    }

    /// Check if contract is paused
    public fun is_paused(config: &MembershipConfig): bool {
        config.paused
    }

    /// Get treasury address
    public fun get_treasury(config: &MembershipConfig): address {
        config.treasury
    }

    // -----------------------------------------------------------------------
    // Helper functions
    // -----------------------------------------------------------------------
    
    fun emit_config_update(config: &MembershipConfig) {
        event::emit(ConfigUpdated {
            treasury: config.treasury,
            membership_fee: config.membership_fee,
        });
    }
}

Deployment Process

The contract was deployed using the following steps:

1. Build the Contract

iota move build

2. Publish the Contract

iota client publish 

This resulted in the contract being published with following details:
Package Id: 0x0424e1fceaf98db1b75b732d9953de08be816a657247385549118fa85df6464f

Config Id: 0xa536c4bc0204bf40226a511eac7b1e4cc35c3fb8943d89507cf5e4a12d1dedbe
Display Id: 0x8184af13bd523b24e26691af15b0e48a10357ce9fde7c3320bef70f16418a171
Publisher Id: 0xa19023fdb19b04b74eb54dc9b98fe733e9ff261e8c68195da6c7266fd0178358

Deployer address: 0xc196e256a58a1ea07e2cb27887090c3cf6a1ad1ec189c8a40575e7cde6c3dc4a
Treasury address: 0xda5f052f5d98d7d6bdbeaff2df40acd1ff7e7e48368de591870d542e2e8eaf48
Membership fee: 0.02 IOTA
Starting member Number: 1

Transaction digest: HJi7RtERPeFhoyTiupxdiE3s21Zz6SheQtFFRrMcGzPN
Block Explorer: https://explorer.iota.org/txblock/HJi7RtERPeFhoyTiupxdiE3s21Zz6SheQtFFRrMcGzPN?network=mainnet

Interacting with the Contract through the CLI

NOTE that in this version the user has to provide the ID of a COIN of exactly 0.02 IOTA (use SPLIT)

iota client call \
  --package THE_PACKAGE_ID \
  --module membership \
  --function join \
  --args THE_CONFIG_ID THEIR_COIN_ID \
  --gas-budget 10000000
So, for example:

iota client call \
  --package 0x0424e1fceaf98db1b75b732d9953de08be816a657247385549118fa85df6464f \
  --module membership \
  --function join \
  --args 0xa536c4bc0204bf40226a511eac7b1e4cc35c3fb8943d89507cf5e4a12d1dedbe 0x74893c62e66023b17342d5788a08ce06f6fe7ca79ae5ec2a70074908dc79c298\
  --gas-budget 10000000
Transaction Digest for member 1 was: ELhA4bXG123wGi6a1XjXcB39x5DFoNUmrPpmGPQ1SxBB
and Transaction Digest for member 2: Fvbe72kxwPJ3r16M4stHGJsvUX76YLi7GiSiFDG9mAiw