ERC20 atomic swap in solidity

928 views Asked by At

My goal is to create a simple Ethereum smart contract that allows for an atomic swap between tokens. Initially it should've been a cross-chain swap but I am struggling to do the basics. I am using ERC20 tokens that i have created using the openzeppelin ERC20.sol and IERC20.sol contracts and using Remix-IDE to create the contracts.

When i try to transfer the tokens or use the claim() function, I encounter errors such as

' ERC20: transfer amount exceeds balance'

' ERC20: insufficient allowance'

'Allowance must be greater than 0'

These errors occur even after i use the approve() function in the AliceCoin and BobCoin contracts

Some Questions:

A: Do i need to deploy the tokens separately before i can use it in my contract?

B: Should all the ERC20 functions be in the same contract as my atomic swap contract?

C: I have create ETH addresses for owner, recipient and token address. But how to i utilize the Remix IDE addresses given to me in my contract?

Any help is much appreciated

Code:

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.1;


import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";


contract AliceCoin is ERC20 {
    constructor(uint256 supply) ERC20("AliceCoin", "ALI") {
        _mint(msg.sender, supply);
    } 
}

contract BobCoin is ERC20{
    constructor(uint256 supply) ERC20("BobCoin", "BOB"){
        _mint(msg.sender, supply);
    }
}



contract AtomicSwap{

    ERC20 public tokenA;
    ERC20 public tokenB;


 
     // A constructor for the smart contract
     constructor() payable {
        //_owner = payable(msg.sender);
        //_recipient = payable(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        tokenA = new AliceCoin(100000);
        tokenB = new BobCoin(100000);

     }
    
    


     /* 
    Attributes of the Atomic Swap

    -- Timelock
    -- Hashlock
    -- Address transfer from
    -- Address transfer to
    -- secret key
    -- Token | That would use functions from the interface
    -- 

     */
    struct Swap {
        
        // User who should recieve the contract
        address payable recipient;


        // Owner of the coin 
        address payable Owner;

        // Address of the token
        address tokenAddress;
        // The amount of the tokens swapped 
        uint256 amount;
        //uint256 amount; // An amount of the token wanting to be transferred


        // Time stated for Swap to be executed
        uint256 timelock;
        

        // Cryptographic secret key 
        bytes32 Hashlock; //0xd218ef7a2461c961fdd5c0cd5a547f52b863d14db3e08e55f365e4cd0b4333c5;

        // Secret key 
        string secret; 

        // Boolean to check if the owner has been refunded 
        bool refunded; 
        // Boolean to check if the token has been claimed
        bool claimed;
    }

    mapping(address => mapping(address => uint256)) private _allowances;
    mapping(bytes32 => Swap) public swaps;


    event NewAtomicSwap(
        bytes32 swapId,
        address payable Owner,
        address payable recipient,
        address tokenAddress,
        uint256 amount,
        bytes32 Hashlock,
        uint256 timelock
    );
    event Claimed(
        bytes32 swapId
    );
    event Refunded(
        bytes32 swapId
    );

    // Modifiers 

    modifier checkAllowance(address _token, address _Owner, uint256 _amount){
        require(_amount > 0, "Token amount must be greater than 0");
        require(
            ERC20(_token).allowance(_Owner, address(this)) >= _amount,
            "Allowance must be greater than 0"
            
        );
        _;
    }

    modifier futureTimelock(uint256 _time){
        require(_time > block.timestamp, "timelock has to be set in the future");
        _;
    }


    modifier claimable(bytes32 _swapId) {
            require(swaps[_swapId].recipient == msg.sender, "This is not the right recipient");
            require(swaps[_swapId].claimed == false, "already claimed");
            require(swaps[_swapId].refunded == false, "already refunded");

            _;
    }

    modifier matchingHashlocks(bytes32 _swapId, bytes32 _x){
        require(
            swaps[_swapId].Hashlock == keccak256(abi.encodePacked(_x)),
            "incorrect hashlock"
        );
        _;
    }

    modifier existingContract(bytes32 _swapId) {
        require(haveContract(_swapId), "contract does not exist");
        _;
    }

    modifier refundable(bytes32 _swapId) {
        require(swaps[_swapId].Owner == msg.sender, "Only the sender of this coin can refund");
        require(swaps[_swapId].refunded == false, "Already refunded");
        require(swaps[_swapId].claimed == false, "Already claimed");
        require(swaps[_swapId].timelock <= block.timestamp, "Timelock not yet passed");
        _;
    }

    function newSwap(
        address payable _recipient, 
        bytes32 _Hashlock,
        uint256 _timelock,
        address _tokenAddress,
        uint256 _amount
        )
    public  // Visibility
    payable
    checkAllowance(_tokenAddress, msg.sender, _amount)
    futureTimelock(_timelock)
    returns(bytes32 swapId)
    {


        swapId = keccak256(
            abi.encodePacked(
                msg.sender,
                _recipient,
                _tokenAddress,
                _amount,
                _Hashlock,
                _timelock
            )
        );

        if(haveContract(swapId))
            revert("Contract exists");

        if(!AliceCoin(_tokenAddress).transfer(_recipient , _amount))
            revert("transfer failed");



        swaps[swapId] = Swap({
            recipient : _recipient,
            Owner : payable(msg.sender),
            tokenAddress : _tokenAddress,
            amount : msg.value,
            timelock : getTimestamp() + 60000,
            Hashlock : _Hashlock,
            secret : "djkcoeuxhjkdf",
            Open : false,
            locked : false,
            finished : false,
            refunded : false,
            claimed: false

        });

        emit NewAtomicSwap(
            swapId,
            payable(msg.sender),
            _recipient,
            _tokenAddress,
            _amount,
            _Hashlock,
            _timelock
        );     
    }

    /* Function for recipient to claim token */
    /* Only be claimed if Owner has opened the swap with _____ */

    function claim(bytes32 _swapId, bytes32 _Hashlock) 
    public 
    payable
    claimable(_swapId)
    matchingHashlocks(_swapId, _Hashlock)
    existingContract(_swapId)
    returns (bool)

    {
        Swap storage s = swaps[_swapId];
        s.Hashlock = _Hashlock;
        s.claimed = true;
        AliceCoin(s.tokenAddress).transfer(s.recipient, s.amount);
        emit Claimed(_swapId);
        return true;

    }

    function refund(bytes32 _swapId) 
    external
    existingContract(_swapId)
    refundable(_swapId)
    returns (bool) 
    {   
       Swap storage s = swaps[_swapId];
       s.refunded = true;
       AliceCoin(s.tokenAddress).transfer(s.Owner, s.amount);
       emit Refunded(_swapId);
       return true;
        

    }

    function haveContract(bytes32 _swapId)
        internal
        view
        returns (bool available)
        {
            available = (swaps[_swapId].Owner != address(0));
        }

1

There are 1 answers

1
Antonio Carito On

In your smart contract the issue is that the owner about your two ERC20 token (AliceCoin and BobCoin) is your smart contact. When you do this:

constructor() payable {
        //_owner = payable(msg.sender);
        //_recipient = payable(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        tokenA = new AliceCoin(100000);
        tokenB = new BobCoin(100000);
}

The msg.sender that interact with your ERC20 two contracts is smart contact itself. In this way, AtomicSwap smart contract has the ERC20 tokens and it is the owner and not your address! For resolve this issue, you must change your constructor logic in this way:

constructor(address _addressAliceCoin, address _addressBobCoin) payable {
    tokenA = ERC20(_addressAliceCoin);
    tokenB = ERC20(_addressBobCoin);
 }

And you must to deploy first ERC20 smart contracts and then your AtomicSwap contract, and pass into his constructor the address about ERC20 smart contracts. Another issue is the use about transfer in newSwap() function. This method to move tokens don't include two transactions like: approve+transfer consequently here you throw the error 'Allowance must be greater than 0'. To solve this issue you must to change this line:

if(!AliceCoin(_tokenAddress).transfer(_recipient , _amount))
            revert("transfer failed");

with this:

if(!AliceCoin(_tokenAddress).transferFrom(_recipient, address(this), _amount))
        revert("transfer failed")

that allow you to transfer user token into smart contract. I made some changes to your original smart contract, you can see and test this code:

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.1;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract AliceCoin is ERC20 {
    constructor(uint256 supply) ERC20("AliceCoin", "ALI") {
        _mint(msg.sender, supply);
    } 
}

contract BobCoin is ERC20{
    constructor(uint256 supply) ERC20("BobCoin", "BOB"){
        _mint(msg.sender, supply);
    }
}



contract AtomicSwap{
    ERC20 public tokenA;
    ERC20 public tokenB;

     // A constructor for the smart contract
     constructor(address _addressAliceCoin, address _addressBobCoin) payable {
        tokenA = ERC20(_addressAliceCoin);
        tokenB = ERC20(_addressBobCoin);
     }

     /* 
    Attributes of the Atomic Swap

    -- Timelock
    -- Hashlock
    -- Address transfer from
    -- Address transfer to
    -- secret key
    -- Token | That would use functions from the interface
    -- 

     */
    struct Swap {
        
        // User who should recieve the contract
        address payable recipient;
        // Owner of the coin 
        address payable Owner;
        // Address of the token
        address tokenAddress;
        // The amount of the tokens swapped 
        uint256 amount;
        //uint256 amount; // An amount of the token wanting to be transferred
        // Time stated for Swap to be executed
        uint256 timelock;
        // Cryptographic secret key 
        bytes32 Hashlock; //0xd218ef7a2461c961fdd5c0cd5a547f52b863d14db3e08e55f365e4cd0b4333c5;
        // Secret key 
        string secret; 
        // Boolean to check if the owner has been refunded 
        bool refunded; 
        // Boolean to check if the token has been claimed
        bool claimed;
    }

    mapping(address => mapping(address => uint256)) private _allowances;
    mapping(bytes32 => Swap) public swaps;


    event NewAtomicSwap(
        bytes32 swapId,
        address payable Owner,
        address payable recipient,
        address tokenAddress,
        uint256 amount,
        bytes32 Hashlock,
        uint256 timelock
    );
    event Claimed(
        bytes32 swapId
    );
    event Refunded(
        bytes32 swapId
    );

    // Modifiers 

    modifier checkAllowance(address _token, address _Owner, uint256 _amount){
        require(_amount > 0, "Token amount must be greater than 0");
        require(ERC20(_token).allowance(_Owner, address(this)) >= _amount, "Allowance must be greater than 0");
        _;
    }

    modifier futureTimelock(uint256 _time){
        require(_time > block.timestamp, "timelock has to be set in the future");
        _;
    }

    modifier claimable(bytes32 _swapId) {
            require(swaps[_swapId].recipient == msg.sender, "This is not the right recipient");
            require(swaps[_swapId].claimed == false, "already claimed");
            require(swaps[_swapId].refunded == false, "already refunded");
            _;
    }

    modifier matchingHashlocks(bytes32 _swapId, bytes32 _x){
        require(
            swaps[_swapId].Hashlock == keccak256(abi.encodePacked(_x)),
            "incorrect hashlock"
        );
        _;
    }

    modifier existingContract(bytes32 _swapId) {
        require(haveContract(_swapId), "contract does not exist");
        _;
    }

    modifier refundable(bytes32 _swapId) {
        require(swaps[_swapId].Owner == msg.sender, "Only the sender of this coin can refund");
        require(swaps[_swapId].refunded == false, "Already refunded");
        require(swaps[_swapId].claimed == false, "Already claimed");
        require(swaps[_swapId].timelock <= block.timestamp, "Timelock not yet passed");
        _;
    }

    function newSwap(
        address payable _recipient, 
        bytes32 _Hashlock,
        uint256 _timelock,
        address _tokenAddress,
        uint256 _amount
        )
    public  // Visibility
    payable
    checkAllowance(_tokenAddress, msg.sender, _amount)
    futureTimelock(_timelock)
    returns(bytes32 swapId)
    {
        swapId = keccak256(
            abi.encodePacked(
                msg.sender,
                _recipient,
                _tokenAddress,
                _amount,
                _Hashlock,
                _timelock
            )
        );

        if(haveContract(swapId))
            revert("Contract exists");

        // NOTE: Use transferFrom() function if you would to approve and transfer ERC20 user tokens.
        if(!AliceCoin(_tokenAddress).transferFrom(_recipient, address(this), _amount))
            revert("transfer failed");



        swaps[swapId] = Swap({
            recipient : _recipient,
            Owner : payable(_recipient),
            tokenAddress : _tokenAddress,
            amount : _amount,
            timelock : block.timestamp + 60000,
            Hashlock : _Hashlock,
            secret : "djkcoeuxhjkdf",
            // Open : false,
            // locked : false,
            // finished : false,
            refunded : false,
            claimed: false

        });

        emit NewAtomicSwap(
            swapId,
            payable(_recipient),
            _recipient,
            _tokenAddress,
            _amount,
            _Hashlock,
            _timelock
        );     
    }

    /* Function for recipient to claim token */
    /* Only be claimed if Owner has opened the swap with _____ */

    function claim(bytes32 _swapId, bytes32 _Hashlock) public payable claimable(_swapId) matchingHashlocks(_swapId, _Hashlock) existingContract(_swapId) returns(bool){
        Swap storage s = swaps[_swapId];
        s.Hashlock = _Hashlock;
        s.claimed = true;
        AliceCoin(s.tokenAddress).transfer(s.recipient, s.amount);
        emit Claimed(_swapId);
        return true;
    }

    function refund(bytes32 _swapId) external existingContract(_swapId) refundable(_swapId) returns(bool) {   
       Swap storage s = swaps[_swapId];
       s.refunded = true;
       AliceCoin(s.tokenAddress).transfer(s.Owner, s.amount);
       emit Refunded(_swapId);
       return true;
    }

    function haveContract(bytes32 _swapId) internal view returns (bool available){
        available = (swaps[_swapId].Owner != address(0));
    }
}