diff --git a/.gitmodules b/.gitmodules index e69de29..5183b0c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "riff/lib/forge-std"] + path = riff/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "riff/lib/solmate"] + path = riff/lib/solmate + url = https://github.com/Rari-Capital/solmate diff --git a/README.md b/README.md index 5141240..5228730 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Each project in this repo lives in its own directory and includes a dedicated RE Below is a quick summary of each prototype currently available in this repository: -1. **`Project 1 here`** - Description here. +1. **`Riff`** + A bonding curve that you can hear. 2. **`Project 2 here`** Description here. diff --git a/riff/.gitignore b/riff/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/riff/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/riff/README.md b/riff/README.md new file mode 100644 index 0000000..6ac1e15 --- /dev/null +++ b/riff/README.md @@ -0,0 +1,10 @@ +# RIFF + +## Problem +Trading experience on every Automated Market Maker (AMM) is the same, it's time for something radically different like Riff. + +## Insight +What if we used other senses like hearing to make calls? + +## Solution +Encrypt a bonding curve to create an asset with a price that no one can see. Let an AI violin be the only party that can see the price. The violin generates music whenever it sees price fluctuations. Now, instead of seeing a price chart, users need to listen to it. \ No newline at end of file diff --git a/riff/foundry.toml b/riff/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/riff/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/riff/lib/forge-std b/riff/lib/forge-std new file mode 160000 index 0000000..3b20d60 --- /dev/null +++ b/riff/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 diff --git a/riff/lib/solmate b/riff/lib/solmate new file mode 160000 index 0000000..c93f771 --- /dev/null +++ b/riff/lib/solmate @@ -0,0 +1 @@ +Subproject commit c93f7716c9909175d45f6ef80a34a650e2d24e56 diff --git a/riff/src/Riff.sol b/riff/src/Riff.sol new file mode 100644 index 0000000..c688139 --- /dev/null +++ b/riff/src/Riff.sol @@ -0,0 +1,149 @@ +/* + * SPDX-License-Identifier: UNLICENSED + * + * AMM that hides the price of quote asset until it's above some threshold. + * + */ +pragma solidity ^0.8.13; + +import "solmate/tokens/ERC20.sol"; +import "solmate/utils/FixedPointMathLib.sol"; +import "solmate/utils/ReentrancyGuard.sol"; + +import "./ViolinCoin.sol"; + +/*////////////////////////////////////////////////////////////// +// ViolinAMM Contract +//////////////////////////////////////////////////////////////*/ + +contract Riff is ReentrancyGuard { + /*////////////////////////////////////////////////////////////// + // TOKEN STORAGE + //////////////////////////////////////////////////////////////*/ + ViolinCoin public baseAsset; + ViolinCoin public quoteAsset; + + /*////////////////////////////////////////////////////////////// + // AMM STORAGE + //////////////////////////////////////////////////////////////*/ + saddress adminAddress; + + saddress violinAddress; + + // Fixed point arithmetic unit + suint256 wad; + + // Since the reserves are encrypted, people can't access + // the price information until they swap + suint256 baseReserve; + suint256 quoteReserve; + + /*////////////////////////////////////////////////////////////// + // MODIFIERS + //////////////////////////////////////////////////////////////*/ + + /* + * Only off-chain violin can call this function + */ + modifier onlyViolinListener() { + require(saddress(msg.sender) == violinAddress, "You don't have violin access"); + _; + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor( + ViolinCoin _baseAsset, + ViolinCoin _quoteAsset, + uint256 _wad, + address _adminAddress, + address _violinAddress + ) { + baseAsset = _baseAsset; + quoteAsset = _quoteAsset; + + adminAddress = saddress(_adminAddress); + violinAddress = saddress(_violinAddress); + + // Stored as suint256 for convenience. Not actually shielded bc it's a + // transparent parameter in the constructor + wad = suint256(_wad); + } + /*////////////////////////////////////////////////////////////// + AMM LOGIC + //////////////////////////////////////////////////////////////*/ + + /* + * Add liquidity to pool. No LP rewards in this implementation. + */ + function addLiquidity(suint256 baseAmount, suint256 quoteAmount) external { + baseReserve = baseReserve + baseAmount; + quoteReserve = quoteReserve + quoteAmount; + + saddress ssender = saddress(msg.sender); + saddress sthis = saddress(address(this)); + baseAsset.transferFrom(ssender, sthis, baseAmount); + quoteAsset.transferFrom(ssender, sthis, quoteAmount); + } + + /* + * Wrapper around swap so calldata for trade looks the same regardless of + * direction. + */ + function swap(suint256 baseIn, suint256 quoteIn) public nonReentrant { + // After listening to the music, the swapper can call this function to swap the assets, + // then the price gets revealed to the swapper + + suint256 baseOut; + suint256 quoteOut; + + (baseOut, baseReserve, quoteReserve) = _swap(baseAsset, quoteAsset, baseReserve, quoteReserve, baseIn); + (quoteOut, quoteReserve, baseReserve) = _swap(quoteAsset, baseAsset, quoteReserve, baseReserve, quoteIn); + } + + /* + * Swap for cfAMM. No fees. + */ + function _swap(ViolinCoin tokenIn, ViolinCoin tokenOut, suint256 reserveIn, suint256 reserveOut, suint256 amountIn) + internal + returns (suint256 amountOut, suint256 reserveInNew, suint256 reserveOutNew) + { + suint256 numerator = mulDivDown(reserveOut, amountIn, wad); + suint256 denominator = reserveIn + amountIn; + amountOut = mulDivDown(numerator, wad, denominator); + + reserveInNew = reserveIn + amountIn; + reserveOutNew = reserveOut - amountOut; + + saddress ssender = saddress(msg.sender); + saddress sthis = saddress(address(this)); + tokenIn.transferFrom(ssender, sthis, amountIn); + tokenOut.transfer(ssender, amountOut); + } + + /* + * Returns price of quote asset. + */ + function getPrice() external view onlyViolinListener returns (uint256 price) { + return uint256(_computePrice()); + } + + /* + * Compute price of quote asset. + */ + function _computePrice() internal view returns (suint256 price) { + price = mulDivDown(baseReserve, wad, quoteReserve); + } + + /* + * For wad math. + */ + function mulDivDown(suint256 x, suint256 y, suint256 denominator) internal pure returns (suint256 z) { + require( + denominator != suint256(0) && (y == suint256(0) || x <= suint256(type(uint256).max) / y), + "Overflow or division by zero" + ); + z = (x * y) / denominator; + } +} diff --git a/riff/src/SRC20.sol b/riff/src/SRC20.sol new file mode 100644 index 0000000..8d2c77f --- /dev/null +++ b/riff/src/SRC20.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +/*////////////////////////////////////////////////////////////// +// ISRC20 Interface +//////////////////////////////////////////////////////////////*/ + +interface ISRC20 { + /*////////////////////////////////////////////////////////////// + METADATA FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + + /*////////////////////////////////////////////////////////////// + ERC20 FUNCTIONS + //////////////////////////////////////////////////////////////*/ + function balanceOf() external view returns (uint256); + function approve(saddress spender, suint256 amount) external returns (bool); + function transfer(saddress to, suint256 amount) external returns (bool); + function transferFrom(saddress from, saddress to, suint256 amount) external returns (bool); +} + +/*////////////////////////////////////////////////////////////// +// SRC20 Contract +//////////////////////////////////////////////////////////////*/ + +abstract contract SRC20 is ISRC20 { + /*////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + string public name; + string public symbol; + uint8 public immutable decimals; + + /*////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + // All storage variables that will be mutated must be confidential to + // preserve functional privacy. + suint256 internal totalSupply; + mapping(saddress => suint256) internal balance; + mapping(saddress => mapping(saddress => suint256)) internal allowance; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + } + + /*////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + function balanceOf() public view virtual returns (uint256) { + return uint256(balance[saddress(msg.sender)]); + } + + function approve(saddress spender, suint256 amount) public virtual returns (bool) { + allowance[saddress(msg.sender)][spender] = amount; + return true; + } + + function transfer(saddress to, suint256 amount) public virtual returns (bool) { + // msg.sender is public information, casting to saddress below doesn't change this + balance[saddress(msg.sender)] -= amount; + unchecked { + balance[to] += amount; + } + return true; + } + + function transferFrom(saddress from, saddress to, suint256 amount) public virtual returns (bool) { + suint256 allowed = allowance[from][saddress(msg.sender)]; // Saves gas for limited approvals. + if (allowed != suint256(type(uint256).max)) { + allowance[from][saddress(msg.sender)] = allowed - amount; + } + + balance[from] -= amount; + unchecked { + balance[to] += amount; + } + return true; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + function _mint(saddress to, suint256 amount) internal virtual { + totalSupply += amount; + unchecked { + balance[to] += amount; + } + } + + function _burn(saddress to, suint256 amount) internal virtual { + totalSupply -= amount; + balance[to] -= amount; + } +} diff --git a/riff/src/ViolinCoin.sol b/riff/src/ViolinCoin.sol new file mode 100644 index 0000000..bbd098f --- /dev/null +++ b/riff/src/ViolinCoin.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import {SRC20, ISRC20} from "./SRC20.sol"; + +/*////////////////////////////////////////////////////////////// +// IViolinCoin Interface +//////////////////////////////////////////////////////////////*/ + +// IViolinCoin extends ISRC20 by adding the mint function. +interface IViolinCoin is ISRC20 { + function mint(saddress to, suint256 amount) external; +} +/*////////////////////////////////////////////////////////////// +// ViolinCoin Contract +//////////////////////////////////////////////////////////////*/ + +contract ViolinCoin is SRC20, IViolinCoin { + address public owner; + + constructor(address _owner, string memory _name, string memory _symbol, uint8 _decimals) + SRC20(_name, _symbol, _decimals) + { + owner = _owner; + } + + modifier onlyOwner() { + require(msg.sender == owner, "Must be owner"); + _; + } + + /// @notice Mints new tokens to the specified address. + function mint(saddress to, suint256 amount) public onlyOwner { + _mint(to, amount); + } + + /// @notice Returns the balance of msg.sender. + function balanceOf() public view override(ISRC20, SRC20) returns (uint256) { + return super.balanceOf(); + } + + /// @notice Transfers tokens to another address. + function transfer(saddress to, suint256 amount) public override(ISRC20, SRC20) returns (bool) { + return super.transfer(to, amount); + } + + /// @notice Transfers tokens from one address to another. + function transferFrom(saddress from, saddress to, suint256 amount) public override(ISRC20, SRC20) returns (bool) { + return super.transferFrom(from, to, amount); + } +} diff --git a/riff/test/Riff.t.sol b/riff/test/Riff.t.sol new file mode 100644 index 0000000..af96a86 --- /dev/null +++ b/riff/test/Riff.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "solmate/tokens/ERC20.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import "../src/Riff.sol"; +import {Test, console} from "forge-std/Test.sol"; + +/*////////////////////////////////////////////////////////////// +// ViolinAMMTest Contract +//////////////////////////////////////////////////////////////*/ +contract ViolinAMMTest is Test { + /*////////////////////////////////////////////////////////////// + // AMM STORAGE + //////////////////////////////////////////////////////////////*/ + Riff public amm; + + /*////////////////////////////////////////////////////////////// + // TOKEN STORAGE + //////////////////////////////////////////////////////////////*/ + ViolinCoin baseAsset; + ViolinCoin quoteAsset; + + /*////////////////////////////////////////////////////////////// + // AMM STORAGE + //////////////////////////////////////////////////////////////*/ + address testAdmin = address(0xabcd); + + address constant violinAddress = address(0x123); + + uint256 constant WAD = 1e18; + uint8 constant WAD_ZEROS = 18; + + address constant SWAPPER1_ADDR = address(123); + address constant SWAPPER2_ADDR = address(456); + + address constant NON_LISTENER_ADDR = address(789); + + /*////////////////////////////////////////////////////////////// + // SETUP + //////////////////////////////////////////////////////////////*/ + function setUp() public { + baseAsset = new ViolinCoin(address(this), "Circle", "USDC", 18); + quoteAsset = new ViolinCoin(address(this), "Chainlink", "LINK", 18); + + // Start with pool price 1 LINK = 20 USDC + amm = new Riff(ViolinCoin(address(baseAsset)), ViolinCoin(address(quoteAsset)), WAD, testAdmin, violinAddress); + baseAsset.mint(saddress(address(this)), suint256(200000 * WAD)); + quoteAsset.mint(saddress(address(this)), suint256(10000 * WAD)); + baseAsset.approve(saddress(address(amm)), suint256(200000 * WAD)); + quoteAsset.approve(saddress(address(amm)), suint256(10000 * WAD)); + amm.addLiquidity(suint256(200000 * WAD), suint256(10000 * WAD)); + + // Two swappers start with 50k units of each, LINK and USDC + baseAsset.mint(saddress(SWAPPER1_ADDR), suint256(50000 * WAD)); + quoteAsset.mint(saddress(SWAPPER1_ADDR), suint256(50000 * WAD)); + baseAsset.mint(saddress(SWAPPER2_ADDR), suint256(50000 * WAD)); + quoteAsset.mint(saddress(SWAPPER2_ADDR), suint256(50000 * WAD)); + + // Another address that starts with 50k units of each, LINK and USDC + baseAsset.mint(saddress(NON_LISTENER_ADDR), suint256(50000 * WAD)); + quoteAsset.mint(saddress(NON_LISTENER_ADDR), suint256(50000 * WAD)); + } + + /*////////////////////////////////////////////////////////////// + // TEST CASES + //////////////////////////////////////////////////////////////*/ + + /* + * Test case for zero swap. If the user attempts to swap zero of both assets, + * then there is no change in the price. + */ + function test_ZeroSwap() public { + // Fetch the initial price as violin + vm.startPrank(violinAddress); + uint256 priceT0 = amm.getPrice(); + vm.stopPrank(); + + // Now try a zero swap of base + vm.startPrank(SWAPPER1_ADDR); + baseAsset.approve(saddress(address(amm)), suint256(50000 * WAD)); + amm.swap(suint256(0), suint256(0)); + vm.stopPrank(); + + // Another user attempts a zero swap of quote + vm.startPrank(SWAPPER2_ADDR); + quoteAsset.approve(saddress(address(amm)), suint256(50000 * WAD)); + amm.swap(suint256(0), suint256(0)); + vm.stopPrank(); + + // Finally access the price as the violin + vm.startPrank(violinAddress); + assertEq(priceT0, amm.getPrice()); + vm.stopPrank(); + } + + /* + * Test case for price going up after swap + */ + function test_PriceUp() public { + vm.startPrank(violinAddress); + uint256 priceT0 = amm.getPrice(); + vm.stopPrank(); + + vm.startPrank(SWAPPER1_ADDR); + uint256 swapperBaseT0 = baseAsset.balanceOf(); + uint256 swapperQuoteT0 = quoteAsset.balanceOf(); + + baseAsset.approve(saddress(address(amm)), suint256(30000 * WAD)); + amm.swap(suint256(30000 * WAD), suint256(0)); + + uint256 swapperBaseT1 = baseAsset.balanceOf(); + uint256 swapperQuoteT1 = quoteAsset.balanceOf(); + vm.stopPrank(); + + vm.startPrank(violinAddress); + assertLt(priceT0, amm.getPrice()); + vm.stopPrank(); + + assertGt(swapperBaseT0, swapperBaseT1); + assertLt(swapperQuoteT0, swapperQuoteT1); + } + + /* + * Test case for price going down after swap. + */ + function test_PriceNetDown() public { + vm.startPrank(violinAddress); + uint256 priceT0 = amm.getPrice(); + vm.stopPrank(); + + vm.startPrank(SWAPPER1_ADDR); + baseAsset.approve(saddress(address(amm)), suint256(5000 * WAD)); + amm.swap(suint256(5000 * WAD), suint256(0)); + vm.stopPrank(); + + vm.startPrank(SWAPPER2_ADDR); + quoteAsset.approve(saddress(address(amm)), suint256(5000 * WAD)); + amm.swap(suint256(0), suint256(5000 * WAD)); + vm.stopPrank(); + + vm.startPrank(violinAddress); + assertGt(priceT0, amm.getPrice()); + vm.stopPrank(); + } + + /* + * Test case for access control. Only the violin can call getPrice. + */ + function test_AccessControl() public { + vm.startPrank(SWAPPER1_ADDR); + vm.expectRevert("You don't have violin access"); + amm.getPrice(); + vm.stopPrank(); + + vm.startPrank(violinAddress); + amm.getPrice(); + vm.stopPrank(); + } + + /* + * Test case for swap access control. Any user can call swap + */ + function test_SwapAccessControl() public { + vm.startPrank(SWAPPER1_ADDR); + baseAsset.approve(saddress(address(amm)), suint256(5000 * WAD)); + amm.swap(suint256(5000 * WAD), suint256(0)); + vm.stopPrank(); + + vm.startPrank(SWAPPER2_ADDR); + quoteAsset.approve(saddress(address(amm)), suint256(5000 * WAD)); + amm.swap(suint256(0), suint256(5000 * WAD)); + vm.stopPrank(); + } + + /* + * Test case for liquidity invariance. If two different listeners perform + * swaps, the price should remain almost the same with some level of rounding + * error. + */ + function test_LiquidityInvariance() public { + vm.startPrank(address(this)); + uint256 baseBefore = baseAsset.balanceOf(); + uint256 quoteBefore = quoteAsset.balanceOf(); + + uint256 invariantBefore = baseBefore * quoteBefore; + vm.stopPrank(); + + // Have two different listeners perform swaps + vm.startPrank(SWAPPER1_ADDR); + baseAsset.approve(saddress(address(amm)), suint256(50000 * WAD)); + amm.swap(suint256(500 * WAD), suint256(0)); + vm.stopPrank(); + + uint256 baseAfterSwp1 = baseAsset.balanceOf(); + uint256 quoteAfterSwp1 = quoteAsset.balanceOf(); + + uint256 invariantAfterSwp1 = baseAfterSwp1 * quoteAfterSwp1; + + vm.startPrank(SWAPPER2_ADDR); + baseAsset.approve(saddress(address(amm)), suint256(20000 * WAD)); + amm.swap(suint256(200 * WAD), suint256(0)); + vm.stopPrank(); + + vm.startPrank(address(this)); + uint256 baseAfterSwp2 = baseAsset.balanceOf(); + uint256 quoteAfterSwp2 = quoteAsset.balanceOf(); + uint256 invariantAfterSwp2 = baseAfterSwp2 * quoteAfterSwp2; + vm.stopPrank(); + + // Allow a small tolerance for rounding error. + assertApproxEqRel(invariantBefore, invariantAfterSwp1, 1e16); + assertApproxEqRel(invariantBefore, invariantAfterSwp2, 1e16); + vm.stopPrank(); + } +}