From a45ec56f84f0e53c1638524af77a67c9e3e91d0b Mon Sep 17 00:00:00 2001 From: MiloTruck Date: Wed, 4 Sep 2024 06:12:06 +0800 Subject: [PATCH] Add contracts --- script/{Counter.s.sol => Example.s.sol} | 6 +- src/Counter.sol | 14 --- src/amm/ConstantSumPair.sol | 125 ++++++++++++++++++++++++ src/amm/interfaces/ICallbacks.sol | 6 ++ src/interfaces/IERC20.sol | 22 +++++ src/tokens/ERC20.sol | 65 ++++++++++++ src/utils/FixedPointMathLib.sol | 38 +++++++ src/utils/SafeTransferLib.sol | 94 ++++++++++++++++++ src/utils/VoteHistoryLib.sol | 73 ++++++++++++++ test/Counter.t.sol | 24 ----- test/Example.t.sol | 6 ++ 11 files changed, 430 insertions(+), 43 deletions(-) rename script/{Counter.s.sol => Example.s.sol} (63%) delete mode 100644 src/Counter.sol create mode 100644 src/amm/ConstantSumPair.sol create mode 100644 src/amm/interfaces/ICallbacks.sol create mode 100644 src/interfaces/IERC20.sol create mode 100644 src/tokens/ERC20.sol create mode 100644 src/utils/FixedPointMathLib.sol create mode 100644 src/utils/SafeTransferLib.sol create mode 100644 src/utils/VoteHistoryLib.sol delete mode 100644 test/Counter.t.sol create mode 100644 test/Example.t.sol diff --git a/script/Counter.s.sol b/script/Example.s.sol similarity index 63% rename from script/Counter.s.sol rename to script/Example.s.sol index cdc1fe9..30e7e76 100644 --- a/script/Counter.s.sol +++ b/script/Example.s.sol @@ -2,18 +2,14 @@ pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; -contract CounterScript is Script { - Counter public counter; +contract ExampleScript is Script { function setUp() public {} function run() public { vm.startBroadcast(); - counter = new Counter(); - vm.stopBroadcast(); } } diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/amm/ConstantSumPair.sol b/src/amm/ConstantSumPair.sol new file mode 100644 index 0000000..cbc0c7d --- /dev/null +++ b/src/amm/ConstantSumPair.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {IERC20} from "src/interfaces/IERC20.sol"; +import {ERC20} from "src/tokens/ERC20.sol"; +import {SafeTransferLib} from "src/utils/SafeTransferLib.sol"; +import {FixedPointMathLib} from "src/utils/FixedPointMathLib.sol"; +import {ICallbacks} from "./interfaces/ICallbacks.sol"; + +/// @title ConstantSumPair +/// @notice A minimal x + y = k AMM. +contract ConstantSumPair is ERC20 { + using SafeTransferLib for IERC20; + using FixedPointMathLib for uint256; + + address public immutable owner; + + IERC20 public immutable tokenX; + IERC20 public immutable tokenY; + + uint256 public k; + + uint256 public price; + + // ======================================== CONSTRUCTOR ======================================== + + constructor() { + owner = msg.sender; + } + + // ======================================== MODIFIERS ======================================== + + /** + * @notice Enforces the x + y = k invariant + */ + modifier invariant() { + _; + + require(_computeK() >= k, "K"); + } + + // ======================================== PERMISSIONED FUNCTIONS ======================================== + + /** + * @notice Set the price for the AMM. + * + * @param _price The price of tokenY in tokenX. Has 18 decimals. + */ + function setPrice(uint256 _price) external { + require(msg.sender == owner, "OWNER"); + + price = _price; + k = _computeK(); + } + + // ======================================== MUTATIVE FUNCTIONS ======================================== + + /** + * @notice Add liquidity to the pair and mint LP tokens. + * + * @param deltaK The amount of liquidity added. + */ + function addK(uint256 deltaK) external invariant returns (uint256 shares) { + shares = k == 0 ? deltaK : deltaK.mulDivDown(totalSupply, k); + + k += deltaK; + _mint(msg.sender, shares); + } + + /** + * @notice Remove liquidity form the pair and burn LP tokens. + * + * @param amountXOut The amount of tokenX to withdraw. + * @param amountYOut The amount of tokenY to withdraw. + * @param deltaK The amount of liquidity removed. + */ + function removeK(uint256 amountXOut, uint256 amountYOut, uint256 deltaK) + external + invariant + returns (uint256 shares) + { + shares = deltaK.mulDivUp(totalSupply, k); + + k -= deltaK; + _burn(msg.sender, shares); + + tokenX.safeTransfer(msg.sender, amountXOut); + tokenY.safeTransfer(msg.sender, amountYOut); + } + + /** + * @notice Transfer tokens out from the pair. + * + * @param amountXOut The amount of tokenX to transfer out. + * @param amountYOut The amount of tokenY to transfer out. + * @param data Data passed to caller in the onTokensReceived callback. + */ + function transferTokens(uint256 amountXOut, uint256 amountYOut, bytes calldata data) external invariant { + if (amountXOut != 0) tokenX.safeTransfer(msg.sender, amountXOut); + if (amountYOut != 0) tokenY.safeTransfer(msg.sender, amountYOut); + + if (data.length != 0) { + ICallbacks(msg.sender).onTokensReceived(msg.sender, amountXOut, amountYOut, data); + } + } + + // ======================================== VIEW FUNCTIONS ======================================== + + function name() public pure override returns (string memory) { + return "ConstantSumPairLiquidity"; + } + + function symbol() public pure override returns (string memory) { + return "CSPL"; + } + + // ======================================== VIEW FUNCTIONS ======================================== + + function _computeK() internal view returns (uint256) { + uint256 reserveX = tokenX.balanceOf(address(this)); + uint256 reserveY = tokenY.balanceOf(address(this)); + + return reserveX + reserveY.divWadDown(price); + } +} diff --git a/src/amm/interfaces/ICallbacks.sol b/src/amm/interfaces/ICallbacks.sol new file mode 100644 index 0000000..598abde --- /dev/null +++ b/src/amm/interfaces/ICallbacks.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +interface ICallbacks { + function onTokensReceived(address sender, uint256 amountXOut, uint256 amountYOut, bytes calldata data) external; +} diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol new file mode 100644 index 0000000..57de7cb --- /dev/null +++ b/src/interfaces/IERC20.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +/// @title IERC20 +/// @notice Interface of the ERC-20 standard. +interface IERC20 { + event Transfer(address indexed from, address indexed to, uint256 value); + + event Approval(address indexed owner, address indexed spender, uint256 value); + + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function transfer(address to, uint256 value) external returns (bool); + + function transferFrom(address from, address to, uint256 value) external returns (bool); +} diff --git a/src/tokens/ERC20.sol b/src/tokens/ERC20.sol new file mode 100644 index 0000000..26454a8 --- /dev/null +++ b/src/tokens/ERC20.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +/// @title ERC20 +/// @notice A minimal ERC20 implementation. +abstract contract ERC20 { + event Transfer(address indexed from, address indexed to, uint256 amount); + event Approval(address indexed owner, address indexed spender, uint256 amount); + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + function name() public view virtual returns (string memory); + + function symbol() public view virtual returns (string memory); + + function decimals() public view virtual returns (uint8) { + return 18; + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) external returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom(address from, address to, uint256 amount) public returns (bool) { + allowance[from][msg.sender] -= amount; + + balanceOf[from] -= amount; + balanceOf[to] += amount; + + emit Transfer(from, to, amount); + + return true; + } + + function _mint(address to, uint256 amount) internal virtual { + balanceOf[to] += amount; + totalSupply += amount; + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + totalSupply -= amount; + + emit Transfer(from, address(0), amount); + } +} diff --git a/src/utils/FixedPointMathLib.sol b/src/utils/FixedPointMathLib.sol new file mode 100644 index 0000000..0d44fd9 --- /dev/null +++ b/src/utils/FixedPointMathLib.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +/// @title FixedPointMathLib +/// @notice Library to manage fixed-point arithmetic. +library FixedPointMathLib { + uint256 constant WAD = 1e18; + + /// @dev Returns (x * y) / WAD rounded down. + function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, y, WAD); + } + + /// @dev Returns (x * y) / WAD rounded up. + function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, y, WAD); + } + + /// @dev Returns (x * WAD) / y rounded down. + function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivDown(x, WAD, y); + } + + /// @dev Returns (x * WAD) / y rounded up. + function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) { + return mulDivUp(x, WAD, y); + } + + /// @dev Returns (x * y) / d rounded down. + function mulDivDown(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y) / d; + } + + /// @dev Returns (x * y) / d rounded up. + function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + return (x * y + (d - 1)) / d; + } +} diff --git a/src/utils/SafeTransferLib.sol b/src/utils/SafeTransferLib.sol new file mode 100644 index 0000000..0949ec0 --- /dev/null +++ b/src/utils/SafeTransferLib.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {IERC20} from "src/interfaces/IERC20.sol"; + +/// @title SafeTransferLib +/// @notice Library for safe ETH and ERC20 transfers. +library SafeTransferLib { + error ETHTransferFailed(); + error ERC20OperationFailed(); + + /** + * @dev Send `amount` of ETH and returns whether the transfer succeeded. + */ + function tryTransferETH(address to, uint256 amount) internal returns (bool success) { + assembly { + success := call(gas(), to, amount, 0, 0, 0, 0) + } + } + + /** + * @dev Send `amount` of ETH and revert if the transfer failed. + */ + function transferETH(address to, uint256 amount) internal { + bool success = tryTransferETH(to, amount); + if (!success) { + revert ETHTransferFailed(); + } + } + + /** + * @dev Forcefully send `amount` of ETH to the recipient. + */ + function forceTransferETH(address to, uint256 amount) internal { + bool success = tryTransferETH(to, amount); + + // If the transfer with CALL fails, use SELFDESTRUCT to forcefully transfer ETH. + if (!success) { + assembly { + mstore(0x00, to) // Store the address in scratch space. + mstore8(0x0b, 0x73) // Opcode `PUSH20`. + mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`. + pop(create(amount, 0x0b, 0x16)) // Return value is not checked as CREATE should never revert. + } + } + } + + /** + * @dev Send `amount` of `token`. Revert if the transfer failed. + */ + function safeTransfer(IERC20 token, address to, uint256 amount) internal { + _callOptionalReturnWithRevert(token, abi.encodeCall(token.transfer, (to, amount))); + } + + /** + * @dev Transfer `amount` of `token` from `from` to `to`. Revert if the transfer failed. + */ + function safeTransferFrom(IERC20 token, address from, address to, uint256 amount) internal { + _callOptionalReturnWithRevert(token, abi.encodeCall(token.transferFrom, (from, to, amount))); + } + + /** + * @dev Set an allowance for `token` of `amount`. Revert if the approval failed. + * This does not work when called with `amount = 0` for tokens that revert on zero approval (eg. BNB). + */ + function safeApprove(IERC20 token, address to, uint256 amount) internal { + bytes memory approveData = abi.encodeCall(token.approve, (to, amount)); + bool success = _callOptionalReturn(token, approveData); + + // If the original approval fails, call approve(to, 0) before re-trying. + // For tokens that revert on non-zero to non-zero approval (eg. USDT). + if (!success) { + _callOptionalReturnWithRevert(token, abi.encodeCall(token.approve, (to, 0))); + _callOptionalReturnWithRevert(token, approveData); + } + } + + function _callOptionalReturnWithRevert(IERC20 token, bytes memory data) internal { + bool success = _callOptionalReturn(token, data); + if (!success) { + revert ERC20OperationFailed(); + } + } + + function _callOptionalReturn(IERC20 token, bytes memory data) internal returns (bool) { + (bool success, bytes memory returndata) = address(token).call(data); + + return success && ( + returndata.length == 0 + ? address(token).code.length != 0 // if returndata is empty, token must have code + : abi.decode(returndata, (bool)) // if returndata is not empty, it must be true + ); + } +} diff --git a/src/utils/VoteHistoryLib.sol b/src/utils/VoteHistoryLib.sol new file mode 100644 index 0000000..f46c7fc --- /dev/null +++ b/src/utils/VoteHistoryLib.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +/// @title VoteHistoryLib +/// @notice Library to store and retrieve vote history based on block number. +library VoteHistoryLib { + struct Checkpoint { + uint32 blockNumber; + uint224 votes; + } + + struct History { + mapping(address => Checkpoint[]) checkpoints; + } + + /** + * @dev Pushes a (block.number, votes) checkpoint into a user's history. + */ + function push(History storage history, address user, uint224 votes) internal { + Checkpoint[] storage checkpoints = history.checkpoints[user]; + uint256 length = checkpoints.length; + + uint256 latestBlock; + if (length != 0) { + latestBlock = checkpoints[length - 1].blockNumber; + } + + if (latestBlock == block.number) { + checkpoints[length - 1].votes = votes; + } else { + checkpoints.push(Checkpoint({ + blockNumber: uint32(block.number), + votes: votes + })); + } + } + + /** + * @dev Returns votes in the last checkpoint, or zero if there is none. + */ + function getLatestVotingPower(History storage history, address user) internal view returns (uint224) { + Checkpoint[] storage checkpoints = history.checkpoints[user]; + uint256 length = checkpoints.length; + + return length == 0 ? 0 : checkpoints[length - 1].votes; + } + + /** + * @dev Returns votes in the last checkpoint with blockNumber lower or equal to + * latestBlock, or zero if there is none. + */ + function getVotingPower(History storage history, address user, uint256 latestBlock) + internal + view + returns (uint224) + { + Checkpoint[] storage checkpoints = history.checkpoints[user]; + + uint256 low = 0; + uint256 high = checkpoints.length; + + while (low < high) { + uint256 mid = (high + low) / 2; + if (checkpoints[mid].blockNumber > latestBlock) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : checkpoints[high - 1].votes; + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/Example.t.sol b/test/Example.t.sol new file mode 100644 index 0000000..35bbd4c --- /dev/null +++ b/test/Example.t.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; + +contract ExampleTest is Test {}