- Deciphered Enigma
- Posts
- Part TWO: Comprehensive Guide to Blockchain DEX Exchange Smart Contracts: Swaps, Staking, Lending & Security
Part TWO: Comprehensive Guide to Blockchain DEX Exchange Smart Contracts: Swaps, Staking, Lending & Security

Main Functionality of DEX
In the previous article, we identified Swap and Staking as essential components of the DEX exchange. Building on that foundation, we will now delve into advanced functionalities such as Lending, Slippage Control, and Security Measures implemented through smart contracts. Let's explore further!

Lending Process
Lending in DEX Exchanges
What is Lending?
Lending in DEXs allows users to lend their tokens to others in exchange for interest. This function is crucial for maintaining liquidity in the DEX ecosystem.
Solidity Code Snippet for a Lending Contract
pragma solidity ^0.8.0;
contract DEXLending {
IERC20 public lendingToken;
uint256 public interestRate = 10; // Example annual interest rate
struct Loan {
uint256 amount;
uint256 interest;
uint256 startTime;
bool repaid;
}
mapping(address => Loan) public loans;
event LoanCreated(address indexed borrower, uint256 amount, uint256 interest);
event LoanRepaid(address indexed borrower, uint256 amount, uint256 interest);
event CollateralClaimed(address indexed borrower, uint256 amount, uint256 interest);
constructor(IERC20 _lendingToken) {
lendingToken = _lendingToken;
}
function lend(uint256 _amount) external {
require(_amount > 0, "Cannot lend 0");
require(loans[msg.sender].amount == 0, "Existing loan must be repaid first");
lendingToken.transferFrom(msg.sender, address(this), _amount);
uint256 interest = (_amount * interestRate) / 100;
loans[msg.sender] = Loan(_amount, interest, block.timestamp, false);
emit LoanCreated(msg.sender, _amount, interest);
}
function repay() external {
Loan storage loan = loans[msg.sender];
require(loan.amount > 0, "No active loan");
require(!loan.repaid, "Loan already repaid");
uint256 timeElapsed = block.timestamp - loan.startTime;
uint256 accruedInterest = (loan.amount * interestRate * timeElapsed) / (365 days * 100);
uint256 totalOwed = loan.amount + accruedInterest;
lendingToken.transferFrom(msg.sender, address(this), totalOwed);
loan.repaid = true;
emit LoanRepaid(msg.sender, loan.amount, accruedInterest);
}
function claimCollateral() external {
Loan storage loan = loans[msg.sender];
require(loan.repaid, "Loan not repaid yet");
uint256 payout = loan.amount + loan.interest;
lendingToken.transfer(msg.sender, payout);
delete loans[msg.sender];
emit CollateralClaimed(msg.sender, loan.amount, loan.interest);
}
}
Code Explanation
pragma solidity ^0.8.0;Specifies the version of Solidity that the contract is compatible with. It requires Solidity version
0.8.0or higher.
contract DEXLending {Declares the start of the
DEXLendingcontract.
IERC20 public lendingToken;Declares a public state variable
lendingTokenof typeIERC20, representing the token used for lending and repayment.
uint256 public interestRate = 10; // Example annual interest rateDeclares a public state variable
interestRate, representing the annual interest rate for loans. The rate is expressed as a percentage.
struct Loan { uint256 amount; uint256 interest; uint256 startTime; bool repaid; }Defines a
Loanstruct to store information about each loan:amount: The principal amount of the loan.interest: The interest due on the loan.startTime: The timestamp when the loan was created.repaid: A boolean indicating whether the loan has been repaid.
mapping(address => Loan) public loans;Declares a mapping
loansthat maps a borrower's address to their respectiveLoanstruct.
event LoanCreated(address indexed borrower, uint256 amount, uint256 interest);Defines an event
LoanCreatedthat logs information when a loan is created, including the borrower's address, the loan amount, and the interest.
event LoanRepaid(address indexed borrower, uint256 amount, uint256 interest);Defines an event
LoanRepaidthat logs information when a loan is repaid, including the borrower's address, the loan amount, and the accrued interest.
event CollateralClaimed(address indexed borrower, uint256 amount, uint256 interest);Defines an event
CollateralClaimedthat logs information when a borrower claims their collateral, including the borrower's address, the loan amount, and the interest.
constructor(IERC20 _lendingToken) { lendingToken = _lendingToken; }Defines the constructor for the
DEXLendingcontract, which initializes thelendingTokenwith the address provided as_lendingToken.
function lend(uint256 _amount) external {Declares a public function
lendthat allows users to create a loan by lending a specified amount oflendingToken. The function is markedexternal, meaning it can be called from outside the contract.
require(_amount > 0, "Cannot lend 0");Ensures that the lending amount is greater than 0. If not, the transaction is reverted with the message "Cannot lend 0".
require(loans[msg.sender].amount == 0, "Existing loan must be repaid first");Ensures that the user does not have an active loan. If they do, the transaction is reverted with the message "Existing loan must be repaid first".
lendingToken.transferFrom(msg.sender, address(this), _amount);Transfers the specified amount of
lendingTokenfrom the borrower's address (msg.sender) to the contract's address. This requires the borrower to have approved the contract to spend their tokens.
uint256 interest = (_amount * interestRate) / 100;Calculates the flat interest due on the loan based on the principal amount and the annual interest rate.
loans[msg.sender] = Loan(_amount, interest, block.timestamp, false);Creates a new loan entry for the borrower in the
loansmapping, storing the loan amount, interest, the current timestamp, and marking it as not yet repaid.
emit LoanCreated(msg.sender, _amount, interest);Emits the
LoanCreatedevent, logging the creation of the new loan.
function repay() external {Declares a public function
repaythat allows users to repay their loans. The function is markedexternal, meaning it can be called from outside the contract.
Loan storage loan = loans[msg.sender];Retrieves the borrower's loan information from the
loansmapping and stores it in aLoanvariable.
require(loan.amount > 0, "No active loan");Ensures that the borrower has an active loan. If not, the transaction is reverted with the message "No active loan".
require(!loan.repaid, "Loan already repaid");Ensures that the loan has not already been repaid. If it has, the transaction is reverted with the message "Loan already repaid".
uint256 timeElapsed = block.timestamp - loan.startTime;Calculates the time elapsed since the loan was created.
uint256 accruedInterest = (loan.amount * interestRate * timeElapsed) / (365 days * 100);Calculates the interest accrued over time based on the loan amount, interest rate, and time elapsed. The interest is adjusted to account for a full year (365 days).
uint256 totalOwed = loan.amount + accruedInterest;Calculates the total amount owed by the borrower, including the principal and the accrued interest.
lendingToken.transferFrom(msg.sender, address(this), totalOwed);Transfers the total owed amount from the borrower's address to the contract's address.
loan.repaid = true;Marks the loan as repaid in the
loansmapping.
emit LoanRepaid(msg.sender, loan.amount, accruedInterest);Emits the
LoanRepaidevent, logging the repayment of the loan.
function claimCollateral() external {Declares a public function
claimCollateralthat allows borrowers to claim their collateral after repaying their loan. The function is markedexternal, meaning it can be called from outside the contract.
Loan storage loan = loans[msg.sender];Retrieves the borrower's loan information from the
loansmapping and stores it in aLoanvariable.
require(loan.repaid, "Loan not repaid yet");Ensures that the loan has been repaid. If not, the transaction is reverted with the message "Loan not repaid yet".
uint256 payout = loan.amount + loan.interest;Calculates the total payout amount, which includes the principal and the accrued interest.
lendingToken.transfer(msg.sender, payout);Transfers the payout amount from the contract's address to the borrower's address.
delete loans[msg.sender];Deletes the borrower's loan entry from the
loansmapping, effectively resetting their loan status.
emit CollateralClaimed(msg.sender, loan.amount, loan.interest);Emits the
CollateralClaimedevent, logging the claiming of collateral by the borrower.

Slippage Control
Slippage Control in DEX Exchanges
What is Slippage Control?
Slippage refers to the difference between the expected price of a trade and the price at which the trade is executed. In volatile markets, slippage control mechanisms are essential to protect users from unfavorable price movements.
Solidity Code Snippet for Slippage Control
pragma solidity ^0.8.0;
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract DEXSlippageControl {
function executeTrade(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 expectedAmountOut,
uint256 maxSlippage
) external {
uint256 actualAmountOut = getActualAmountOut(tokenIn, tokenOut, amountIn);
uint256 slippage;
if (actualAmountOut < expectedAmountOut) {
slippage = ((expectedAmountOut - actualAmountOut) * 100) / expectedAmountOut;
} else {
slippage = 0; // No slippage if actualAmountOut is greater than or equal to expected
}
require(slippage <= maxSlippage, "Slippage too high");
// Transfer the input tokens to the contract
require(IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn), "Transfer of tokenIn failed");
// Transfer the output tokens to the user
require(IERC20(tokenOut).transfer(msg.sender, actualAmountOut), "Transfer of tokenOut failed");
emit TradeExecuted(msg.sender, tokenIn, tokenOut, amountIn, actualAmountOut, slippage);
}
function getActualAmountOut(
address tokenIn,
address tokenOut,
uint256 amountIn
) internal view returns (uint256) {
// Get the current reserves of tokenIn and tokenOut in the contract
uint256 reserveIn = IERC20(tokenIn).balanceOf(address(this));
uint256 reserveOut = IERC20(tokenOut).balanceOf(address(this));
// Calculate the amountOut using the constant product formula
uint256 amountInWithFee = amountIn * 997; // Assuming a 0.3% fee
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
uint256 amountOut = numerator / denominator;
return amountOut;
}
event TradeExecuted(
address indexed user,
address indexed tokenIn,
address indexed tokenOut,
uint256 amountIn,
uint256 amountOut,
uint256 slippage
);
}
Code Explanation
pragma solidity ^0.8.0;Specifies the version of Solidity that this contract is written for. This ensures compatibility with Solidity version 0.8.0 and above.
interface IERC20 {Declares the
IERC20interface, which defines the standard functions for interacting with ERC-20 tokens.
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);Declares the
transferFromfunction from the ERC-20 standard. This function allows a contract to transfer tokens from one address to another, provided that the sender has granted the contract approval to do so.
function transfer(address recipient, uint256 amount) external returns (bool);Declares the
transferfunction from the ERC-20 standard, which allows transferring tokens from the contract to a recipient.
function balanceOf(address account) external view returns (uint256);Declares the
balanceOffunction from the ERC-20 standard, which returns the token balance of a specific account.
contract DEXSlippageControl {Declares the start of the
DEXSlippageControlcontract, which handles token swaps with slippage control.
function executeTrade( address tokenIn, address tokenOut, uint256 amountIn, uint256 expectedAmountOut, uint256 maxSlippage ) external {Declares the
executeTradefunction, which is responsible for performing the token swap while enforcing slippage limits. The function is markedexternal, meaning it can be called from outside the contract.
uint256 actualAmountOut = getActualAmountOut(tokenIn, tokenOut, amountIn);Calls the
getActualAmountOutfunction to calculate the actual amount oftokenOutthe user will receive, based on the input amount oftokenIn.
uint256 slippage;Declares a variable
slippageto store the calculated slippage percentage.
if (actualAmountOut < expectedAmountOut) { slippage = ((expectedAmountOut - actualAmountOut) * 100) / expectedAmountOut; } else { slippage = 0; }Calculates the slippage percentage only if the
actualAmountOutis less thanexpectedAmountOut. IfactualAmountOutis greater than or equal toexpectedAmountOut, slippage is set to 0.
require(slippage <= maxSlippage, "Slippage too high");Ensures that the calculated slippage is within the acceptable limit (
maxSlippage). If the slippage is too high, the transaction is reverted with the message "Slippage too high".
require(IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn), "Transfer of tokenIn failed");Transfers the input tokens (
tokenIn) from the user's address (msg.sender) to the contract's address. If the transfer fails, the transaction is reverted with the message "Transfer of tokenIn failed".
require(IERC20(tokenOut).transfer(msg.sender, actualAmountOut), "Transfer of tokenOut failed");Transfers the output tokens (
tokenOut) from the contract's address to the user's address (msg.sender). If the transfer fails, the transaction is reverted with the message "Transfer of tokenOut failed".
emit TradeExecuted(msg.sender, tokenIn, tokenOut, amountIn, actualAmountOut, slippage);Emits the
TradeExecutedevent, logging the details of the trade, including the user's address, the input token, the output token, the input amount, the output amount, and the calculated slippage.
function getActualAmountOut( address tokenIn, address tokenOut, uint256 amountIn ) internal view returns (uint256) {Declares the
getActualAmountOutfunction, which calculates the actual amount oftokenOutthat the user will receive based on the input amount oftokenIn. The function is markedinternal view, meaning it can only be called within the contract and does not modify the blockchain state.
uint256 reserveIn = IERC20(tokenIn).balanceOf(address(this));Fetches the current reserve of
tokenInheld by the contract. This value is needed to calculate the output amount using the AMM formula.
uint256 reserveOut = IERC20(tokenOut).balanceOf(address(this));Fetches the current reserve of
tokenOutheld by the contract. This value is also needed for the AMM calculation.
uint256 amountInWithFee = amountIn * 997; // Assuming a 0.3% feeAdjusts the input amount (
amountIn) by applying a 0.3% fee. This simulates a common fee structure in decentralized exchanges.
uint256 numerator = amountInWithFee * reserveOut;Calculates the numerator for the AMM formula, which determines the potential output amount of
tokenOut.
uint256 denominator = reserveIn * 1000 + amountInWithFee;Calculates the denominator for the AMM formula, which, combined with the numerator, gives the final output amount of
tokenOut.
uint256 amountOut = numerator / denominator;Divides the numerator by the denominator to compute the final amount of
tokenOutthat the user will receive from the trade.
return amountOut;Returns the calculated amount of
tokenOutto the calling function.
event TradeExecuted( address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, uint256 slippage );Declares an event
TradeExecutedthat logs the execution of a trade. The event includes the user's address, the input token, the output token, the input amount, the output amount, and the calculated slippage. Theindexedkeyword allows for efficient filtering of logs based on these parameters.

Security Measures
Security in DEX Exchanges
Security Smart Contracts
Security in DEXs is paramount, given the potential for vulnerabilities in smart contracts. Security mechanisms often include time locks, ownership control, and reentrancy guards.
Solidity Code Snippet for a Security Contract
pragma solidity ^0.8.0;
contract DEXSecurity {
address public owner;
bool private locked;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
modifier noReentrant() {
require(!locked, "Reentrant call detected");
locked = true;
_;
locked = false;
}
constructor() {
owner = msg.sender;
emit OwnershipTransferred(address(0), owner);
}
function secureFunction() external onlyOwner noReentrant {
// Assume this function handles sensitive operations like fund transfers.
address payable recipient = payable(msg.sender);
uint256 amount = address(this).balance;
require(amount > 0, "Insufficient funds");
// Secure transfer logic with fallback mechanism
(bool success, ) = recipient.call{value: amount}("");
require(success, "Transfer failed");
}
function changeOwner(address newOwner) external onlyOwner {
require(newOwner != address(0), "Invalid address");
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
// Function to deposit ether into the contract
function deposit() external payable onlyOwner {
require(msg.value > 0, "No ether sent");
}
// Fallback function to accept ether
receive() external payable {}
}
Code Explanation
pragma solidity ^0.8.0;Specifies the version of Solidity that this contract is written for. This ensures compatibility with Solidity version 0.8.0 and above.
contract DEXSecurity {Declares the start of the
DEXSecuritycontract, which is designed to include security mechanisms such as ownership control and protection against reentrancy attacks.
address public owner;Declares a public state variable
owner, which stores the address of the contract's owner.
bool private locked;Declares a private state variable
locked, used to prevent reentrancy attacks by indicating whether the contract is currently in the middle of a critical operation.
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);Declares an event
OwnershipTransferredthat logs ownership changes, including the previous and new owner addresses. Theindexedkeyword allows for efficient filtering of these events.
modifier onlyOwner() { require(msg.sender == owner, "Not the owner"); _; }Declares the
onlyOwnermodifier, which restricts access to certain functions to only the contract owner. It checks if the caller (msg.sender) is the owner; if not, it reverts with the message "Not the owner".
modifier noReentrant() { require(!locked, "Reentrant call detected"); locked = true; _; locked = false; }Declares the
noReentrantmodifier, which prevents reentrancy attacks. It ensures that the contract is not already in the middle of an operation by checking thelockedvariable. Thelockedvariable is set totruebefore executing the function's logic and reset tofalseafterward.
constructor() { owner = msg.sender; emit OwnershipTransferred(address(0), owner); }Declares the constructor, which is executed once when the contract is deployed. It sets the deployer of the contract as the initial owner and emits the
OwnershipTransferredevent, logging the transfer of ownership from the zero address (indicating no previous owner) to the deployer.
function secureFunction() external onlyOwner noReentrant {Declares the
secureFunction, which is protected by bothonlyOwnerandnoReentrantmodifiers. This function can only be called by the owner and is protected against reentrancy attacks. The logic within this function should handle critical operations securely.
address payable recipient = payable(msg.sender);Retrieves the caller’s address (
msg.sender) and casts it topayable, allowing ether transfers to this address.
uint256 amount = address(this).balance;Retrieves the current ether balance of the contract.
require(amount > 0, "Insufficient funds");Ensures that the contract has a non-zero balance before proceeding with the transfer.
(bool success, ) = recipient.call{value: amount}("");Transfers the ether balance to the recipient using the
callmethod, which is a safer alternative totransferorsend. It allows specifying a gas limit and handling errors more effectively.
require(success, "Transfer failed");Ensures that the ether transfer was successful. If not, the transaction is reverted with the message "Transfer failed".
function changeOwner(address newOwner) external onlyOwner {Declares the
changeOwnerfunction, which allows the current owner to transfer ownership of the contract to a new owner. This function is protected by theonlyOwnermodifier.
require(newOwner != address(0), "Invalid address");Ensures that the new owner's address is valid (i.e., not the zero address). If the new owner’s address is invalid, the transaction is reverted with the message "Invalid address".
emit OwnershipTransferred(owner, newOwner);Emits the
OwnershipTransferredevent, logging the change of ownership from the current owner to the new owner.
owner = newOwner;Sets the
ownervariable to the new owner's address.
function deposit() external payable onlyOwner {Declares a
depositfunction that allows the owner to deposit ether into the contract. TheonlyOwnermodifier ensures that only the owner can call this function.
require(msg.value > 0, "No ether sent");Ensures that ether is sent along with the transaction. If no ether is sent, the transaction is reverted with the message "No ether sent".
receive() external payable {}Declares a
receivefunction, which allows the contract to accept ether directly without any function call. This is necessary for the contract to receive plain ether transfers.
|
|
|
The core functionalities of a DEX exchange—swaps, staking, lending, slippage control, and security—are all powered by smart contracts. These contracts automate the processes, ensuring that transactions are executed efficiently, securely, and transparently. By understanding the Solidity code behind these contracts, developers can build more robust and secure DEX platforms, contributing to the growing ecosystem of decentralized finance.



Reply