DSwap ERC20 Standard
Concept Overview
The Dswap ERC20 Standard by DeUnity-DePerp represents an ERC20 token integrated with an Automated Market Maker (AMM) model based on a constant product formula. This standard allows users to exchange tokens for ETH and vice versa, as well as participate in staking to earn transaction fees. Key features and functionalities of the contract include:
Key Features
-
AMM Formula
- Description: Utilizes the constant product formula
x * y = k
, wherex
andy
are the reserves of two assets (ETH and tokens), andk
is a constant that remains unchanged. - Function: Maintains the product of reserves constant during transactions, automatically adjusting prices based on the volume of traded assets.
- Description: Utilizes the constant product formula
-
Token Exchange
- Buying Tokens: Users can exchange ETH for tokens. The contract allows for setting a minimum token amount to avoid slippage.
- Selling Tokens: Users can exchange tokens for ETH. A minimum ETH amount can also be set to protect against adverse price changes.
-
Liquidity Management
- Reserves: The contract maintains ETH and token reserves, which are automatically updated after each transaction.
- Adding/Removing Liquidity: The contract does not support direct liquidity management. Liquidity is managed through exchange operations.
- Basis Reserves: Upon deployment, the contract sets the basis reserves, determining the baseline value.
-
Fees and Staking
- Fees: A 0.3% fee is charged on each transaction and sent to the staking address.
- Staking: Developers can specify a staking contract address where trading fees from the token contract will be directed.
-
Transparency and Decentralization
- Automation: The contract automatically handles purchases, sales, and fee calculations, eliminating the need for manual intervention.
- Decentralization: A fully decentralized system with no centralized control.
-
Price Evaluation and Prediction
- Token Evaluation: The
getCurrentPrice
function returns the current token price in ETH. - Trade Predictions: Functions
getEstimatedTokensForETH
andgetEstimatedETHForTokens
provide estimates for token and ETH exchanges.
- Token Evaluation: The
Technical Description and Formulas
AMM Formula:
The constant product formula used is:
x * y = k
where:
- x = ETH Reserve
- y = Token Reserve
- k = Constant (product of reserves)
Exchange Calculation:
To compute the amount of tokens or ETH received in an exchange, the following formula is used:
This formula ensures balanced reserves and adjusts the price based on transaction volume.
Fee Calculation:
A 0.3% fee is applied to each transaction:
where:
COMMISSION_FEE
= 30 (basis points)BPS
= 10000 (basis points)
The fee is deducted from the transaction amount and sent to the staking address.
Concept and Answers to Questions
How Does the AMM Work?
The AMM uses the formula x * y = k
to maintain reserve balance during transactions. This formula adjusts prices based on trade volume, ensuring the constant product of reserves.
What Problem Does It Solve?
It offers a decentralized solution for token trading and liquidity management without relying on centralized exchanges. Provides transparent, automated management of trading and fees with staking opportunities.
Who Will Use This?
- Developers and projects looking to create tokens with integrated trading and liquidity mechanisms.
- Traders and users interested in exchanging tokens or ETH in a decentralized manner.
- Speculators seeking to launch or trade innovative tokens and assets.
What Are the Benefits?
- Transparency: Automated processes and open-source code ensure transparency.
- Decentralization: Eliminates centralized control or intermediaries.
- Automation: Handles trading, fees, and liquidity management automatically.
- Security: Includes protections against common attacks and errors.
Interaction with DeUnity-DePerp:
- The DeUnity-DePerp platform provides an interface for trading and analytics supporting this standard.
- Each token can be represented on futures without needing external oracle listings.
- DeUnity-DePerp is working on extending this standard to support swaps, spot markets, initial token offerings, and cross-chain integrations.
Key Components
-
Imported Contracts and Libraries
ERC20
andERC20Burnable
from OpenZeppelin: Standard ERC20 token contract with burnable capabilities.ReentrancyGuard
from OpenZeppelin: Protection against reentrant calls.
-
State Variables
INITIAL_SUPPLY
: Initial token supply, set to total supply tokens with 18 decimal places.ethReserve
: The ETH reserve held by the contract.tokenReserve
: The token reserve held by the tokenscontract.basisValue
: Base virtual reserve value for initial valuation.BPS
: Basis points for percentage calculations (10000, i.e., 1% = 100).DEV_SUPPLY_PERCENT
: Percentage of initial token supply allocated to developers.COMMISSION_FEE
: Swap fee in basis points (0.3%).Q112
: Constant for fixed-point arithmetic (2^112).stake
: Address to receive fees.accumulatedFeesInToken
: Accumulated fees in tokens.accumulatedFeesInETH
: Accumulated fees in ETH.lastClaimTime
: Timestamp of the last fee claim.claimCooldown
: Cooldown period for fee claims (1 day).
Events
TokensPurchased
: Emitted when tokens are purchased, including the buyer, ETH amount, and token amount.TokensSold
: Emitted when tokens are sold, including the seller, token amount, and ETH amount.ReservesUpdated
: Emitted when reserves are updated.FeesWithdrawn
: Emitted when fees are withdrawn, including the recipient address and amounts in tokens and ETH.FeeAccumulated
: Emitted when fees are accumulated in tokens and ETH.
Functions
Constructor
constructor(address _stake) ERC20('DSWAP', 'DSWAP') {
require(_stake != address(0), 'Invalid stake address');
stake = _stake;
uint256 devInitialShare = (INITIAL_SUPPLY * DEV_SUPPLY_PERCENT) / BPS;
uint256 remainingSupply = INITIAL_SUPPLY - devInitialShare;
_mint(address(this), remainingSupply);
_mint(msg.sender, devInitialShare);
tokenReserve = remainingSupply;
ethReserve = basisValue;
emit ReservesUpdated(ethReserve, tokenReserve);
}
Initializes the contract, sets the fee recipient address, mints initial tokens, and updates reserves.
Swap Buy Function
/**
* @notice Buys tokens with ETH.
* @param minTokenAmount The minimum amount of tokens expected to avoid slippage.
*/
function buyTokens(uint256 minTokenAmount) external payable nonReentrant {
// Ensure that some ETH is sent with the transaction
require(msg.value > 0, 'You need to send some ETH');
uint256 ethAmount = msg.value;
// Retrieve current ETH and token reserves
(uint256 currentEthReserve, uint256 currentTokenReserve) = getReserves();
// Ensure there are tokens available in reserve for purchase
require(currentTokenReserve > 0, 'Reserve is low');
// Calculate the transaction fee as a percentage of the ETH amount
uint256 fee = (ethAmount * COMMISSION_FEE) / BPS;
// Calculate the amount of ETH remaining after the fee
uint256 amountAfterFee = ethAmount - fee;
// Determine how many tokens are equivalent to the ETH amount after fee
uint256 tokenAmount = getSwapAmount(amountAfterFee, currentEthReserve, currentTokenReserve);
// Ensure the amount of tokens is not less than the minimum expected
require(tokenAmount >= minTokenAmount, 'Slippage limit exceeded');
// Ensure there are enough tokens in the reserve to fulfill the purchase
require(tokenAmount <= currentTokenReserve, 'Not enough tokens in reserve');
// Update the reserves with the ETH amount after the fee
ethReserve += amountAfterFee;
tokenReserve -= tokenAmount;
// Transfer the calculated amount of tokens to the buyer
_transfer(address(this), msg.sender, tokenAmount);
// Accumulate the fee in ETH
accumulatedFeesInETH += fee;
// Emit an event for the token purchase
emit TokensPurchased(msg.sender, ethAmount, tokenAmount);
// Emit an event to indicate updated reserves
emit ReservesUpdated(ethReserve, tokenReserve);
// Emit an event for the accumulated fee
emit FeeAccumulated(0, fee);
}
Allows users to buy tokens with ETH, accounting for fees and slippage protection.
Swap Sell Function
function sellTokens(uint256 tokenAmount, uint256 minEthAmount) external nonReentrant {
// Ensure the token amount to sell is greater than zero
require(tokenAmount > 0, 'You need to sell some tokens');
// Ensure the sender has enough tokens to sell
require(balanceOf(msg.sender) >= tokenAmount, 'Not enough tokens');
// Retrieve the current reserves of ETH and tokens
(uint256 currentEthReserve, uint256 currentTokenReserve) = getReserves();
// Ensure the ETH reserve is above the minimum required basis value
require(currentEthReserve > basisValue, 'Reserve is below the minimum basis value');
// Calculate the amount of ETH to be returned for the specified token amount
uint256 ethAmount = getSwapAmount(tokenAmount, currentTokenReserve, currentEthReserve);
// Ensure the calculated ETH amount meets the minimum amount specified by the user
require(ethAmount >= minEthAmount, 'Slippage limit exceeded');
// Ensure the contract has enough ETH to fulfill the swap request
require(address(this).balance >= ethAmount, 'Not enough ETH in reserve');
// Calculate the commission fee in tokens
uint256 fee = (tokenAmount * COMMISSION_FEE) / BPS;
// Calculate the amount of tokens after deducting the fee
uint256 amountAfterFee = tokenAmount - fee;
// Update the ETH reserve before making transfers and burning tokens
ethReserve -= ethAmount;
// Set the current token reserve to ensure it matches the latest state
tokenReserve = currentTokenReserve;
// Transfer the calculated fee amount to the staking contract
_transfer(msg.sender, stake, fee);
// Accumulate the fee in tokens
accumulatedFeesInToken += fee;
// Burn the remaining tokens after the fee has been deducted
_burn(msg.sender, amountAfterFee);
// Transfer the ETH amount to the user
(bool success, ) = msg.sender.call{value: ethAmount}('');
// Ensure the ETH transfer was successful
require(success, 'ETH transfer failed');
// Emit an event to log the sale of tokens
emit TokensSold(msg.sender, tokenAmount, ethAmount);
// Emit an event to log the updated reserves
emit ReservesUpdated(ethReserve, tokenReserve);
// Emit an event to log the accumulated fees
emit FeeAccumulated(fee, 0);
}
Allows users to sell tokens for ETH, with fee deduction and slippage protection. In this example: tokens are burned upon sale, demonstrating the possibility of additional custom logic. After the sale, the token reserve remains as it was, while the ETH reserve decreases. Each token can choose individual parameters in the future when selecting a contract and may choose not to use this function.
require(currentEthReserve > basisValue, 'Reserve is below the minimum basis value');
In the sellTokens
function of the contract, the condition currentEthReserve > basisValue
is used to ensure the contract maintains sufficient ETH liquidity before executing a token sale.
currentEthReserve
: This refers to the current amount of ETH held by the contract. This value can fluctuate based on token purchases and sales.basisValue
: A predefined value representing the minimum required ETH reserve to maintain liquidity or avoid liquidity issues.
Purpose of the Condition
The condition currentEthReserve > basisValue
serves several purposes:
-
Liquidity Protection:
- Ensures the contract does not run out of ETH due to excessive token sales or other transactions. It helps prevent situations where the ETH reserve becomes critically low.
-
Exchange Rate Stability:
- By maintaining a minimum ETH reserve, the contract helps stabilize the token-to-ETH exchange rate, promoting fairness and stability in token exchanges.
-
Contract Safety:
- Protects the contract from potential attacks or exploits that could occur if liquidity is too low, which might manipulate the exchange rate or disrupt contract operations.
Example Application
Suppose basisValue
is set to 100 ETH. The condition currentEthReserve > basisValue
ensures that:
- Before allowing a token sale, the contract checks if the ETH reserve is greater than 100 ETH.
- If the ETH reserve is 100 ETH or less, the condition will fail, and the token sale will be rejected.
Swap Amount Calculation Function
function getSwapAmount(
uint256 inputAmount,
uint256 inputReserve,
uint256 outputReserve
) internal pure returns (uint256) {
// Ensure that reserves are non-zero to avoid division by zero
require(inputReserve > 0, 'Input reserve is zero');
require(outputReserve > 0, 'Output reserve is zero');
// Convert inputAmount to fixed-point representation to maintain precision
uint256 scaledInputAmount = inputAmount * Q112;
// Compute the numerator and denominator for the swap amount calculation
uint256 numerator = scaledInputAmount * outputReserve;
uint256 denominator = inputReserve * Q112 + scaledInputAmount;
// Perform division to get the output amount
return numerator / denominator;
}
Calculates the amount of output based on the input amount and reserves.
Price and Reserve Functions
getCurrentPrice()
: Returns the current price of the token in terms of ETH.
getReserves()
: Returns the current ETH and token reserves.
getTokenReserve()
: Returns the current token reserve.
getEthReserve()
: Returns the current ETH reserve.
getAccumulatedFeesInToken()
: Returns accumulated fees in tokens.
getAccumulatedFeesInETH()
: Returns accumulated fees in ETH.
Exchange Estimation Functions
getEstimatedTokensForETH(uint256 ethAmount)
: Estimates the amount of tokens for a given amount of ETH.
getEstimatedETHForTokens(uint256 tokenAmount)
: Estimates the amount of ETH for a given amount of tokens.
Complete example of contract
Example of code Dswap
contract Dswap
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import '@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
/**
* @title Dswap
* @notice This contract implements ERC20 token with AMM functionalities.
*/
contract Dswap is ERC20, ERC20Burnable, ReentrancyGuard {
uint256 private constant INITIAL_SUPPLY = 1000000 * 10 ** 18;
uint256 public ethReserve;
uint256 public tokenReserve;
uint256 public basisValue = 1 ether; // Base virtual reserve value for determining the initial valuation
uint256 private constant BPS = 10000; // bps
uint256 private constant DEV_SUPPLY_PERCENT = 1000; // 10%
uint256 private constant COMMISSION_FEE = 30; // Basis points (0.3%)
uint224 constant Q112 = 2 ** 112;
address public stake; // Address to receive fees
uint256 public accumulatedFeesInToken; // Accumulated fees in the token
uint256 public accumulatedFeesInETH; // Accumulated fees in ETH
uint256 public lastClaimTime;
uint256 public claimCooldown = 1 days; // claim to stake cooldown period of 1 day
event TokensPurchased(address indexed buyer, uint256 ethAmount, uint256 tokenAmount);
event TokensSold(address indexed seller, uint256 tokenAmount, uint256 ethAmount);
event ReservesUpdated(uint256 newEthReserve, uint256 newTokenReserve);
event FeesWithdrawn(address indexed recipient, uint256 tokenAmount, uint256 ethAmount);
event FeeAccumulated(uint256 tokenFeeAmount, uint256 ethFeeAmount); // Updated event for accumulated fees
/**
* @notice Constructor to initialize the token, mint initial supplies, and set the fee recipient.
*/
constructor(address _stake) ERC20('DSWAP', 'DSWAP') {
require(_stake != address(0), 'Invalid stake address');
stake = _stake;
uint256 devInitialShare = (INITIAL_SUPPLY * DEV_SUPPLY_PERCENT) / BPS;
uint256 remainingSupply = INITIAL_SUPPLY - devInitialShare;
_mint(address(this), remainingSupply);
_mint(msg.sender, devInitialShare);
tokenReserve = remainingSupply;
ethReserve = basisValue;
emit ReservesUpdated(ethReserve, tokenReserve);
}
/**
* @notice Buys tokens with ETH.
* @param minTokenAmount The minimum amount of tokens expected to avoid slippage.
*/
function buyTokens(uint256 minTokenAmount) external payable nonReentrant {
// Ensure that some ETH is sent with the transaction
require(msg.value > 0, 'You need to send some ETH');
uint256 ethAmount = msg.value;
// Retrieve current ETH and token reserves
(uint256 currentEthReserve, uint256 currentTokenReserve) = getReserves();
// Ensure there are tokens available in reserve for purchase
require(currentTokenReserve > 0, 'Reserve is low');
// Calculate the transaction fee as a percentage of the ETH amount
uint256 fee = (ethAmount * COMMISSION_FEE) / BPS;
// Calculate the amount of ETH remaining after the fee
uint256 amountAfterFee = ethAmount - fee;
// Determine how many tokens are equivalent to the ETH amount after fee
uint256 tokenAmount = getSwapAmount(amountAfterFee, currentEthReserve, currentTokenReserve);
// Ensure the amount of tokens is not less than the minimum expected
require(tokenAmount >= minTokenAmount, 'Slippage limit exceeded');
// Ensure there are enough tokens in the reserve to fulfill the purchase
require(tokenAmount <= currentTokenReserve, 'Not enough tokens in reserve');
// Update the reserves with the ETH amount after the fee
ethReserve += amountAfterFee;
tokenReserve -= tokenAmount;
// Transfer the calculated amount of tokens to the buyer
_transfer(address(this), msg.sender, tokenAmount);
// Accumulate the fee in ETH
accumulatedFeesInETH += fee;
// Emit an event for the token purchase
emit TokensPurchased(msg.sender, ethAmount, tokenAmount);
// Emit an event to indicate updated reserves
emit ReservesUpdated(ethReserve, tokenReserve);
// Emit an event for the accumulated fee
emit FeeAccumulated(0, fee);
}
/**
* @notice Sells tokens for ETH.
* @param tokenAmount The amount of tokens to sell.
* @param minEthAmount The minimum amount of ETH expected to avoid slippage.
*/
function sellTokens(uint256 tokenAmount, uint256 minEthAmount) external nonReentrant {
// Ensure the token amount to sell is greater than zero
require(tokenAmount > 0, 'You need to sell some tokens');
// Ensure the sender has enough tokens to sell
require(balanceOf(msg.sender) >= tokenAmount, 'Not enough tokens');
// Retrieve the current reserves of ETH and tokens
(uint256 currentEthReserve, uint256 currentTokenReserve) = getReserves();
// Ensure the ETH reserve is above the minimum required basis value
require(currentEthReserve > basisValue, 'Reserve is below the minimum basis value');
// Calculate the amount of ETH to be returned for the specified token amount
uint256 ethAmount = getSwapAmount(tokenAmount, currentTokenReserve, currentEthReserve);
// Ensure the calculated ETH amount meets the minimum amount specified by the user
require(ethAmount >= minEthAmount, 'Slippage limit exceeded');
// Ensure the contract has enough ETH to fulfill the swap request
require(address(this).balance >= ethAmount, 'Not enough ETH in reserve');
// Calculate the commission fee in tokens
uint256 fee = (tokenAmount * COMMISSION_FEE) / BPS;
// Calculate the amount of tokens after deducting the fee
uint256 amountAfterFee = tokenAmount - fee;
// Update the ETH reserve before making transfers and burning tokens
ethReserve -= ethAmount;
// Set the current token reserve to ensure it matches the latest state
tokenReserve = currentTokenReserve;
// Transfer the calculated fee amount to the staking contract
_transfer(msg.sender, stake, fee);
// Accumulate the fee in tokens
accumulatedFeesInToken += fee;
// Burn the remaining tokens after the fee has been deducted
_burn(msg.sender, amountAfterFee);
// Transfer the ETH amount to the user
(bool success, ) = msg.sender.call{value: ethAmount}('');
// Ensure the ETH transfer was successful
require(success, 'ETH transfer failed');
// Emit an event to log the sale of tokens
emit TokensSold(msg.sender, tokenAmount, ethAmount);
// Emit an event to log the updated reserves
emit ReservesUpdated(ethReserve, tokenReserve);
// Emit an event to log the accumulated fees
emit FeeAccumulated(fee, 0);
}
/**
* @notice Calculates the amount of output tokens/ETH for a given input amount.
* @param inputAmount The amount of input tokens/ETH.
* @param inputReserve The current reserve of the input asset.
* @param outputReserve The current reserve of the output asset.
* @return amount of output.
*/
function getSwapAmount(
uint256 inputAmount,
uint256 inputReserve,
uint256 outputReserve
) internal pure returns (uint256) {
// Ensure that reserves are non-zero to avoid division by zero
require(inputReserve > 0, 'Input reserve is zero');
require(outputReserve > 0, 'Output reserve is zero');
// Convert inputAmount to fixed-point representation to maintain precision
uint256 scaledInputAmount = inputAmount * Q112;
// Compute the numerator and denominator for the swap amount calculation
uint256 numerator = scaledInputAmount * outputReserve;
uint256 denominator = inputReserve * Q112 + scaledInputAmount;
// Perform division to get the output amount
return numerator / denominator;
}
/**
* @notice Retrieves the current price of the token in terms of ETH.
* @return scaledPrice Current price of the token in ETH, scaled by 10^18 for precision.
*/
function getCurrentPrice() public view returns (uint256) {
// Ensure that the token reserve is non-zero to avoid division by zero
require(tokenReserve > 0, 'Token reserve is zero');
// Optionally, check if ETH reserve is non-zero, although this is less critical if the token reserve is positive
require(ethReserve > 0, 'ETH reserve is zero');
// Calculate the price of the token in ETH, scaled by 10^18 for precision
uint256 price = (ethReserve * 10 ** 18) / tokenReserve;
return price;
}
/**
* @notice Retrieves the current reserves.
* @return currentEthReserve The current ETH reserve.
* @return currentTokenReserve current token reserve.
*/
function getReserves() public view returns (uint256 currentEthReserve, uint256 currentTokenReserve) {
return (ethReserve, tokenReserve);
}
/**
* @notice Retrieves the current reserves.
* @return tokenReserve current token reserve.
*/
function getTokenReserve() external view returns (uint256) {
return tokenReserve;
}
/**
* @notice Retrieves the current reserves.
* @return ethReserve The current ETH reserve.
*/
function getEthReserve() external view returns (uint256) {
return ethReserve;
}
/**
* @notice Retrieves the current accumulated fees in tokens.
* @return accumulatedFeesInToken The current accumulated fees in tokens.
*/
function getAccumulatedFeesInToken() external view returns (uint256) {
return accumulatedFeesInToken;
}
/**
* @notice Retrieves the current accumulated fees in ETH.
* @return accumulatedFeesInETH The current accumulated fees in ETH.
*/
function getAccumulatedFeesInETH() external view returns (uint256) {
return accumulatedFeesInETH;
}
/**
* @notice Estimates the amount of tokens for a given amount of ETH.
* @param ethAmount The amount of ETH.
* @return tokenAmount estimated amount of tokens.
*/
function getEstimatedTokensForETH(uint256 ethAmount) external view returns (uint256) {
require(ethAmount > 0, 'ETH amount must be greater than zero');
(uint256 currentEthReserve, uint256 currentTokenReserve) = getReserves();
uint256 tokenAmount = getSwapAmount(ethAmount, currentEthReserve, currentTokenReserve);
return tokenAmount;
}
/**
* @notice Estimates the amount of ETH for a given amount of tokens.
* @param tokenAmount The amount of tokens.
* @return ethAmount estimated amount of ETH.
*/
function getEstimatedETHForTokens(uint256 tokenAmount) external view returns (uint256) {
require(tokenAmount > 0, 'Token amount must be greater than zero');
(uint256 currentEthReserve, uint256 currentTokenReserve) = getReserves();
uint256 ethAmount = getSwapAmount(tokenAmount, currentTokenReserve, currentEthReserve);
return ethAmount;
}
/**
* @notice Claim accumulated fees to the stake address in both ETH and token.
*/
function claimFees() external nonReentrant {
require(block.timestamp >= lastClaimTime + claimCooldown, 'Cooldown period has not passed');
require(stake != address(0), 'Stake address not set');
uint256 tokenAmount = accumulatedFeesInToken;
uint256 ethAmount = accumulatedFeesInETH;
accumulatedFeesInToken = 0;
accumulatedFeesInETH = 0;
lastClaimTime = block.timestamp;
if (tokenAmount > 0) {
_transfer(address(this), stake, tokenAmount);
(bool success, bytes memory data) = stake.call(
abi.encodeWithSelector(bytes4(keccak256('updateRewardTokenReserve(uint256)')), tokenAmount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), 'Failed to update stake token reserve');
}
if (ethAmount > 0) {
(bool success, ) = payable(stake).call{value: ethAmount}('');
require(success, 'ETH transfer failed');
}
emit FeesWithdrawn(stake, tokenAmount, ethAmount);
}
}
Base Test
This test script verifies the functionality of the Dswap
contract by covering the following aspects:
1. Contract Deployment
- Verifies that the
Dswap
contract is deployed with the correct initial parameters. - Checks the total token supply and the token balances of the owner and the contract.
2. Buying Tokens with ETH
- Ensures that buying tokens with ETH works correctly, including the proper calculation of fees.
- Compares the expected amount of tokens with the actual amount received, allowing for a small slippage tolerance.
- Verifies that the ETH spent on the purchase matches the expected amount and that the fee is accumulated correctly.
3. Selling Tokens for ETH
- Ensures that selling tokens for ETH functions correctly, including the calculation of fees in tokens.
- Compares the expected amount of ETH received from the sale with the actual amount received.
- Verifies the correct accumulation of fees in tokens.
4. Fee Claim Cooldown Requirement
- Checks that fees can only be claimed after a specified cooldown period has passed.
- Verifies that an attempt to claim fees before the cooldown period results in an error.
5. Fee Accumulation
- Ensures that fees are accumulated correctly after purchasing tokens.
- Verifies that the accumulated fees are reset to zero after being claimed.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Dswap", function () {
let Dswap;
let dswap;
let owner;
let addr1;
let addr2;
let stakeAddress;
beforeEach(async function () {
Dswap = await ethers.getContractFactory("Dswap");
[owner, addr1, addr2] = await ethers.getSigners();
// Generate a random address or use a specific one
stakeAddress = ethers.Wallet.createRandom().address;
// Deploy contract with stake address as an argument
dswap = await Dswap.deploy(stakeAddress);
await dswap.deployed();
});
it("Should deploy with the correct initial supply", async function () {
const totalSupply = await dswap.totalSupply();
const ownerBalance = await dswap.balanceOf(owner.address);
const contractBalance = await dswap.balanceOf(dswap.address);
console.log("Initial Deployment Details:");
console.log("Total Supply:", ethers.utils.formatEther(totalSupply));
console.log("Owner Balance:", ethers.utils.formatEther(ownerBalance));
console.log("Contract Balance:", ethers.utils.formatEther(contractBalance));
expect(totalSupply).to.equal(ethers.utils.parseEther("1000000"));
expect(ownerBalance).to.be.above(0);
expect(contractBalance).to.equal(ethers.utils.parseEther("900000")); // Initial tokens in the contract
});
it("Should allow buying tokens with ETH and deduct the correct fee", async function () {
const buyAmount = ethers.utils.parseEther("1"); // 1 ETH
const feePercent = ethers.BigNumber.from("30"); // 0.3%
const feeScale = ethers.BigNumber.from("10000"); // Scale for fee calculation
// Calculate fee and net amount
const feeAmount = buyAmount.mul(feePercent).div(feeScale);
const netAmount = buyAmount.sub(feeAmount);
// Calculate minTokenAmount
const minTokenAmount = await dswap.getEstimatedTokensForETH(netAmount);
// Initial balances
const initialTokenBalance = await dswap.balanceOf(owner.address);
const initialEthBalance = await ethers.provider.getBalance(owner.address);
const initialStakeBalance = await ethers.provider.getBalance(stakeAddress);
// Buy tokens
await dswap.connect(owner).buyTokens(minTokenAmount, { value: buyAmount });
// Log the current price after purchase
const currentTokenPriceAfterPurchase = await dswap.getCurrentPrice();
console.log("Token Price after Purchase:", ethers.utils.formatEther(currentTokenPriceAfterPurchase));
// Final balances
const newTokenBalance = await dswap.balanceOf(owner.address);
const newEthBalance = await ethers.provider.getBalance(owner.address);
const newStakeBalance = await ethers.provider.getBalance(stakeAddress);
// Calculate differences
const tokenBalanceDifference = newTokenBalance.sub(initialTokenBalance);
const ethSpentOnBuy = initialEthBalance.sub(newEthBalance); // ETH spent (including fee)
const feeAccumulated = await dswap.getAccumulatedFeesInETH(); // Check accumulated fees
console.log("Initial ETH Balance:", ethers.utils.formatEther(initialEthBalance));
console.log("New ETH Balance:", ethers.utils.formatEther(newEthBalance));
console.log("Fee Amount:", ethers.utils.formatEther(feeAmount));
console.log("ETH Spent (should be buyAmount):", ethers.utils.formatEther(ethSpentOnBuy));
console.log("Accumulated Fee in ETH (should match feeAmount):", ethers.utils.formatEther(feeAccumulated));
console.log("New Token Balance:", ethers.utils.formatEther(newTokenBalance));
console.log("Token Balance Difference (should be around minTokenAmount):", ethers.utils.formatEther(tokenBalanceDifference));
// Assertions
expect(tokenBalanceDifference).to.be.within(
minTokenAmount.mul(95).div(100), // 5% slippage tolerance
minTokenAmount.mul(105).div(100)
); // Check token amount received
expect(ethSpentOnBuy).to.be.closeTo(buyAmount, ethers.utils.parseEther("0.0001")); // Check total ETH spent
expect(feeAccumulated).to.be.closeTo(feeAmount, ethers.utils.parseEther("0.0001")); // Check fee accumulation
});
it("Should allow selling tokens for ETH and accumulate the correct fee in tokens", async function () {
const buyAmount = ethers.utils.parseEther("1"); // 1 ETH
const feePercent = ethers.BigNumber.from("30"); // 0.3% fee
const feeScale = ethers.BigNumber.from("10000"); // Scale for fee calculation
// Calculate fee on ETH and tokens
const feeAmountETH = buyAmount.mul(feePercent).div(feeScale);
const netAmountETH = buyAmount.sub(feeAmountETH);
// Get estimated tokens for the given ETH
const minTokenAmount = await dswap.getEstimatedTokensForETH(netAmountETH);
// Buy tokens first
await dswap.connect(owner).buyTokens(minTokenAmount, { value: buyAmount });
// Calculate the amount of tokens to sell
const tokenAmountToSell = minTokenAmount.div(2); // Selling half of the bought tokens
const minEthAmount = await dswap.getEstimatedETHForTokens(tokenAmountToSell);
// Initial balances before selling
const initialTokenBalance = await dswap.balanceOf(owner.address);
const initialEthBalance = await ethers.provider.getBalance(owner.address);
const initialStakeBalance = await ethers.provider.getBalance(stakeAddress);
// Initial accumulated fee in tokens
const initialAccumulatedFeesInToken = await dswap.accumulatedFeesInToken();
// Sell tokens
await dswap.connect(owner).sellTokens(tokenAmountToSell, minEthAmount);
// Log the current price after sale
const currentTokenPriceAfterSale = await dswap.getCurrentPrice();
console.log("Token Price after Sale:", ethers.utils.formatEther(currentTokenPriceAfterSale));
// Final balances after selling
const newTokenBalance = await dswap.balanceOf(owner.address);
const newEthBalance = await ethers.provider.getBalance(owner.address);
const newStakeBalance = await ethers.provider.getBalance(stakeAddress);
// Final accumulated fee in tokens
const newAccumulatedFeesInToken = await dswap.accumulatedFeesInToken();
// Calculate expected fee in tokens
const expectedFeeInTokens = tokenAmountToSell.mul(feePercent).div(feeScale);
// Convert all balances to BigNumber for comparison
const tokenBalanceDifference = initialTokenBalance.sub(newTokenBalance);
const ethBalanceDifference = newEthBalance.sub(initialEthBalance);
console.log("Selling Tokens:");
console.log("Initial Token Balance:", ethers.utils.formatEther(initialTokenBalance));
console.log("Initial ETH Balance:", ethers.utils.formatEther(initialEthBalance));
console.log("Initial Stake Balance:", ethers.utils.formatEther(initialStakeBalance));
console.log("Post Sale Details:");
console.log("New Token Balance:", ethers.utils.formatEther(newTokenBalance));
console.log("New ETH Balance:", ethers.utils.formatEther(newEthBalance));
console.log("New Stake Balance:", ethers.utils.formatEther(newStakeBalance));
console.log("Initial Accumulated Fee in Tokens:", ethers.utils.formatEther(initialAccumulatedFeesInToken));
console.log("New Accumulated Fee in Tokens:", ethers.utils.formatEther(newAccumulatedFeesInToken));
// Assertions
expect(tokenBalanceDifference).to.equal(tokenAmountToSell); // Tokens should decrease
expect(ethBalanceDifference).to.be.within(
minEthAmount.sub(ethers.utils.parseEther("0.0001")), // Small buffer
minEthAmount.add(ethers.utils.parseEther("0.0001")) // Small buffer
); // Check ETH amount received
expect(newAccumulatedFeesInToken.sub(initialAccumulatedFeesInToken)).to.equal(expectedFeeInTokens); // Verify fee in tokens
});
it("Should allow claiming fees after cooldown period", async function () {
const buyAmount = ethers.utils.parseEther("1"); // 1 ETH
const feePercent = ethers.BigNumber.from("30"); // 0.3%
const feeScale = ethers.BigNumber.from("10000"); // Scale for fee calculation
// Calculate fee on ETH
const feeAmountETH = buyAmount.mul(feePercent).div(feeScale);
// Buy tokens first
await dswap.connect(owner).buyTokens(0, { value: buyAmount });
// Log the current price after purchase
const currentTokenPriceAfterPurchase = await dswap.getCurrentPrice();
console.log("Token Price after Purchase:", ethers.utils.formatEther(currentTokenPriceAfterPurchase));
// Advance time by cooldown period
await ethers.provider.send("evm_increaseTime", [86400]); // 1 day
await ethers.provider.send("evm_mine"); // Mine a block to update the time
// Claim fees
await expect(dswap.connect(owner).claimFees()).to.emit(dswap, "FeesWithdrawn").withArgs(stakeAddress, ethers.utils.parseEther("0"), feeAmountETH);
});
it("Should enforce claim cooldown", async function () {
// Assume an initial cooldown period of 1 day
const cooldownPeriod = await dswap.claimCooldown();
// Claim fees for the first time
await dswap.connect(owner).claimFees();
// Attempt to claim fees again before cooldown period has passed
await expect(dswap.connect(owner).claimFees()).to.be.revertedWith('Cooldown period has not passed');
// Advance time to just after the cooldown period
await network.provider.send("evm_increaseTime", [cooldownPeriod.toNumber() + 1]);
await network.provider.send("evm_mine"); // Mine a new block to apply the increased time
// Claim fees again after cooldown period
await dswap.connect(owner).claimFees();
});
it("Should accumulate fees correctly", async function () {
const buyAmount = ethers.utils.parseEther("1"); // 1 ETH
const feePercent = ethers.BigNumber.from("30"); // 0.3%
const feeScale = ethers.BigNumber.from("10000"); // Scale for fee calculation
// Calculate fee on ETH
const feeAmountETH = buyAmount.mul(feePercent).div(feeScale);
// Buy tokens first
await dswap.connect(owner).buyTokens(0, { value: buyAmount });
// Log the current price after purchase
const currentTokenPriceAfterPurchase = await dswap.getCurrentPrice();
console.log("Token Price after Purchase:", ethers.utils.formatEther(currentTokenPriceAfterPurchase));
// Check accumulated fees
const accumulatedFeesInETH = await dswap.getAccumulatedFeesInETH();
expect(accumulatedFeesInETH).to.equal(feeAmountETH);
// Claim fees
await ethers.provider.send("evm_increaseTime", [86400]); // Advance time by 1 day
await ethers.provider.send("evm_mine"); // Mine a block to update the time
await dswap.connect(owner).claimFees();
// Check fees after claiming
const accumulatedFeesInETHAfterClaim = await dswap.getAccumulatedFeesInETH();
expect(accumulatedFeesInETHAfterClaim).to.equal(0);
});
});
LOG
- Writing the concept for the DSWAP standard; ✅
- Writing the concept for the staking contract;
- Listing-routing contract;
- Advanced analytics;
- Swap interface;
- Staking interface;
- Independent audits;
- Creating/deploying tokens in the interface;
- Spot market - interface;
- Futures Listing;
- Adding Token extensions;
- OFT LayerZero
- Omni xcall