Swapper. Atomic swap | multi-token | multi-standard | multi-type NFT / FT

I am presenting to you what I believe is an integral part of DeFi, which if done right will be useful to everyone in the ecosystem.
I am hoping this becomes a project owned by the community to which everyone contributes, I have no plans of developing it alone, I am just initiating it, so don’t wait for me to do it all, add some code/tests if it interests you.
It’s a permissionless swapping system where nobody is left out and anyone can add their own standard and deploy their own Swapper instances.

Abstraction:
Maker - someone who creates order and describes what they are giving and receiving. Think of it as two briefcases with token descriptions. The maker gives the order to a Taker.
The Taker fills their briefcase according to the description and hands it to a custodian (immutable canister - Swapper)
Next Swapper checks and verifies everything is as described. Maker fills their briefcase. It’s checked by Swapper again. If everything is as both agreed on the swap of briefcase ownership is done. Now both can take out their tokens. If not, they can get a refund - their own briefcases.

Note:
This was born in the ICRC-1 thread, but it should be on its own.
I’ve just upgraded it from single token swap to multi-token multi-standard multi-type multi-address.

Architecture: It makes one or two more inter-canister calls compared to doing all this inside a ledger canister or inside a ledger+minter. However, having it isolated in its own immutable canister, you gain tremendous amounts of interoperability, security, and trustlessness. It provides a guarantee that each party will get what they agreed to. Bugs coming from 3rd party developers will result in refunds and nothing will be lost.

Repo here: GitHub - infu/swapper
License Apache 2, feel free to fork and send pull requests.
Current status: theoretically working, untested

Pseudo code usage:
this interface will need rework and callback functions

let orderId = await Swapper.make(
// maker description
[ 
#ICPL{ledger="...icp.ledger.principal.."; count= 1000000000 } // first token
#ANV{ count= 100000 } // second token (intentionally a different record)
]
// taker description
[#DIP{ledger="....some.ledger.principal....."; count = 2343434}]
);

//.. now we send these tokens to Swapper addresses by calling each ledger
await ICPLedger.transfer(....
await Swapper.notify(orderId, 0); // notify Swapper that we sent this token

await ANVLedger.transfer(....
await Swapper.notify(orderId, 1); // notify Swapper that we sent this token

//.. here we wait for a callback from Swapper or timeout

//.. and now we take our swapped tokens out
await Swapper.transfer(orderId, 0, #DIP{address="...my.address..."});

Contributing:
I’ve managed to isolate every standard to be in it’s own file.
The logic is in swapper.mo
Standards are in /standards/
It currently has one - ICPL - ICP Ledger

To add a new standard, you add a new file and then attach it inside standards.mo
Let’s imagine we had 3 token standards, it would look like this:

public type Target = {
      #ICPL : ICPL.Target;
      #DIP : DIP.Target;
      #ANV : ANV.Target;

      //ADDHERE
  };

  public type TokenDescription = {
      #ICPL : ICPL.TokenDescription;
      #DIP : DIP.TokenDescription;
      #ANV : ANV.TokenDescription;

      //ADDHERE
  };

  public type TokenState = {
      #ICPL : ICPL.TokenState;
      #DIP : DIP.TokenState;
      #ANV : ANV.TokenState;

      //ADDHERE
  };

  public func status(x : TokenState) : Common.TokenStatus {
    switch(x) {
      case (#ICPL(a)) a.status;
      case (#DIP(a)) a.status;
      case (#ANV(a)) a.status;

      //ADDHERE
    };
  };

  public func make(my_principal: Principal, orderId: Common.OrderId, td: TokenDescription) : TokenState {
    switch(td) {
      case (#ICPL(a)) #ICPL(ICPL.make(my_principal, orderId, a));
      case (#DIP(a)) #DIP(DIP.make(my_principal, orderId, a));
      case (#ANV(a)) #ANV(ANV.make(my_principal, orderId, a));

      //ADDHERE
    };
  };

...

The flow is something like this (no callbacks added yet):

4 Likes

You announced interest in multi-standard operations, so I am tagging you @Hazel @skilesare

1 Like

Awesome @infu! On first blush it looks like a great solution….I’m traveling right now and will take a look later. We have some thing like this in our NFT and I like the pattern.

Does the swapper have to be in its own? If a service is open source and blackhole or managed via a dao could you just ref this code from the service and save some intercanister calls?

2 Likes

Yes, you can merge some functionality and reduce inter-canister calls. I guess it makes sense to do that when providing staking or handing LP tokens.
However it comes with changes in the trust dynamics - you probably won’t be making the whole thing immutable, while making a discardable part like Swapper immutable is easy.
From the diagram below, it seems you just need to change the notify function on swapper.mo to do everything (maybe also have the make function before that).


In other cases, let’s say you want this to power a multi-nft standard multi-fungible marketplace, where you will work with a few non-fungible and few fungible tokens. Then you wouldn’t want to (and probably can’t) add it all around, so isolated is the only option.

2 Likes