This is a simple example in Solidity that can be run in Remix to understand how an ERC20 Token Swap managed by a Swapping Smart Contract works
1. Smart Contracts
1.1 ERC20 Token
An ERC20 Token can be simply created by using the OpenZeppelin implementation that can be easily imported in Remix as follows
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.1.0/contracts/token/ERC20/ERC20.sol";
contract Token1 is ERC20 {
constructor () public ERC20("Token", "TKN1") {
_mint(msg.sender, 1000000 * (10 ** uint256(decimals())));
}
}
This implementation grants the address running the SC Deployment TX 1000000 tokens
Since we need 2 tokens to do the swap, we are going to have Wallet1 deploying TKN1 and Wallet2 deploying TKN2
I am assuming you can compile and deploy them with Remix so the situation will be something like
Role
Address
Wallet1
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
Token1
0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B
Wallet2
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
Token2
0xAc40c9C8dADE7B9CF37aEBb49Ab49485eBD3510d
1.2 Swap Smart Contract
The Swapping Contract will have a swap() method that swaps an amount X1 of TKN1 from Wallet1 to Wallet2 and at the same time an amount X2 of TKN2 from Wallet2 to Wallet1
Since it needs to manipulate the 2 ERC20 Tokens, it needs to import the definition of the IERC20 interface that can be again found in the OpenZepllin framework
Its implementation can be simply
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol";
contract TokenSwap {
IERC20 public token1;
address public owner1;
uint256 public amount1;
IERC20 public token2;
address public owner2;
uint2356 public amount2;
constructor(
address _token1,
address _owner1,
uint256 _amount1,
address _token2,
address _owner2,
uint256 _amount2
) {
token1 = IERC20(_token1);
owner1 = _owner1;
amount1 = _amount1;
token2 = IERC20(_token2);
owner2 = _owner2;
amount2 = _amount2;
}
function swap() public {
require(msg.sender == owner1 || msg.sender == owner2, "Only one of the two token owners is allowed");
require(
token1.allowance(owner1, address(this)) >= amount1,
"Token 1 allowance too low"
);
require(
token2.allowance(owner2, address(this)) >= amount2,
"Token 2 allowance too low"
);
_safeTransferFrom(token1, owner1, owner2, amount1);
_safeTransferFrom(token2, owner2, owner1, amount2);
}
function _safeTransferFrom(
IERC20 token,
address sender,
address recipient,
uint amount
) private {
bool sent = token.transferFrom(sender, recipient, amount);
require(sent, "Token transfer failed");
}
}
The swap() public method essentially does 2 things
first, it runs all the required checks with require() so that if one of these conditions is not met, the TX is reverted before spending any unnecessary gas
second, it runs the actual swaps
The actual implementation of the swaps is in the private function _safeTransferFrom() (that is not accessible outside of the SC so it can be reached only after the require() have been passed) and it is wrapping the ERC20 transferFrom() with a require() that checks its return value
This is a common pattern: use require() to check conditions as soon as possible so for example
at the beginning of a function definition to check the input args
right after a method call to check its return value
Let's proceed to compile and deploy also this contract to the Swap address passing the arguments about Wallet1, Token1, Wallet2 and Token2
At the end of this step the situation will be something like
Role
Address
Wallet1
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
Token1
0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B
Wallet2
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
Token2
0xAc40c9C8dADE7B9CF37aEBb49Ab49485eBD3510d
Swap
0x438eacEBf3F2a1c3E8560277345E83ff228355bE
2. Run
To run the swap() then 2 steps are required
setting the allowances
use the Swap Smart Contract
2.1 Setting the allowance
To be able to swap a given amount of token, each Token Onwer needs to allow the expense in advance
This is done by calling from the owner address the ERC20 approve() method passing both spender address and the amount
In the swap case the spender is the Swap Contract while the other owner is the recipient, so in both cases we need to set spender=0x438eacEBf3F2a1c3E8560277345E83ff228355bE so if we want to swap 100 of Token1 for 50 of Token2 then
for the Token1 we call from 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 so
and this can be checked by calling the ERC20 allowance() method from Wallet1 to Swap
for Token2 we call from 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 so
and then we check
2.2 Swap
Finally we can call from Wallet1 or Wallet2 the Swap at the swap() method
From the Remix Log we see the TX succeeds
3. Check
To verify the swap has been successful let's check the new balances by calling the ERC20 balanceOf() of one token for the opposite owner
For Token1 owned by Wallet1 we see that Wallet2 has
For Token2 owned by Wallet2 we see that Wallet1 has
Overview
This is a simple example in Solidity that can be run in Remix to understand how an ERC20 Token Swap managed by a Swapping Smart Contract works
1. Smart Contracts
1.1 ERC20 Token
An ERC20 Token can be simply created by using the OpenZeppelin implementation that can be easily imported in Remix as follows
This implementation grants the address running the SC Deployment TX 1000000 tokens
Since we need 2 tokens to do the swap, we are going to have
Wallet1
deployingTKN1
andWallet2
deployingTKN2
I am assuming you can compile and deploy them with Remix so the situation will be something like
Wallet1
Token1
Wallet2
Token2
1.2 Swap Smart Contract
The Swapping Contract will have a
swap()
method that swaps an amountX1
ofTKN1
fromWallet1
toWallet2
and at the same time an amountX2
ofTKN2
fromWallet2
toWallet1
Since it needs to manipulate the 2 ERC20 Tokens, it needs to import the definition of the
IERC20
interface that can be again found in the OpenZepllin frameworkIts implementation can be simply
The
swap()
public method essentially does 2 thingsrequire()
so that if one of these conditions is not met, the TX is reverted before spending any unnecessary gasThe actual implementation of the swaps is in the private function
_safeTransferFrom()
(that is not accessible outside of the SC so it can be reached only after therequire()
have been passed) and it is wrapping the ERC20transferFrom()
with arequire()
that checks its return valueThis is a common pattern: use
require()
to check conditions as soon as possible so for exampleLet's proceed to compile and deploy also this contract to the
Swap
address passing the arguments aboutWallet1
,Token1
,Wallet2
andToken2
At the end of this step the situation will be something like
Wallet1
Token1
Wallet2
Token2
Swap
2. Run
To run the
swap()
then 2 steps are required2.1 Setting the allowance
To be able to swap a given amount of token, each Token Onwer needs to allow the expense in advance
This is done by calling from the
owner
address the ERC20approve()
method passing bothspender
address and theamount
In the swap case the
spender
is the Swap Contract while the other owner is the recipient, so in both cases we need to setspender=0x438eacEBf3F2a1c3E8560277345E83ff228355bE
so if we want to swap100
ofToken1
for50
ofToken2
then0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
soand this can be checked by calling the ERC20
allowance()
method fromWallet1
toSwap
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
soand then we check
2.2 Swap
Wallet1
orWallet2
theSwap
at theswap()
method3. Check
balanceOf()
of one token for the opposite ownerToken1
owned byWallet1
we see thatWallet2
hasToken2
owned byWallet2
we see thatWallet1
hasSo that confirms the swap has been successful