Skip to main content

Create the swap contract

The swap contract needs to do the following:

  • Accept swap requests
  • Match swap requests and exchange tokens
  • Handle excess tokens and send them back to wallets located on different shards

Contract definition


pragma solidity ^0.8.26;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

/**
* @title SwapMatch
* @author =nil; Foundation
* @notice The contract matches swap requests and performs token swaps.
* On a successful match, the desired tokens are sent to the swap originators.
* The contract also calculates token excesses and sends them back on a successful match.
*/
contract SwapMatch is NilBase {}

Structs and properties

The contract contains the following structs and properties:

/**
* @notice The struct representing a successful swap.
*/
struct Swap {
address firstParty;
address secondParty;
CurrencyId firstTokenId;
CurrencyId secondTokenId;
uint256 firstTokenExchanged;
uint256 secondTokenExchanged;
}

/**
* @notice The struct representing a single swap request.
*/
struct SwapRequest {
address initiator;
Nil.Token token;
CurrencyId secondTokenId;
uint256 desiredSecondTokenAmount;
bool isMatched;
}

/**
* @notice A map of all successful swaps.
*/
mapping(uint256 => Swap) public swaps;

/**
* @notice A map of all existing swap requests.
*/
mapping(uint256 => SwapRequest) public swapRequests;

/**
* @dev Counters for swaps and swap requests.
*/
uint256 private swapCounter = 0;
uint256 private swapRequestCounter = 0;

  • The Swap struct stores data about a token exchange that has been processed successfuly. It shows both parties that participated in the swap as well as information about the associated tokens
  • The SwapRequest struct represents a single request for a token exchange coming from one wallet (initiator). It contains the token the wallet wants to exchange as well as the ID of the desired token and its amount
  • The swaps and swapRequests maps store recorded swaps and swap requests using the current counter values as keys

Contract events

The contract emits the following events


//Events that are emitted at various stages of the swap process.
event NewSwapRequest(uint256 indexed swapRequestNumber, address indexed);
event NewSwap(uint256 indexed swapNumber, address indexed, address indexed);

event SwapFinalized(
uint256 indexed swapNumber,
address indexed,
address indexed
);

Placing swap requests

Wallets can place new swap requests by calling this function:


/**
* @notice Places a new swap request. Begins searching for a match after the request is created.
* When calling this function, some tokens have to be passed in the message, otherwise it will fail.
* @param _desiredSecondTokenAmount The desired amount of tokens specified by the swap originator.
* @param _secondTokenId The ID of the token that the swap originator wants to receive.
*/
function placeSwapRequest(
uint256 _desiredSecondTokenAmount,
CurrencyId _secondTokenId
) public {
//Create a new swap request
SwapRequest memory newSwapRequest = SwapRequest({
initiator: msg.sender,
token: Nil.msgTokens()[0],
secondTokenId: _secondTokenId,
desiredSecondTokenAmount: _desiredSecondTokenAmount,
isMatched: false
});
//Update the map, the counter, and emit the event
swapRequests[swapRequestCounter] = newSwapRequest;
emit NewSwapRequest(swapRequestCounter, msg.sender);
swapRequestCounter++;

//Begin searching for a match
matchSwapRequests(newSwapRequest);
}

The function initializes a new SwapRequest and increases the swap request counter. It also starts the matching process.

Matching swap requests

The contract matches swap requests using this function:


/**
* @notice Matches a swap request with another swap request.
* On a successful match, begins the withdrawal process.
* @param swapRequest The swap request to match with another swap request.
*/
function matchSwapRequests(SwapRequest memory swapRequest) private {
//Iterate over all registered swap requests
for (uint256 i = 0; i < swapRequestCounter; i++) {
SwapRequest storage possibleCandidate = swapRequests[i];
/**
* @notice Conducts four checks:
* If a possible match stores the desired token
* If a possible match stores a sufficient amount of the desired token
* If the swap request can satisfy the amount requested by a possible match
* If a possible match has not yet been matched
*/
if (
possibleCandidate.token.id == swapRequest.secondTokenId &&
possibleCandidate.token.amount >=
swapRequest.desiredSecondTokenAmount &&
swapRequest.token.amount >=
possibleCandidate.desiredSecondTokenAmount &&
possibleCandidate.isMatched == false
) {
//Create two new Nil.Token objects
Nil.Token memory tokensPaidToSwapOriginator = Nil.Token({
id: possibleCandidate.token.id,
amount: swapRequest.desiredSecondTokenAmount
});
Nil.Token memory tokensPaidToMatchedSwapOriginator = Nil.Token({
id: swapRequest.token.id,
amount: possibleCandidate.desiredSecondTokenAmount
});

//On a successful match, begin sending the tokens
sendTokensAndProcessExcess(
swapRequest,
possibleCandidate,
tokensPaidToSwapOriginator,
tokensPaidToMatchedSwapOriginator
);

//Update the swap requests
swapRequest.isMatched = true;
possibleCandidate.isMatched = true;

//Create a new swap object, add it to the map and update the counter
Swap memory newSwap = Swap({
firstParty: swapRequest.initiator,
secondParty: possibleCandidate.initiator,
firstTokenId: swapRequest.token.id,
secondTokenId: possibleCandidate.token.id,
firstTokenExchanged: tokensPaidToSwapOriginator.amount,
secondTokenExchanged: tokensPaidToMatchedSwapOriginator
.amount
});

swaps[swapCounter] = newSwap;
emit NewSwap(
swapCounter,
swapRequest.initiator,
possibleCandidate.initiator
);
swapCounter++;
}
}
}

The function iterates over all recorded swap requests and finds a suitable partner for the new swap request. This is done based on four conditions:

  • The 'candidate' swap request must not be matched yet
  • The 'candidate' swap request must contain at least the same amount of tokens as desired in the swap request being matched
  • The 'candidate' swap request is asking for the same token as provided in the swap request being matched
  • The swap request being matched can provide at least the same amount of tokens as desired by the 'candidate' swap request

If all conditions are satisfied, the contract begins sending tokens. It also initializes a new Swap object and updates the relevant map and counter.

Sending tokens and handling excesses

The contract sends tokens and makes sure that both parties in a swap receive back any 'excess' tokens that could not be exchanged:


/**
* @notice Sends tokens to both parties participating in the swap and refunds excesses.
* @param swapRequest The swap request that has been matched.
* @param matchedSwapRequest The swap request which match the previous swap request.
* @param tokensPaidToSwapOriginator The tokens paid to the originator of the swap request.
* @param tokensPaidToMatchedSwapOriginator The tokens paid to the originator of the matched swap request.
*/
function sendTokensAndProcessExcess(
SwapRequest memory swapRequest,
SwapRequest memory matchedSwapRequest,
Nil.Token memory tokensPaidToSwapOriginator,
Nil.Token memory tokensPaidToMatchedSwapOriginator
) private {
//Calculate possible excesses
uint256 excessToSwapOriginator = swapRequest.token.amount -
matchedSwapRequest.desiredSecondTokenAmount;
uint256 excessToMatchedSwapOriginator = matchedSwapRequest
.token
.amount - swapRequest.desiredSecondTokenAmount;

//Create two arrays of Nil.Token storing the desired amounts and excesses
Nil.Token[] memory firstTokens = new Nil.Token[](2);
firstTokens[0].id = tokensPaidToSwapOriginator.id;
firstTokens[0].amount = tokensPaidToSwapOriginator.amount;
firstTokens[1].id = swapRequest.token.id;
firstTokens[1].amount = excessToSwapOriginator;
Nil.Token[] memory secondTokens = new Nil.Token[](2);
secondTokens[0].id = tokensPaidToMatchedSwapOriginator.id;
secondTokens[0].amount = tokensPaidToMatchedSwapOriginator.amount;
secondTokens[1].id = matchedSwapRequest.token.id;
secondTokens[1].amount = excessToMatchedSwapOriginator;

//Perform two async calls delivering the tokens
Nil.asyncCallWithTokens(
swapRequest.initiator,
swapRequest.initiator,
address(this),
0,
Nil.FORWARD_REMAINING,
0,
firstTokens,
""
);
Nil.asyncCallWithTokens(
matchedSwapRequest.initiator,
matchedSwapRequest.initiator,
address(this),
0,
Nil.FORWARD_REMAINING,
0,
secondTokens,
""
);
}

The function calculates the possible token excesses:

  • The difference between the amount of tokens desired by the matched swap request and the new swap request
  • The difference between the amount of tokens desired by the new swap request and the matched swap request

The tokens are transformed into token arrays and sent to the originators of swap requests using Nil.asyncCall(). All refunds are also sent to these addresses.

Full contract

Here is the full code of the swap contract:

pragma solidity ^0.8.21;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

/**
* @title SwapMatch
* @author =nil; Foundation
* @notice The contract matches swap requests and performs token swaps.
* On a successful match, the desired tokens are sent to the swap originators.
* The contract also calculates token excesses and sends them back on a successful match.
*/
contract SwapMatch is NilBase {
/**
* @notice The struct representing a successful swap.
*/
struct Swap {
address firstParty;
address secondParty;
CurrencyId firstTokenId;
CurrencyId secondTokenId;
uint256 firstTokenExchanged;
uint256 secondTokenExchanged;
}

/**
* @notice The struct representing a single swap request.
*/
struct SwapRequest {
address initiator;
Nil.Token token;
CurrencyId secondTokenId;
uint256 desiredSecondTokenAmount;
bool isMatched;
}

/**
* @notice A map of all successful swaps.
*/
mapping(uint256 => Swap) public swaps;

/**
* @notice A map of all existing swap requests.
*/
mapping(uint256 => SwapRequest) public swapRequests;

/**
* @dev Counters for swaps and swap requests.
*/
uint256 private swapCounter = 0;
uint256 private swapRequestCounter = 0;

//Events that are emitted at various stages of the swap process.
event NewSwapRequest(uint256 indexed swapRequestNumber, address indexed);
event NewSwap(uint256 indexed swapNumber, address indexed, address indexed);

event SwapFinalized(
uint256 indexed swapNumber,
address indexed,
address indexed
);

/**
* @notice Places a new swap request. Begins searching for a match after the request is created.
* When calling this function, some tokens have to be passed in the message, otherwise it will fail.
* @param _desiredSecondTokenAmount The desired amount of tokens specified by the swap originator.
* @param _secondTokenId The ID of the token that the swap originator wants to receive.
*/
function placeSwapRequest(
uint256 _desiredSecondTokenAmount,
CurrencyId _secondTokenId
) public {
//Create a new swap request
SwapRequest memory newSwapRequest = SwapRequest({
initiator: msg.sender,
token: Nil.msgTokens()[0],
secondTokenId: _secondTokenId,
desiredSecondTokenAmount: _desiredSecondTokenAmount,
isMatched: false
});
//Update the map, the counter, and emit the event
swapRequests[swapRequestCounter] = newSwapRequest;
emit NewSwapRequest(swapRequestCounter, msg.sender);
swapRequestCounter++;

//Begin searching for a match
matchSwapRequests(newSwapRequest);
}

/**
* @notice Matches a swap request with another swap request.
* On a successful match, begins the withdrawal process.
* @param swapRequest The swap request to match with another swap request.
*/
function matchSwapRequests(SwapRequest memory swapRequest) private {
//Iterate over all registered swap requests
for (uint256 i = 0; i < swapRequestCounter; i++) {
SwapRequest storage possibleCandidate = swapRequests[i];
/**
* @notice Conducts four checks:
* If a possible match stores the desired token
* If a possible match stores a sufficient amount of the desired token
* If the swap request can satisfy the amount requested by a possible match
* If a possible match has not yet been matched
*/
if (
possibleCandidate.token.id == swapRequest.secondTokenId &&
possibleCandidate.token.amount >=
swapRequest.desiredSecondTokenAmount &&
swapRequest.token.amount >=
possibleCandidate.desiredSecondTokenAmount &&
possibleCandidate.isMatched == false
) {
//Create two new Nil.Token objects
Nil.Token memory tokensPaidToSwapOriginator = Nil.Token({
id: possibleCandidate.token.id,
amount: swapRequest.desiredSecondTokenAmount
});
Nil.Token memory tokensPaidToMatchedSwapOriginator = Nil.Token({
id: swapRequest.token.id,
amount: possibleCandidate.desiredSecondTokenAmount
});

//On a successful match, begin sending the tokens
sendTokensAndProcessExcess(
swapRequest,
possibleCandidate,
tokensPaidToSwapOriginator,
tokensPaidToMatchedSwapOriginator
);

//Update the swap requests
swapRequest.isMatched = true;
possibleCandidate.isMatched = true;

//Create a new swap object, add it to the map and update the counter
Swap memory newSwap = Swap({
firstParty: swapRequest.initiator,
secondParty: possibleCandidate.initiator,
firstTokenId: swapRequest.token.id,
secondTokenId: possibleCandidate.token.id,
firstTokenExchanged: tokensPaidToSwapOriginator.amount,
secondTokenExchanged: tokensPaidToMatchedSwapOriginator
.amount
});

swaps[swapCounter] = newSwap;
emit NewSwap(
swapCounter,
swapRequest.initiator,
possibleCandidate.initiator
);
swapCounter++;
}
}
}

/**
* @notice Sends tokens to both parties participating in the swap and refunds excesses.
* @param swapRequest The swap request that has been matched.
* @param matchedSwapRequest The swap request which match the previous swap request.
* @param tokensPaidToSwapOriginator The tokens paid to the originator of the swap request.
* @param tokensPaidToMatchedSwapOriginator The tokens paid to the originator of the matched swap request.
*/
function sendTokensAndProcessExcess(
SwapRequest memory swapRequest,
SwapRequest memory matchedSwapRequest,
Nil.Token memory tokensPaidToSwapOriginator,
Nil.Token memory tokensPaidToMatchedSwapOriginator
) private {
//Calculate possible excesses
uint256 excessToSwapOriginator = swapRequest.token.amount -
matchedSwapRequest.desiredSecondTokenAmount;
uint256 excessToMatchedSwapOriginator = matchedSwapRequest
.token
.amount - swapRequest.desiredSecondTokenAmount;

//Create two arrays of Nil.Token storing the desired amounts and excesses
Nil.Token[] memory firstTokens = new Nil.Token[](2);
firstTokens[0].id = tokensPaidToSwapOriginator.id;
firstTokens[0].amount = tokensPaidToSwapOriginator.amount;
firstTokens[1].id = swapRequest.token.id;
firstTokens[1].amount = excessToSwapOriginator;
Nil.Token[] memory secondTokens = new Nil.Token[](2);
secondTokens[0].id = tokensPaidToMatchedSwapOriginator.id;
secondTokens[0].amount = tokensPaidToMatchedSwapOriginator.amount;
secondTokens[1].id = matchedSwapRequest.token.id;
secondTokens[1].amount = excessToMatchedSwapOriginator;

//Perform two async calls delivering the tokens
Nil.asyncCallWithTokens(
swapRequest.initiator,
swapRequest.initiator,
address(this),
0,
Nil.FORWARD_REMAINING,
0,
firstTokens,
""
);
Nil.asyncCallWithTokens(
matchedSwapRequest.initiator,
matchedSwapRequest.initiator,
address(this),
0,
Nil.FORWARD_REMAINING,
0,
secondTokens,
""
);
}
}

Access contract in the Playground