• 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.0 or higher.

  • contract DEXLending {

    • Declares the start of the DEXLending contract.

  • IERC20 public lendingToken;

    • Declares a public state variable lendingToken of type IERC20, representing the token used for lending and repayment.

  • uint256 public interestRate = 10; // Example annual interest rate

    • Declares 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 Loan struct 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 loans that maps a borrower's address to their respective Loan struct.

  • event LoanCreated(address indexed borrower, uint256 amount, uint256 interest);

    • Defines an event LoanCreated that 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 LoanRepaid that 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 CollateralClaimed that 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 DEXLending contract, which initializes the lendingToken with the address provided as _lendingToken.

  • function lend(uint256 _amount) external {

    • Declares a public function lend that allows users to create a loan by lending a specified amount of lendingToken. The function is marked external, 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 lendingToken from 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 loans mapping, storing the loan amount, interest, the current timestamp, and marking it as not yet repaid.

  • emit LoanCreated(msg.sender, _amount, interest);

    • Emits the LoanCreated event, logging the creation of the new loan.

  • function repay() external {

    • Declares a public function repay that allows users to repay their loans. The function is marked external, meaning it can be called from outside the contract.

  • Loan storage loan = loans[msg.sender];

    • Retrieves the borrower's loan information from the loans mapping and stores it in a Loan variable.

  • 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 loans mapping.

  • emit LoanRepaid(msg.sender, loan.amount, accruedInterest);

    • Emits the LoanRepaid event, logging the repayment of the loan.

  • function claimCollateral() external {

    • Declares a public function claimCollateral that allows borrowers to claim their collateral after repaying their loan. The function is marked external, meaning it can be called from outside the contract.

  • Loan storage loan = loans[msg.sender];

    • Retrieves the borrower's loan information from the loans mapping and stores it in a Loan variable.

  • 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 loans mapping, effectively resetting their loan status.

  • emit CollateralClaimed(msg.sender, loan.amount, loan.interest);

    • Emits the CollateralClaimed event, 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 IERC20 interface, which defines the standard functions for interacting with ERC-20 tokens.

  • function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    • Declares the transferFrom function 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 transfer function 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 balanceOf function from the ERC-20 standard, which returns the token balance of a specific account.

  • contract DEXSlippageControl {

    • Declares the start of the DEXSlippageControl contract, which handles token swaps with slippage control.

  • function executeTrade( address tokenIn, address tokenOut, uint256 amountIn, uint256 expectedAmountOut, uint256 maxSlippage ) external {

    • Declares the executeTrade function, which is responsible for performing the token swap while enforcing slippage limits. The function is marked external, meaning it can be called from outside the contract.

  • uint256 actualAmountOut = getActualAmountOut(tokenIn, tokenOut, amountIn);

    • Calls the getActualAmountOut function to calculate the actual amount of tokenOut the user will receive, based on the input amount of tokenIn.

  • uint256 slippage;

    • Declares a variable slippage to store the calculated slippage percentage.

  • if (actualAmountOut < expectedAmountOut) { slippage = ((expectedAmountOut - actualAmountOut) * 100) / expectedAmountOut; } else { slippage = 0; }

    • Calculates the slippage percentage only if the actualAmountOut is less than expectedAmountOut. If actualAmountOut is greater than or equal to expectedAmountOut, 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 TradeExecuted event, 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 getActualAmountOut function, which calculates the actual amount of tokenOut that the user will receive based on the input amount of tokenIn. The function is marked internal 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 tokenIn held 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 tokenOut held by the contract. This value is also needed for the AMM calculation.

  • uint256 amountInWithFee = amountIn * 997; // Assuming a 0.3% fee

    • Adjusts 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 tokenOut that the user will receive from the trade.

  • return amountOut;

    • Returns the calculated amount of tokenOut to the calling function.

  • event TradeExecuted( address indexed user, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, uint256 slippage );

    • Declares an event TradeExecuted that 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. The indexed keyword 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 DEXSecurity contract, 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 OwnershipTransferred that logs ownership changes, including the previous and new owner addresses. The indexed keyword allows for efficient filtering of these events.

  • modifier onlyOwner() { require(msg.sender == owner, "Not the owner"); _; }

    • Declares the onlyOwner modifier, 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 noReentrant modifier, which prevents reentrancy attacks. It ensures that the contract is not already in the middle of an operation by checking the locked variable. The locked variable is set to true before executing the function's logic and reset to false afterward.

  • 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 OwnershipTransferred event, 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 both onlyOwner and noReentrant modifiers. 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 to payable, 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 call method, which is a safer alternative to transfer or send. 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 changeOwner function, which allows the current owner to transfer ownership of the contract to a new owner. This function is protected by the onlyOwner modifier.

  • 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 OwnershipTransferred event, logging the change of ownership from the current owner to the new owner.

  • owner = newOwner;

    • Sets the owner variable to the new owner's address.

  • function deposit() external payable onlyOwner {

    • Declares a deposit function that allows the owner to deposit ether into the contract. The onlyOwner modifier 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 receive function, which allows the contract to accept ether directly without any function call. This is necessary for the contract to receive plain ether transfers.

The Tech BuzzYour exclusive key to unlocking the future of AI and tech investments.
Executive OffenseExecutive Offense: where offensive security meets security strategy.
tl;dr secThe best way to keep up with cybersecurity research. Join >90,000 security professionals getting the best tools, talks, and resources right in their inbox for free.

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.

Quantum Professor

Reply

or to participate.