1

I am getting a Contract Trapped error on one specific message in my ink contract. I tried putting overflow-checks = false into the Cargo.toml. That made no difference. This is the first time I have ever seen that error pop up. This is also the first time I have made a message function that both takes in coin and pays out coin. This contract does not call any other contract. It is completely stand alone.

Any ideas on why I am getting a Contract Trapped on this one message? .... Here's the code...

        #[ink(message, payable)]
        #[openbrush::modifiers(non_reentrant)]
        pub fn send_message_to_paid_list (&mut self, 
            to_list_id: Hash,
            new_message: Vec<u8>,
            file_url: Vec<u8>,
        ) -> Result<(), Error> {

            // COLLECT PAYMENT FROM THE CALLER
            // the 'payable' tag on this message allows the user to send any amount
            let staked: Balance = self.env().transferred_value();
            
            // Does this list exist? and do you own it? If so, proceed
            let caller = Self::env().caller();
            let owned_lists = self.account_owned_paid_lists.get(&caller).unwrap_or_default().hashvector;
            if owned_lists.contains(&to_list_id) {
                
                // total the fees required to send to this list              
                // get the list_accounts from paid_list_details: Mapping<Hash, PaidListDetails>
                let listaccounts = self.paid_list_details.get(&to_list_id).unwrap_or_default().list_accounts;
                let mut total_fee: Balance = 0;
                // for each acccount on the paid list...
                for inbox in &listaccounts {
                    // get their inbox fee from account_settings: Mapping<AccountID, Settings>
                    let fee: Balance = self.account_settings.get(&inbox).unwrap_or_default().inbox_fee;
                    // add it to the total_fee
                    total_fee += fee;
                }

                // if staked is more than total_fee, proceed
                if staked > total_fee {

                    // set up clones
                    let new_message_clone = new_message.clone();
                    // set up the data that will go into the new_message_id hash
                    let new_timestamp = self.env().block_timestamp();
                    // create the new_message_id by hashing the above data
                    let encodable = (caller, to_list_id, new_message, new_timestamp); // Implements `scale::Encode`
                    let mut new_message_id_u8 = <Sha2x256 as HashOutput>::Type::default(); // 256-bit buffer
                    ink::env::hash_encoded::<Sha2x256, _>(&encodable, &mut new_message_id_u8);
                    let new_message_id: Hash = Hash::from(new_message_id_u8);

                    // SET UP THE MESSAGE DETAILS FOR THE NEW MESSAGE
                    let fromusername = self.account_settings.get(&caller).unwrap_or_default().username;
                    let listname = self.paid_list_details.get(&to_list_id).unwrap_or_default().list_name;
                    let new_details = ListMessageDetails {
                        message_id: new_message_id,
                        from_acct: Self::env().caller(),
                        username: fromusername,
                        to_list_id: to_list_id,
                        to_list_name: listname,
                        message: new_message_clone,  
                        file_url: file_url,
                        timestamp: self.env().block_timestamp(),
                    };

                    // add the message to paid_list_message_details: Mapping<Hash, ListMessageDetails>
                    self.paid_list_message_details.insert(&new_message_id, &new_details);

                    // add the message ID to sent_messages_to_paid_list: Mapping<Hash, HashVector>
                    // get the messages vector for this list
                    let mut current_messages = self.sent_messages_to_paid_list.get(&to_list_id).unwrap_or_default();
                    // add this message to the messages vector for this list
                    current_messages.hashvector.push(new_message_id);
                    // update the sent_messages_to_paid_list map
                    self.sent_messages_to_paid_list.insert(&to_list_id, &current_messages);

                    // PAY EACH ACCOUNT ON THE PAID LIST 
                    for inbox in &listaccounts {
                        // get their inbox fee from account_settings: Mapping<AccountID, Settings>
                        let fee: Balance = self.account_settings.get(&inbox).unwrap_or_default().inbox_fee;
                        // send them their inbox_fee
                        self.env().transfer(*inbox, fee).expect("payout failed");
                        if self.env().transfer(*inbox, fee).is_err() {
                            return Err(Error::PayoutFailed);
                        }
                    }
                    // then pay the caller what is left from their stake after paying the list
                    let leftovers: Balance = staked - total_fee;
                    if leftovers > 0 {
                        self.env().transfer(caller, leftovers).expect("payout failed");
                        if self.env().transfer(caller, leftovers).is_err() {
                            return Err(Error::PayoutFailed);
                        }
                    }

                    Ok(())
                }
                // if staked is less than or = to the total fee, send an error
                else {
                    return Err(Error::InsufficientStake);
                }
            }
            // if the list does not exist, or you do not own it, send an error
            else {
                return Err(Error::NonexistentList);
            }
        }

1 Answer 1

3

Just from a quick look at the code, I think the problem might be you are calling self.env().transfer() two times, and contract is trapped because it fails on expect instead of returning a specific error.

// calling transfer first time, if it fails we panic - very first call should pass
self.env().transfer(caller, leftovers).expect("payout failed");
// if this transfer fails an error is returned, however this can pass and then
// the call can panic on the previous transfer since it is in a loop
if self.env().transfer(caller, leftovers).is_err() {
  return Err(Error::PayoutFailed);
}

If you meant to return a specific error if the transfer fails, then you only need

if self.env().transfer(caller, leftovers).is_err() {
  return Err(Error::PayoutFailed);
}

Also a small note - if you want to transfer native tokens to multiple users like this, you should not expect it to pass, one transfer may fail leading to the call panicking and the message not being able to pass anymore (since the accounts are pulled from the storage - will you be able to modify storage in such case? etc.)

Example: If the list contains user A, B and C and transfer to B fails for whatever reason, calling the same message will also try to transfer to the same users.

4
  • Oh! OK. I think I get it. You are saying I should keep only ONE of either self.env().transfer(caller, leftovers).expect("payout failed") OR the if statement with the specific error, but not both. And that since I am paying out multiple people, the if statement with a specific error is a better idea than the self.env().transfer(caller, leftovers).expect("payout failed") Did I get that right? Commented Sep 11, 2023 at 20:36
  • 1) yes, keep only one transfer, try if that fixes it
    – kriko.eth
    Commented Sep 11, 2023 at 23:26
  • 2) that was just a recommendation regarding possible dos, it does not matter if you expect, panic or return error, call will fail in any case. It has nothing to do with your question it was just something I caught) you can read more about such casey here consensys.github.io/smart-contract-best-practices/attacks/…
    – kriko.eth
    Commented Sep 11, 2023 at 23:30
  • 1
    That fixed it beautifully! I commented out the .expect version and kept the if statement to return a specific error and that solved the ContractTrapped issue perfectly. Thank you so much for your insight! You have made me a better programmer! Commented Sep 12, 2023 at 13:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.