How to swap stablecoin with unlisted custom token without using Moralis plugins?

Hello, good day guys. :wave:
I have a question & being a first time blockchain dev which is why I’m not really sure how to approach this, hence it’s what got me here.

I am trying to build a token swap feature that allow users to swap their stablecoins for our own unlisted custom token on BSC Network, the custom token has already been created & we don’t plan to list it on any DEX at the moment, meaning using Moralis’ 1inch plugin would be out of the question.

What is the best approach for me to build this feature?
Pointers would be much appreciated.

Hey @Lime,

I think one way to do this is building a smart contract that have a method function that will transfer stablecoin from user wallet (needs approve) and send your custom token from an escrow/treasury contract (this contract could also be the same contract that is doing this transfer). And, that’s one way to do the swapping as you described.

Happy BUIDL~

@YosephKS thanks for the pointers, I somewhat managed to get a clue but once again unsure what to do again.

I was able to find an example smart contract for the swap feature, my smart contract is similar except I don’t have “_token2” & “_owner2” as required parameters, I predefined them in the constructor(), as “_token2” will always be our custom token & “_owner2” will always be the wallet address where we store our custom token.

Anyway, moving along. To interact with the swap smart contract, this is the function that would run it when called after user has input the amount they want to swap:

(using “moralis”: “^0.0.134” & “react-moralis”: “^0.3.1” packages on React)

const swapToken = async () => {
    const web3 = await Moralis.enableWeb3();
    // This is the smart contract address
    const swapConAdd = process.env.REACT_APP_TEST_SWAP_SMART_CON; 

    try {
      let stableAdd; // stablecoin's contract address
      const stableSym = fromToken; // stablecoin symbol
      const stableDec = fromDec; // stablecoin decimal
      const buyerAdd = props.wallAdd; // user wallet address
      const buyerAmt = stableVal; // stablecoin value/amount
      const sellerAmt = tysVal; // custom token value/amount

      if(stableSym.toLowerCase() === "usdt") {
        stableAdd = process.env.REACT_APP_TEST_USDT_ADD;
      }

      const abi = [
        {
          constant: true,
          inputs: [
            {
              internalType: "address",
              name: "buyerToken",
              type: "address"
            },
            {
              internalType: "address",
              name: "buyer",
              type: "address"
            },
            {
              internalType: "uint256",
              name: "buyerAmt",
              type: "uint256"
            },
            {
              internalType: "uint256",
              name: "sellerAmt",
              type: "uint256"
            }
          ],
          name: "swap",
          outputs: [
            {
              internalType: "uint256",
              name: "",
              type: "uint256"
            }
          ],
          payable: true,
          stateMutability: "view",
          type: "function"
        }
      ];
      
      const contract = new web3.eth.Contract(abi, swapConAdd);
      const swap = await contract.methods.swap(
        stableAdd,
        buyerAdd,
        Moralis.Units.Token(buyerAmt.toString(), stableDec.toString()),
        Moralis.Units.Token(sellerAmt.toString(), "18")
      ).send({
        from: buyerAdd,
        value: Moralis.Units.Token(buyerAmt.toString(), stableDec.toString()),
        gasPrice: web3.utils.toWei("5", "gwei"),
        gas: web3.utils.toWei("0.0000000000005", "ether")
      });

      console.log(swap);
    } catch(error) {
      console.log("transfer error", error);
    }

  }  

Once ran, the Metamask popup would appear to ask for confirmation of the transaction.

However, a few things were wrong:

  • The smart contract address ended up being the recipient address.
  • The swap value is correct but it’s not in USDT, instead it’s TBNB.

And when the transaction is confirmed, I got this error in the console:

I couldn’t see what’s the issue here & is clueless. Could it be the predefined variables in the smart contract? Or is it something to do with how the smart contract method was executed through React?

NOTE: All of this is on BSC Testnet, even the smart contract was compiled & deployed to BSC Testnet with Remix.

the gas fee will always be native currency, in your case probably you called the native currency TBNB when you added Binance testnet to metamask.

In order to make a transaction with ERC20 tokens sometimes you also need to approve the transfer of those tokens so that a smart contract can transfers the tokens from one address to another address.

@cryptokid

I’ve modified the function slightly to run approve before running the swap but to no avail.

const swapToken = async () => {
    const web3 = await Moralis.enableWeb3();
    // This is the smart contract address
    const swapConAdd = process.env.REACT_APP_SWAP_SMART_CON;

    try {
      let stableAdd; // stablecoin's contract address
      const stableSym = fromToken; // stablecoin symbol
      const stableDec = fromDec; // stablecoin decimal
      const buyerAdd = props.wallAdd; // user wallet address
      const buyerAmt = stableVal; // stablecoin value/amount
      const sellerAmt = tysVal; // custom token value/amount

      if(stableSym.toLowerCase() === "usdt") {
        stableAdd = process.env.REACT_APP_USDT_ADD;
      }

      // Approve user wallet for transaction
      const stableAbi = await getStableCoinAbi(stableSym.toLowerCase());
      const stableOpt = {
        contractAddress: process.env.REACT_APP_USDT_ADD,
        functionName: "approve",
        abi: stableAbi,
        params: {
          spender: buyerAdd,
          amount: Moralis.Units.Token(buyerAmt.toString(), stableDec.toString())
        },
      };
      
      const approveStable = await Moralis.executeFunction(stableOpt);

      // Approve tys wallet for transaction
      const tysAbi = await getStableCoinAbi("xrp"); // testing with xrp
      const tysOpt = {
        contractAddress: process.env.REACT_APP_TYS_ADD,
        functionName: "approve",
        abi: tysAbi,
        params: {
          spender: process.env.REACT_APP_TYS_WALLET,
          amount: Moralis.Units.Token(sellerAmt.toString(), "18")
        },
      };
      
      const approveTys = await Moralis.executeFunction(tysOpt);

      const abi = await getSwapAbi();
      const swapOpt = {
        contractAddress: swapConAdd,
        functionName: "swap",
        abi: abi,
        params: {
          buyerToken: stableAdd,
          buyer: buyerAdd,
          buyerAmt: Moralis.Units.Token(buyerAmt.toString(), stableDec.toString()),
          sellerAmt: Moralis.Units.Token(sellerAmt.toString(), "18")
        }
      };

      const swap = await Moralis.executeFunction(swapOpt);
      swap.on("error", (error) => {
        console.log(error);
      })

    } catch(error) {
      console.log("transfer error", error);
    }
  }  

Approving both erc20 tokens was a success, it was showing the right value, with the right token during Metamask confirmation & I was able to get the blockHash data & etc. back.

But when it run the executeFunction for swap, I got “execution reverted” error.

What am I missing here? How do I make this work?

who is buyerAdd, you should approve the smart contract address

@cryptokid buyerAdd is the buyer’s wallet address, which is also the current user’s wallet address, seller’s wallet address will always be the wallet where we hold our own custom token.

How do you approve a smart contract & which smart contract?
Do you mean calling the approve method for a transfer that’s in an erc20 token’s smart contract?

If it’s the latter then that’s already been called in “stableOpt”, under “contractAddress” :

...

const stableOpt = {
    contractAddress: process.env.REACT_APP_USDT_ADD, // USDT SMART CONTRACT ADDRESS
    functionName: "approve",
    abi: stableAbi,
    params: {
      spender: buyerAdd,
      amount: Moralis.Units.Token(buyerAmt.toString(), stableDec.toString())
    },
};

...

I think that on approve, you have to put on spender the address that will transfer that ERC20 token, in your case it may be the smart contract that will do that transfer from one address to another address.

Ok, let me give that a try.

@cryptokid I think you were right, I should have approved the smart contract address instead of wallet addresses as stated in the Metamask confirmation popup.

However, still getting the same “execution reverted” error when trying to executeFunction for swap.
What is the usual cause of this error?

sometimes you can see a specific error message in bscscan after you submit the transaction, if it happen for the execution to stop in a require

Yea sorry but there’s nothing on the testnet BscScan, no swap transactions recorded anywhere, I can only see the approve ones.

I mean the transaction that you are trying to make now, if you submit it, probably it will fail, but it could also show an error message

Yes, this is all that it is saying, same as before, which doesn’t really say much.

image

I mean, after you make that transaction with MetaMask, you can see the transaction that failed in bscscan and see there if there is additional info for the error.

There is literally NOTHING.

This is the latest transaction in Metamask

These are the latest transactions recorded on BscScan.

This is the last recorded bep-20 token transaction.

Like I said, I only see transactions for approve but not swap.

Then maybe you didn’t execute that swap, it should be there as a failed transaction.

Here’s the smart contract for swap:

pragma solidity ^0.8.10;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.4/contracts/token/ERC20/IERC20.sol";

contract TokenSwap {
  IERC20 public buyerToken;
  address public buyer;
  uint public buyerAmt;
  IERC20 public sellerToken;
  address public seller;
  uint public sellerAmt;

  constructor(
    address _buyerToken,
    address _buyer,
    uint _buyerAmt,
    uint _sellerAmt
  ) {
    buyerToken = IERC20(_buyerToken);
    buyer = _buyer;
    buyerAmt = _buyerAmt;
    sellerToken = IERC20(<<custom token contract address>>);
    seller = <<wallet address that holds our custom token>>;
    sellerAmt = _sellerAmt;
  }

  function swap() public {
    require(msg.sender == buyer || msg.sender == seller, "Not authorized");
    require(
        buyerToken.allowance(buyer, address(this)) >= buyerAmt,
        "Token 1 allowance too low"
    );
    require(
        sellerToken.allowance(seller, address(this)) >= sellerAmt,
        "Token 2 allowance too low"
    );

    _safeTransferFrom(buyerToken, buyer, seller, buyerAmt);
      _safeTransferFrom(sellerToken, seller, buyer, sellerAmt);
  }

  function _safeTransferFrom(
    IERC20 token,
    address sender,
    address recipient,
    uint amount
  ) private {
    bool sent = token.transferFrom(sender, recipient, amount);
    require(sent, "Token transfer failed");
  }
}

This is how swap function was executed:

...

const abi = await getSwapAbi();
const swapOpt = {
    contractAddress: swapConAdd, // swap contract address
    functionName: "swap",
    abi: abi,
    params: {
      buyerToken: stableAdd, // stablecoin contract address
      buyer: buyerAdd, // buyer's wallet address
      buyerAmt: Moralis.Units.Token(buyerAmt.toString(), stableDec.toString()), // value of how much buyer has to transfer
      sellerAmt: Moralis.Units.Token(sellerAmt.toString(), "18") // value of how much seller has to transfer
    }
};

const swap = await Moralis.executeFunction(swapOpt);
swap.on("error", (error) => {
    console.log(error);
})

...

This is the ABI for swap:

const abi = [
    {
      constant: true,
      inputs: [
        {
          internalType: "address",
          name: "buyerToken",
          type: "address"
        },
        {
          internalType: "address",
          name: "buyer",
          type: "address"
        },
        {
          internalType: "uint256",
          name: "buyerAmt",
          type: "uint256"
        },
        {
          internalType: "uint256",
          name: "sellerAmt",
          type: "uint256"
        }
      ],
      name: "swap",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256"
        }
      ],
      payable: true,
      stateMutability: "view",
      type: "function"
    }
];

Now, just looking at these, can you see anything & tell me how swap was not executed correctly?

it looks like there are 2 transfers here, that means that it will require two approves

Yes, 2 transfers. I did approve both, even though it might’ve been the wrong approach.

...

// Approve user wallet for transaction
      const stableAbi = await getStableCoinAbi(stableSym.toLowerCase());
      const stableOpt = {
        contractAddress: process.env.REACT_APP_USDT_ADD,
        functionName: "approve",
        abi: stableAbi,
        params: {
          spender: buyerAdd,
          amount: Moralis.Units.Token(buyerAmt.toString(), stableDec.toString())
        },
      };
      
      const approveStable = await Moralis.executeFunction(stableOpt);

      // Approve custom token wallet for transaction
      const tysAbi = await getStableCoinAbi("xrp"); // testing with xrp
      const tysOpt = {
        contractAddress: process.env.REACT_APP_TYS_ADD,
        functionName: "approve",
        abi: tysAbi,
        params: {
          spender: process.env.REACT_APP_TYS_WALLET,
          amount: Moralis.Units.Token(sellerAmt.toString(), "18")
        },
      };
      
      const approveTys = await Moralis.executeFunction(tysOpt);

...

Can I transfer out from our custom token wallet without having to approve? Since our goal is to make the swap automatic every time a user is trying to buy our custom token.