From f3bc64a34c8851087ff32f05b22b8edab75d248b Mon Sep 17 00:00:00 2001 From: Rabbitcoccus Date: Tue, 23 May 2023 23:48:05 +0800 Subject: [PATCH] update --- AMM_flattened.sol | 774 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 12 +- contracts/AMM.sol | 83 ++--- contracts/Token.sol | 2 +- hardhat.config.ts | 7 + scripts/deploy.ts | 35 +- test/AMM.ts | 65 ++-- 7 files changed, 884 insertions(+), 94 deletions(-) create mode 100644 AMM_flattened.sol diff --git a/AMM_flattened.sol b/AMM_flattened.sol new file mode 100644 index 0000000..95fd0df --- /dev/null +++ b/AMM_flattened.sol @@ -0,0 +1,774 @@ +// Sources flattened with hardhat v2.14.0 https://hardhat.org + +// File contracts/eips/IERC20.sol + +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + + +// File contracts/utils/Math.sol + + +// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/Math.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + enum Rounding { + Down, // Toward negative infinity + Up, // Toward infinity + Zero // Toward zero + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds up instead + * of rounding down. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b - 1) / b can overflow on addition, so we distribute. + return a == 0 ? 0 : (a - 1) / b + 1; + } + + /** + * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) + * with further edits by Uniswap Labs also under MIT license. + */ + function mulDiv( + uint256 x, + uint256 y, + uint256 denominator + ) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use + // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2^256 + prod0. + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod0 := mul(x, y) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + return prod0 / denominator; + } + + // Make sure the result is less than 2^256. Also prevents denominator == 0. + require(denominator > prod1); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1. + // See https://cs.stackexchange.com/q/138556/92363. + + // Does not overflow because the denominator cannot be zero at this stage in the function. + uint256 twos = denominator & (~denominator + 1); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such + // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv = 1 mod 2^4. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works + // in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2^8 + inverse *= 2 - denominator * inverse; // inverse mod 2^16 + inverse *= 2 - denominator * inverse; // inverse mod 2^32 + inverse *= 2 - denominator * inverse; // inverse mod 2^64 + inverse *= 2 - denominator * inverse; // inverse mod 2^128 + inverse *= 2 - denominator * inverse; // inverse mod 2^256 + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is + // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inverse; + return result; + } + } + + /** + * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv( + uint256 x, + uint256 y, + uint256 denominator, + Rounding rounding + ) internal pure returns (uint256) { + uint256 result = mulDiv(x, y, denominator); + if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) { + result += 1; + } + return result; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down. + * + * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). + */ + function sqrt(uint256 a) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. + // + // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have + // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. + // + // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` + // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` + // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` + // + // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. + uint256 result = 1 << (log2(a) >> 1); + + // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision + // into the expected uint128 result. + unchecked { + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + return min(result, a / result); + } + } + + /** + * @notice Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + (rounding == Rounding.Up && result * result < a ? 1 : 0); + } + } + + /** + * @dev Return the log in base 2, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 128; + } + if (value >> 64 > 0) { + value >>= 64; + result += 64; + } + if (value >> 32 > 0) { + value >>= 32; + result += 32; + } + if (value >> 16 > 0) { + value >>= 16; + result += 16; + } + if (value >> 8 > 0) { + value >>= 8; + result += 8; + } + if (value >> 4 > 0) { + value >>= 4; + result += 4; + } + if (value >> 2 > 0) { + value >>= 2; + result += 2; + } + if (value >> 1 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 10, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10**64) { + value /= 10**64; + result += 64; + } + if (value >= 10**32) { + value /= 10**32; + result += 32; + } + if (value >= 10**16) { + value /= 10**16; + result += 16; + } + if (value >= 10**8) { + value /= 10**8; + result += 8; + } + if (value >= 10**4) { + value /= 10**4; + result += 4; + } + if (value >= 10**2) { + value /= 10**2; + result += 2; + } + if (value >= 10**1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + (rounding == Rounding.Up && 10**result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 256, rounded down, of a positive value. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 16; + } + if (value >> 64 > 0) { + value >>= 64; + result += 8; + } + if (value >> 32 > 0) { + value >>= 32; + result += 4; + } + if (value >> 16 > 0) { + value >>= 16; + result += 2; + } + if (value >> 8 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + (rounding == Rounding.Up && 1 << (result * 8) < value ? 1 : 0); + } + } +} + + +// File contracts/utils/Context.sol + + +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + + +// File contracts/utils/Ownable.sol + + +// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _transferOwnership(_msgSender()); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require( + newOwner != address(0), + "Ownable: new owner is the zero address" + ); + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} + + +// File contracts/utils/ReentrancyGuard.sol + + +// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor() { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + _nonReentrantBefore(); + _; + _nonReentrantAfter(); + } + + function _nonReentrantBefore() private { + // On the first call to nonReentrant, _status will be _NOT_ENTERED + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + } + + function _nonReentrantAfter() private { + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} + + +// File contracts/AMM.sol + + +pragma solidity ^0.8.9; + +// Uncomment this line to use console.log +// import "hardhat/console.sol"; + + + + +contract AMM is ReentrancyGuard, Ownable { + IERC20 public immutable token0; + IERC20 public immutable token1; + + uint public reserve0; + uint public reserve1; + + uint public totalSupply; + mapping(address => uint) public balances; + + bool private initialized; + + constructor(address _token0, address _token1) Ownable() ReentrancyGuard() { + token0 = IERC20(_token0); + token1 = IERC20(_token1); + initialized = false; + } + + function initialize(uint _amount0, uint _amount1) external onlyOwner { + require(initialized == false, "already initialized"); + token0.transferFrom(_msgSender(), address(this), _amount0); + token1.transferFrom(_msgSender(), address(this), _amount1); + _mint(_msgSender(), Math.sqrt(_amount0 * _amount1)); + _update(); + initialized = true; + } + + function balanceOf(address _from) public view returns (uint256) { + return balances[_from]; + } + + function _mint(address _to, uint _amount) private { + balances[_to] += _amount; + totalSupply += _amount; + } + + function _burn(address _from, uint _amount) private { + balances[_from] -= _amount; + totalSupply -= _amount; + } + + function _update() private { + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + } + + function swap( + address _tokenIn, + uint _amountIn + ) external nonReentrant returns (uint amountOut) { + require( + _tokenIn == address(token0) || _tokenIn == address(token1), + "invalid token" + ); + require(_amountIn > 0, "amount in = 0"); + + bool isToken0 = _tokenIn == address(token0); + ( + IERC20 tokenIn, + IERC20 tokenOut, + uint reserveIn, + uint reserveOut + ) = isToken0 + ? (token0, token1, reserve0, reserve1) + : (token1, token0, reserve1, reserve0); + + tokenIn.transferFrom(_msgSender(), address(this), _amountIn); + + uint amountInWithoutFee = (_amountIn * 997) / 1000; + amountOut = + (reserveOut * amountInWithoutFee) / + (reserveIn + amountInWithoutFee); + tokenOut.transfer(_msgSender(), amountOut); + _update(); + } + + function _calculateLiquidity( + uint _amount0, + uint _amount1, + uint _reserve0, + uint _reserve1, + uint _totalSupply + ) private pure returns (uint liquidity) { + liquidity = Math.min( + (_amount0 * _totalSupply) / _reserve0, + (_amount1 * _totalSupply) / _reserve1 + ); + } + + function addLiquidity( + uint _amount0, + uint _amount1 + ) external nonReentrant returns (uint liquidity) { + require(_amount0 > 0 && _amount1 > 0, "invalid amount"); + require(reserve0 * _amount1 == reserve1 * _amount0, "x / y != dx / dy"); + + token0.transferFrom(_msgSender(), address(this), _amount0); + token1.transferFrom(_msgSender(), address(this), _amount1); + + liquidity = _calculateLiquidity( + _amount0, + _amount1, + reserve0, + reserve1, + totalSupply + ); + require(liquidity > 0, "liquidity = 0"); + _mint(_msgSender(), liquidity); + + _update(); + } + + function removeLiquidity( + uint _liquidity + ) external nonReentrant returns (uint amount0, uint amount1) { + require( + balanceOf(_msgSender()) >= _liquidity, + "insufficient liquidity" + ); + uint bal0 = token0.balanceOf(address(this)); + uint bal1 = token1.balanceOf(address(this)); + + amount0 = (_liquidity * bal0) / totalSupply; + amount1 = (_liquidity * bal1) / totalSupply; + require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0"); + + _burn(_msgSender(), _liquidity); + + token0.transfer(_msgSender(), amount0); + token1.transfer(_msgSender(), amount1); + + _update(); + } +} diff --git a/README.md b/README.md index 7cba5aa..7edcec2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # amm -# test on hardhat local +## test on hardhat local ``` npx hardhat test ``` -# deploy +## deploy +Fill in account private key and run: +``` + npx hardhat run scripts/deploy.ts --network sepolia +``` + +## sepolia + +[https://sepolia.etherscan.io/address/0x28DB425eE5E93a7838DFe18629053830fE406497#code](https://sepolia.etherscan.io/address/0x28DB425eE5E93a7838DFe18629053830fE406497#code) diff --git a/contracts/AMM.sol b/contracts/AMM.sol index 9a69f62..abd145e 100644 --- a/contracts/AMM.sol +++ b/contracts/AMM.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.9; // Uncomment this line to use console.log // import "hardhat/console.sol"; -import "./utils/IERC20.sol"; +import "./eips/IERC20.sol"; import "./utils/Math.sol"; import "./utils/Ownable.sol"; import "./utils/ReentrancyGuard.sol"; @@ -17,7 +17,7 @@ contract AMM is ReentrancyGuard, Ownable { uint public reserve1; uint public totalSupply; - mapping(address => uint) public balanceOf; + mapping(address => uint) public balances; bool private initialized; @@ -27,57 +27,26 @@ contract AMM is ReentrancyGuard, Ownable { initialized = false; } - function initialize(uint amount1, uint amount2) external onlyOwner { + function initialize(uint _amount0, uint _amount1) external onlyOwner { require(initialized == false, "already initialized"); - token0.transferFrom(_msgSender(), address(this), amount1); - token1.transferFrom(_msgSender(), address(this), amount2); - reserve0 = amount1; - reserve1 = amount2; + token0.transferFrom(_msgSender(), address(this), _amount0); + token1.transferFrom(_msgSender(), address(this), _amount1); + _mint(_msgSender(), Math.sqrt(_amount0 * _amount1)); + _update(); initialized = true; } - function deposit(address tokenIn, uint amount) external onlyOwner { - require( - tokenIn == address(token0) || tokenIn == address(token1), - "invalid token" - ); - bool isToken0 = tokenIn == address(token0); - IERC20 token = isToken0 ? token0 : token1; - require( - amount > 0 && amount <= token.balanceOf(_msgSender()), - "invalid amount" - ); - isToken0 ? reserve0 += amount : reserve1 += amount; - IERC20(token).transferFrom(_msgSender(), address(this), amount); - } - - function withdraw(address tokenIn, uint amount) external onlyOwner { - require( - tokenIn == address(token0) || tokenIn == address(token1), - "invalid token" - ); - - bool isToken0 = tokenIn == address(token0); - (IERC20 token, uint reserve) = isToken0 - ? (token0, reserve0) - : (token1, reserve1); - require(amount < reserve && amount > 0, "invalid amount"); - - isToken0 ? reserve0 -= amount : reserve1 -= amount; - IERC20(token).transfer(_msgSender(), amount); - } - - function _balanceOf(address _from) private view returns (uint256) { - return balanceOf[_from]; + function balanceOf(address _from) public view returns (uint256) { + return balances[_from]; } function _mint(address _to, uint _amount) private { - balanceOf[_to] += _amount; + balances[_to] += _amount; totalSupply += _amount; } function _burn(address _from, uint _amount) private { - balanceOf[_from] -= _amount; + balances[_from] -= _amount; totalSupply -= _amount; } @@ -116,6 +85,19 @@ contract AMM is ReentrancyGuard, Ownable { _update(); } + function _calculateLiquidity( + uint _amount0, + uint _amount1, + uint _reserve0, + uint _reserve1, + uint _totalSupply + ) private pure returns (uint liquidity) { + liquidity = Math.min( + (_amount0 * _totalSupply) / _reserve0, + (_amount1 * _totalSupply) / _reserve1 + ); + } + function addLiquidity( uint _amount0, uint _amount1 @@ -126,14 +108,13 @@ contract AMM is ReentrancyGuard, Ownable { token0.transferFrom(_msgSender(), address(this), _amount0); token1.transferFrom(_msgSender(), address(this), _amount1); - if (totalSupply == 0) { - liquidity = Math.sqrt(_amount0 * _amount1); - } else { - liquidity = Math.min( - (_amount0 * totalSupply) / reserve0, - (_amount1 * totalSupply) / reserve1 - ); - } + liquidity = _calculateLiquidity( + _amount0, + _amount1, + reserve0, + reserve1, + totalSupply + ); require(liquidity > 0, "liquidity = 0"); _mint(_msgSender(), liquidity); @@ -144,7 +125,7 @@ contract AMM is ReentrancyGuard, Ownable { uint _liquidity ) external nonReentrant returns (uint amount0, uint amount1) { require( - _balanceOf(_msgSender()) >= _liquidity, + balanceOf(_msgSender()) >= _liquidity, "insufficient liquidity" ); uint bal0 = token0.balanceOf(address(this)); diff --git a/contracts/Token.sol b/contracts/Token.sol index b04414a..2981e18 100644 --- a/contracts/Token.sol +++ b/contracts/Token.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; -import "./utils/ERC20.sol"; +import "./eips/ERC20.sol"; contract Token is ERC20 { constructor( diff --git a/hardhat.config.ts b/hardhat.config.ts index cd8df42..0f574d6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -3,6 +3,13 @@ import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { solidity: "0.8.18", + networks: { + "sepolia": { + url: "https://rpc.sepolia.org", + chainId: 0xaa36a7, + accounts: ["", ""] + } + } }; export default config; diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 3fbff00..a64d16b 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -1,16 +1,37 @@ import { ethers } from "hardhat"; async function main() { - let token0 = "", token1 = ""; - const [owner] = await ethers.getSigners(); - const AMM = await ethers.getContractFactory("AMM", owner); - let amm = await AMM.deploy(token0, token1); - console.log(`amm deploy to ${amm.address}`); + const [owner] = await ethers.getSigners(); + console.log("owner: " + owner.address); + + // prepare token0 and token1 + const TOKEN = await ethers.getContractFactory("Token", owner); + let token0 = await TOKEN.deploy("token0", "t0", ethers.utils.parseEther("1000000")); + let token1 = await TOKEN.deploy("token1", "t1", ethers.utils.parseEther("1000000")); + console.log(`token0 deploy to ${token0.address}\ntoken1 deploy to ${token1.address}`); + console.log(`owner balance: token0: ${ethers.utils.formatEther(await token0.balanceOf(owner.address))} token1: ${ethers.utils.formatEther(await token1.balanceOf(owner.address))}\n`); + + // prepare amm contract + const AMM = await ethers.getContractFactory("AMM", owner); + let amm = await AMM.deploy(token0.address, token1.address); + console.log(`amm deploy to ${amm.address}`); + + // initialize amm contract + let initialDeposit = ethers.utils.parseEther("10000"); + let tx = await token0.approve(amm.address, initialDeposit); + await tx.wait(); + tx = await token1.approve(amm.address, initialDeposit); + await tx.wait(); + console.log(ethers.utils.formatEther(await token0.allowance(owner.address, amm.address))); + console.log(ethers.utils.formatEther(await token1.allowance(owner.address, amm.address))); + tx = await amm.initialize(initialDeposit, initialDeposit); + await tx.wait(); + console.log(`amm initialized ${ethers.utils.formatEther(await amm.reserve0())} ${ethers.utils.formatEther(await amm.reserve1())}`); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch((error) => { - console.error(error); - process.exitCode = 1; + console.error(error); + process.exitCode = 1; }); diff --git a/test/AMM.ts b/test/AMM.ts index 6d9159e..38dfd43 100644 --- a/test/AMM.ts +++ b/test/AMM.ts @@ -1,13 +1,14 @@ -import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { expect } from "chai"; import { ethers } from "hardhat"; +import hre from "hardhat"; import { ContractTransaction } from "ethers"; -import { AMM, Token } from "../typechain-types/contracts"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" +import { AMM, Token } from "../typechain-types/"; describe("AMM", function () { var token0: Token, token1: Token, amm: AMM; + var owner: SignerWithAddress, user: SignerWithAddress; async function unwrap(contractInvoke: Promise) { const SUCCESS = 1; @@ -17,9 +18,15 @@ describe("AMM", function () { } before(async function () { - const [owner] = await ethers.getSigners(); + [owner, user] = await ethers.getSigners(); console.log("owner: " + owner.address); + if (hre.network.name === "sepolia") { + token0 = await ethers.getContractAt("Token", "0x4C0fCeed7769ebd1831b53Cd2F3059EC1C13e135"); + token1 = await ethers.getContractAt("Token", "0x7bb376bD0855ce3FD847C0c61e42C36044f59845"); + amm = await ethers.getContractAt("AMM", "0x28DB425eE5E93a7838DFe18629053830fE406497"); + return; + } // prepare token0 and token1 const TOKEN = await ethers.getContractFactory("Token", owner); token0 = await TOKEN.deploy("token0", "t0", ethers.utils.parseEther("1000000")); @@ -42,52 +49,44 @@ describe("AMM", function () { console.log(ethers.utils.formatEther(await token1.allowance(owner.address, amm.address))); tx = await amm.initialize(initialDeposit, initialDeposit); await tx.wait(); - console.log(`amm initialized ${await amm.reserve0()} ${await amm.reserve1()}`); + console.log(`amm initialized ${ethers.utils.formatEther(await amm.reserve0())} ${ethers.utils.formatEther(await amm.reserve1())}`); }); - it("deposit token0", async function () { - const amount = ethers.utils.parseEther("10000"); - await unwrap(token0.approve(amm.address, amount)); - await unwrap(amm.deposit(token0.address, amount)); - expect(await amm.reserve0()).to.equal(ethers.utils.parseEther("20000")); - }); - - it("deposit token1", async function () { - const amount = ethers.utils.parseEther("10000"); - await unwrap(token1.approve(amm.address, amount)); - await unwrap(amm.deposit(token1.address, amount)); - expect(await amm.reserve1()).to.equal(ethers.utils.parseEther("20000")); - }); - - it("withdraw token0", async function () { - const amount = ethers.utils.parseEther("10000"); - await unwrap(amm.withdraw(token0.address, amount)); - expect(await amm.reserve0()).to.equal(ethers.utils.parseEther("10000")); - }); - - it("withdraw token1", async function () { - const amount = ethers.utils.parseEther("10000"); - await unwrap(amm.withdraw(token1.address, amount)); - expect(await amm.reserve1()).to.equal(ethers.utils.parseEther("10000")); + it("add liquidity", async function () { + const provide = ethers.utils.parseEther("10000"); + await unwrap(token0.transfer(user.address, provide)); + await unwrap(token0.connect(user).approve(amm.address, provide)); + await unwrap(token1.transfer(user.address, provide)); + await unwrap(token1.connect(user).approve(amm.address, provide)); + console.log(`balance before add liquidity: token0=${await token0.balanceOf(user.address)} token1=${await token1.balanceOf(user.address)}`); + await unwrap(amm.connect(user).addLiquidity(provide, provide)); + expect(await amm.reserve0()).to.equal(provide.mul(2)); + expect(await amm.balanceOf(user.address)).to.equal(provide); + console.log(`balance after add liquidity: token0=${await token0.balanceOf(user.address)} token1=${await token1.balanceOf(user.address)}`); }); it("swap token0", async function () { - const [_, user] = await ethers.getSigners(); const pay = ethers.utils.parseEther("1"); await unwrap(token0.transfer(user.address, pay)); await unwrap(token0.connect(user).approve(amm.address, pay)); await unwrap(amm.connect(user).swap(token0.address, pay)); - const base = ethers.utils.parseEther("10000"); + const base = ethers.utils.parseEther("20000"); expect(await token1.balanceOf(user.address)).to.equal(base.sub(await amm.reserve1())); }); it("swap token1", async function () { - const [_, user] = await ethers.getSigners(); const pay = ethers.utils.parseEther("1"); await unwrap(token1.transfer(user.address, pay)); await unwrap(token1.connect(user).approve(amm.address, pay)); await unwrap(amm.connect(user).swap(token1.address, pay)); - const base = ethers.utils.parseEther("10001"); + const base = ethers.utils.parseEther("20001"); expect(await token0.balanceOf(user.address)).to.equal(base.sub(await amm.reserve0())); }); + + it("remove liquidity", async function () { + let liquidity = await amm.balanceOf(user.address); + console.log(`balance before remove liquidity: token0=${await token0.balanceOf(user.address)} token1=${await token1.balanceOf(user.address)}`); + await unwrap(amm.connect(user).removeLiquidity(liquidity)); + console.log(`balance after remove liquidity: token0=${await token0.balanceOf(user.address)} token1=${await token1.balanceOf(user.address)}`); + }); });