0

I have deployed and tested my custom token contract using Openzeppelin contracts successfully. I need to design a dapp using metamask. I want to sell my tokens in exchange for Tether Token. In order to transfer token to buyer Is there any way to transfer token to buyer without need to pay the gas fee? I mean using a mechanism to make buyer does transaction.

thanks

3 Answers 3

0

Nice question in terms of Account Abstractions (ERC-4337), though its primary aim is to allow dApp users to submit gasless transactions, dApp developers could also use it as a way to allow users to submit transactions on behalf of the dApp.

Before delving into the specifics of Account Abstractions, there are other solutions to this question:

1. Deploy a Smart Contract

Develop a contract designed to receive USDT from a user and subsequently send a specific amount of tokens back. This option is preferable, as it ensures a higher level of security for the buyer's payment and token receipt, all within a single transaction. The contract could be structured as follows:

contract IDOSale {
    address usdt = address(0xdac17f958d2ee523a2206206994597c13d831ec7);
    address token = address(0x<your_token>);
    
    uint256 priceUsdWei = 1 * 10**usdt.decimals();

    function buyExactTokens (uint amountWei) external {
        require(amount <= token.balanceOf(address(this), "Not enough tokens for sale"));

        uint purchasePrice = amountWei * priceUsdWei / 10**token.decimals();
        // will revert when not enough approval/balance
        usdt.transferFrom(msg.sender, address(this), purchasePrice);
        token.transfer(msg.sender, amount);
    }
    // <... other withdrawal, price update etc. methods ...>
}

2. Create permit signatures

If the token implements EIP-2612: Permit extension:

The backend generates the permit signature and sends it to the user. The user will submit the transaction to approve the tokens and transfer them. The method to create the signature will look like the following with the dequanto library:

import { ERC20Permit } from '@dequanto-contracts/openzeppelin/ERC20Permit';
import { Web3ClientFactory } from '@dequanto/clients/Web3ClientFactory';
import { TAddress } from '@dequanto/models/TAddress';
import { $date } from '@dequanto/utils/$date';
import { $signRaw } from '@dequanto/utils/$signRaw';
import { $signSerializer } from '@dequanto/utils/$signSerializer';

const TOKEN_ADDRESS = '0x....';

async function createPermitSignature (owner: { address, key }, receiver: TAddress, amount: bigint) {
    const client = Web3ClientFactory.get('eth');
    const erc20 = new ERC20Permit(TOKEN_ADDRESS, client);;
    const nonce = await erc20.nonces(owner.address);
    const deadline = $date.tool().add('1month').toUnixTimestamp();

    let typedData = {
        types: {
            EIP712Domain: [
                { name: 'name', type: 'string' },
                { name: 'version', type: 'string' },
                { name: 'chainId', type: 'uint256' },
                { name: 'verifyingContract', type: 'address' },
            ],
            Permit: [
                { type: 'address', name: 'owner' },
                { type: 'address', name: 'spender' },
                { type: 'uint256', name: 'value' },
                { type: 'uint256', name: 'nonce' },
                { type: 'uint256', name: 'deadline' },
            ],
        },
        primaryType: 'Permit',
        domain: {
            name: 'PermitToken',
            version: '1',
            chainId: client.chainId,
            verifyingContract: erc20.address,
        },
        message: {
            owner: owner.address,
            spender: receiver,
            value: amount,
            nonce: nonce,
            deadline: deadline
        }
    };

    let hashed = $signSerializer.serializeTypedData(typedData);
    let { v, r, s } = await $signRaw.signEC(hashed, owner.key);
    return { v, r, s };
}

However, its capabilities are quite limited due to the nonce validation in ERC20Permit.sol#L57. This means that while it's possible to generate multiple permit signatures for different users, each with a new nonce, the signatures must be sent on-chain in the correct order. You can, however, modify OpenZeppelin's implementation similarly to how it functions in the following example for Account Abstraction Transactions.

2. EIP-4337: Account Abstraction

For the example I will use the following 4337 setup and dequanto library:

  1. The token distributor creates the ERC4337 Account and transfers the required amount of tokens to that address.

    import { ERC20 } from '@dequanto-contracts/openzeppelin/ERC20';
    import { BlockChainExplorerProvider } from '@dequanto/BlockchainExplorer/BlockChainExplorerProvider';
    import { Web3ClientFactory } from '@dequanto/clients/Web3ClientFactory';
    import { Erc4337TxWriter } from '@dequanto/erc4337/Erc4337TxWriter';
    
    const TOKEN = '0x<token>'
    
    async function createAndFundThe4337Account (owner: { key, address }, amount: bigint) {
        let client = Web3ClientFactory.get('eth');
        let erc4337Writer = await createErc4337Writer();
    
        // checks if the 4337 account for the owner is already deployed
        let { address: erc4337Address } = await erc4337Writer.ensureAccount(owner);
    
        let erc20 = new ERC20(TOKEN, client);
        // transfer the tokens to the address, so that later the users can claim  them
        let txTransfer = await erc20.transfer(owner, erc4337Address, amount);
        let receipt = await txTransfer.wait();
    }
    
    async function createErc4337Writer () {
        let client = Web3ClientFactory.get('eth');
        let explorer = BlockChainExplorerProvider.get('eth');
        let erc4337Writer = new Erc4337TxWriter(client, explorer, {
            // 4337 setup requires entrypoint contract and accountFactory contract 
            addresses: {
                entryPoint: erc4337Contracts.entryPointContract.address,
                accountFactory: erc4337Contracts.accountFactoryContract.address,
            }
        });
        return erc4337Writer;
    }
    
  2. Now, when the token distributor wants to transfer a particular amount of tokens to the user, they will create the UserOperation and sign it.

    import { ERC20 } from '@dequanto-contracts/openzeppelin/ERC20';
    import { BlockChainExplorerProvider } from '@dequanto/BlockchainExplorer/BlockChainExplorerProvider';
    import { Web3ClientFactory } from '@dequanto/clients/Web3ClientFactory';
    import { Erc4337TxWriter } from '@dequanto/erc4337/Erc4337TxWriter';
    import { UserOperation } from '@dequanto/erc4337/models/UserOperation';
    import { TAddress } from '@dequanto/models/TAddress';
    
    const TOKEN = '0x<token>'
    
    async function prepare4337TransferUserOperation (owner: { key, address }, receiver: TAddress, amount: bigint) {
        let erc4337Writer = await createErc4337Writer();
        let { address: erc4337Address } = await erc4337Writer.getAccount(owner);
        let erc20 = new ERC20(TOKEN);
    
        // Create normal transaction Data for transferring the tokens
        let txTransferData = await erc20.$data().transfer(erc4337Address, receiver, amount);
    
        // Pack the transaction data into the UserOperation JSON and also sign it with owner's key
        let { op: userOperation } = await erc4337Writer.prepareUserOp({
            owner: owner,
            tx: txTransferData,
            erc4337Account: {
                address: erc4337Address
            },
            // here the key point to be able to generate nonces per receiver address
            nonceSalt: BigInt(receiver)
        });
        return userOperation;
    }
    
  3. The user operation JSON is sent to the user, who will then submit it on-chain with their account, thus paying for the gas..

    import { BlockChainExplorerProvider } from '@dequanto/BlockchainExplorer/BlockChainExplorerProvider';
    import { Web3ClientFactory } from '@dequanto/clients/Web3ClientFactory';
    import { Erc4337TxWriter } from '@dequanto/erc4337/Erc4337TxWriter';
    import { UserOperation } from '@dequanto/erc4337/models/UserOperation';
    
    const TOKEN = '0x<token>'
    
    async function submit4337TransferUserOperation (userOperation: UserOperation, receiver: { key, address }) {
        let erc4337Writer = await createErc4337Writer();
        let tx = await erc4337Writer.submitUserOp(receiver, userOperation);
        let txReceipt = await tx.wait();
        return txReceipt;
    }
    

The nonce can be created per recipient address - NonceManager.sol#L37 It splits the uin256 nonce into 2 components: prefix and the incremental nonce, so that address can be used as the prefix.

1

For sending without gas, see the Gas Station Network and Meta Transactions come to mind: https://docs.openzeppelin.com/learn/sending-gasless-transactions

In your case, paying with Tether, they'll have to approve (increase the allowance) first.

Don't overcomplicate, let them pay for gas, that's pretty standard.

1

If you want to sell your token for Tether and you don't want to write a custom contract, you can:

  1. Make a Uniswap pair using UniswapV2Factory https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Factory.sol#L32
  2. Add liquidity: https://uniswap.org/docs/v2/smart-contract-integration/providing-liquidity/
  3. Let users users swap for your token directly on uniswap, or encode the swap yourself. https://uniswap.org/docs/v2/smart-contract-integration/trading-from-a-smart-contract/

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.