How to Use Atomic Swap
1. Set a Token Allowance
A token allowance is required if you want a third-party to move funds on your behalf. In short, you are allowing them to move your tokens.
In our case, we would like the Orion Exchange contract to trade our ERC20 tokens for us, so we will need to approve an allowance (a certain amount) for this contract to move a certain amount of our ERC20 tokens on our behalf.
When setting the token allowance, make sure to provide enough allowance for the buy or sell amount as well as the gas.
TIP
When setting the token allowance, make sure to provide enough allowance for the buy or sell amount as well as the gas; otherwise, you may receive a 'Gas estimation failed' error.
2. Perform initial lock transaction.
In order to carry out a cross-chain transaction, the first step is to lock your assets on the origin chain. This is achieved by invoking the lockAtomic
method on the Exchange contract. The process involves two main steps described in following sections.
2.1 Payload preparation (secret)
Here is the code snippet to correctly generate secretHash & secret for lockOrder.
This part of the process involves generating a secret hash and secret for the lock order. Here's a Go code snippet that accomplishes this task. This code utilizes the crypto/rand
package from Go's standard library to generate random bytes, and the crypto
package from go-ethereum
to hash the secret.
You can check this code here: https://go.dev/play/p/OkyX1dqJorS
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"strconv"
"github.com/ethereum/go-ethereum/crypto"
)
func main() {
const randomBits = 256
randomBytes := make([]byte, randomBits/8)
if _, err := rand.Read(randomBytes); err != nil {
log.Fatal(err)
}
random := hex.EncodeToString(randomBytes)
// user address, amount, asset name, chain id, salt
data := [][]byte{
[]byte("0x61Eed69c0d112C690fD6f44bB621357B89fBE67F"),
[]byte(strconv.FormatInt(12400000000, 10)),
[]byte("ORN"),
[]byte(strconv.FormatInt(0x61, 10)),
[]byte(random),
}
hash := crypto.Keccak256Hash(data...)
fmt.Printf("Hash: %s\n", hash.Hex())
// will output this
// Hash: 0xa1c0f31c75a94bf6beb9587c26da286a4dea9b83936eaac4648851a3f0e7fd46
// Then we can form LockOrder
}
2.2 Payload preparation (lockOrder)
Here is the code snipper to perform lockAtomic
call.
This step involves preparing the lock order payload and then invoking the lockAtomic
function on the smart contract. Here's a Go code snippet that uses the go-ethereum
package to interact with the EVM network. This code snippet essentially constructs a transaction calling the lockAtomic
method on the contract, then signs it with the private key, and finally sends it to the EVM network of your choice.
package atomicswap
import (
"context"
"crypto/ecdsa"
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"math/big"
"strconv"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"time"
)
// solidity-compatible struct
type LockOrder struct {
Sender common.Address
Expiration uint64
Asset common.Address
Amount uint64
TargetChainId *big.Int
SecretHash [32]byte
}
// RedeemOrder represents the input parameters to the redeemAtomic function.
type RedeemOrder struct {
Sender common.Address
Receiver common.Address
ClaimReceiver common.Address
Asset common.Address
Amount uint64
Expiration uint64
SecretHash [32]byte
Signature []byte
}
const ExchangeWithAtomicABI = `[{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint64","name":"expiration","type":"uint64"},{"internalType":"address","name":"asset","type":"address"},{"internalType":"uint64","name":"amount","type":"uint64"},{"internalType":"uint24","name":"targetChainId","type":"uint24"},{"internalType":"bytes32","name":"secretHash","type":"bytes32"}],"internalType":"struct LibAtomic.LockOrder","name":"swap","type":"tuple"}],"name":"lockAtomic","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"claimReceiver","type":"address"},{"internalType":"address","name":"asset","type":"address"},{"internalType":"uint64","name":"amount","type":"uint64"},{"internalType":"uint64","name":"expiration","type":"uint64"},{"internalType":"bytes32","name":"secretHash","type":"bytes32"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct LibAtomic.RedeemOrder","name":"order","type":"tuple"},{"internalType":"bytes","name":"secret","type":"bytes"}],"name":"redeemAtomic","outputs":[],"stateMutability":"nonpayable","type":"function"}]`
// cl65:~ $ cast wallet new
// Address: 0xfb77f4bF210eb58D3966eE6C8fB23E802F8Cf394
// Private key: 0x435200deac28cf6acb411238abf556c48f74afc2e5d6bea4d7a2cd754c78d564
func main() {
client, err := ethclient.Dial("https://rpc.mevblocker.io")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
privateKey, err := crypto.HexToECDSA("435200deac28cf6acb411238abf556c48f74afc2e5d6bea4d7a2cd754c78d564")
if err != nil {
log.Fatal(err)
}
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
parsedABI, err := abi.JSON(strings.NewReader(ExchangeWithAtomicABI)) // replace with your contract ABI
if err != nil {
log.Fatalf("Failed to parse contract ABI: %v", err)
}
targetChain := big.NewInt(56)
swap := &LockOrder{
Sender: common.HexToAddress("0xSenderAddress"), // replace with your sender address
Expiration: time.Now().Unix(),
Asset: common.HexToAddress("0xAssetAddress"), // replace with your asset address
Amount: 1000,
TargetChainId: targetChain,
SecretHash: crypto.Keccak256Hash([]byte("secret")),
}
data, err := parsedABI.Pack("lockAtomic", swap)
if err != nil {
log.Fatalf("Failed to pack data for lockAtomic: %v", err)
}
gasLimit := uint64(450_000)
toAddress := common.HexToAddress("0xYourContractAddress") // replace with your contract address
tx := types.NewTransaction(nonce, toAddress, big.NewInt(0), gasLimit, gasPrice, data)
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
fmt.Printf("signedTx: %+v", signedTx)
//
// err = client.SendTransaction(context.Background(), signedTx)
// if err != nil {
// log.Fatal(err)
// }
}
It's necessary to replace "ExchangeWithAtomicABI"
with the ABI of Exchange contract. Lastly, remember to uncomment the line that sends the transaction if you wish to perform the actual lock operation.
3. Order placement using Atomic Swap API
Next step is let Aggregator (Atomic Swap API) know that you've sent lock transaction. This step is crucial to make redeem possible on target chain.
In our example, origin chain is BSC, destination chain is Fantom.
package main
import (
"bytes"
"log"
"net/http"
)
func main() {
// you should use destination chain API
url := "https://trade-ftm.orionprotocol.io/backend/api/v1/atomic-swap"
data := `{"secretHash":"0xa61f19d6a28c191ccfe32d41754e5ab2545cc1b72b2731f872e0275a1b27520a","sourceNetworkCode":"BSC"}`
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
if err != nil {
log.Fatal("Error creating request: ", err)
}
req.Header.Set("authority", "trade-ftm.orionprotocol.io")
req.Header.Set("accept", "application/json")
req.Header.Set("accept-language", "en-US,en;q=0.9,ru;q=0.8")
req.Header.Set("content-type", "application/json")
req.Header.Set("origin", "https://trade.orionprotocol.io")
req.Header.Set("referer", "https://trade.orionprotocol.io/")
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-site", "same-site")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error sending request: ", err)
}
defer resp.Body.Close()
// Handle response here
}
4. Calling redeemAtomic method
package atomicswap
import (
"context"
"crypto/ecdsa"
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"math/big"
"strconv"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
// cl65:~ $ cast wallet new
// Address: 0xfb77f4bF210eb58D3966eE6C8fB23E802F8Cf394
// Private key: 0x435200deac28cf6acb411238abf556c48f74afc2e5d6bea4d7a2cd754c78d564
func main() {
client, err := ethclient.Dial("https://rpc.mevblocker.io")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
privateKey, err := crypto.HexToECDSA("435200deac28cf6acb411238abf556c48f74afc2e5d6bea4d7a2cd754c78d564")
if err != nil {
log.Fatal(err)
}
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
parsedABI, err := abi.JSON(strings.NewReader(ExchangeWithAtomicABI)) // replace with your contract ABI
if err != nil {
log.Fatalf("Failed to parse contract ABI: %v", err)
}
order := RedeemOrder{
Sender: common.HexToAddress("0xSenderAddress"), // replace with your sender address
Receiver: common.HexToAddress("0xReceiverAddress"), // replace with your receiver address
ClaimReceiver: common.HexToAddress("0xClaimReceiver"), // replace with your claim receiver address
Asset: common.HexToAddress("0xAssetAddress"), // replace with your asset address
Amount: uint64(1000),
Expiration: uint64(123456789),
SecretHash: crypto.Keccak256Hash([]byte("secretHash")),
Signature: []byte("signature"), // replace with your signature
}
// parsedABI, err := abi.JSON(strings.NewReader(contractABI))
if err != nil {
log.Fatalf("Failed to parse contract ABI: %v", err)
}
data, err := parsedABI.Pack("redeemAtomic", order, []byte("secret"))
if err != nil {
log.Fatalf("Failed to pack data for redeemAtomic: %v", err)
}
gasLimit := uint64(450_000)
toAddress := common.HexToAddress("0xYourContractAddress") // replace with your contract address
tx := types.NewTransaction(nonce, toAddress, big.NewInt(0), gasLimit, gasPrice, data)
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
fmt.Printf("signedTx: %+v", signedTx)
//
// err = client.SendTransaction(context.Background(), signedTx)
// if err != nil {
// log.Fatal(err)
// }
}
In the given RedeemOrder structure, you also need to replace []byte("signature")
with the actual byte slice signature you received when the redeem order was created. Please remember that the process of generating a proper signature might be quite complex and can require specific cryptographic libraries or tools.
Last updated