From e4d96e3e3169683c32b3d829b4c83b9f5637d00e Mon Sep 17 00:00:00 2001 From: topanisto Date: Fri, 7 Feb 2025 18:43:31 -0500 Subject: [PATCH 01/12] initial commit --- {walnut => level}/.gitignore | 0 {walnut => level}/foundry.toml | 0 level/src/PermissionedAMM.sol | 197 +++++++++++++++++++++++++++++++ level/src/SRC20.sol | 103 ++++++++++++++++ level/src/WDGSRC20.sol | 178 ++++++++++++++++++++++++++++ level/src/WidgetInterface.sol | 138 ++++++++++++++++++++++ level/test/AMM.t.sol | 102 ++++++++++++++++ level/test/WidgetInterface.t.sol | 127 ++++++++++++++++++++ level/test/utils/MockSrc20.sol | 21 ++++ walnut/lib/forge-std | 1 - walnut/script/Walnut.s.sol | 17 --- walnut/src/Walnut.sol | 87 -------------- walnut/test/Walnut.t.sol | 135 --------------------- 13 files changed, 866 insertions(+), 240 deletions(-) rename {walnut => level}/.gitignore (100%) rename {walnut => level}/foundry.toml (100%) create mode 100644 level/src/PermissionedAMM.sol create mode 100644 level/src/SRC20.sol create mode 100644 level/src/WDGSRC20.sol create mode 100644 level/src/WidgetInterface.sol create mode 100644 level/test/AMM.t.sol create mode 100644 level/test/WidgetInterface.t.sol create mode 100644 level/test/utils/MockSrc20.sol delete mode 160000 walnut/lib/forge-std delete mode 100644 walnut/script/Walnut.s.sol delete mode 100644 walnut/src/Walnut.sol delete mode 100644 walnut/test/Walnut.t.sol diff --git a/walnut/.gitignore b/level/.gitignore similarity index 100% rename from walnut/.gitignore rename to level/.gitignore diff --git a/walnut/foundry.toml b/level/foundry.toml similarity index 100% rename from walnut/foundry.toml rename to level/foundry.toml diff --git a/level/src/PermissionedAMM.sol b/level/src/PermissionedAMM.sol new file mode 100644 index 0000000..ab09470 --- /dev/null +++ b/level/src/PermissionedAMM.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT License +pragma solidity ^0.8.13; + +import {ISRC20} from "./SRC20.sol"; +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; + +/// @title PermissionedAMM - Restricted Access Automated Market Maker +/// @notice A constant product AMM (x*y=k) that manages the liquidity pool for the DePIN price floor protocol +/// @dev Uses shielded data types (suint256) for privacy-preserving calculations +/// @dev All operations are restricted to the owner to so that even properties relevent to the caller, +// e.g. price, can't be observed until the owner wishes to reveal them +contract PermissionedAMM is Ownable(msg.sender) { + ISRC20 public token0; + ISRC20 public token1; + + suint256 reserve0; + suint256 reserve1; + + suint256 totalSupply; + mapping(address => suint256) balanceOf; + + /// @notice Initializes the AMM with token pair addresses + /// @param _token0 Address of the first token (typically WDG) + /// @param _token1 Address of the second token (typically USDC) + constructor(address _token0, address _token1) { + token0 = ISRC20(_token0); + token1 = ISRC20(_token1); + } + + function _mint(address _to, suint256 _amount) private { + balanceOf[_to] += _amount; + totalSupply += _amount; + } + + function _burn(address _from, suint256 _amount) private { + balanceOf[_from] -= _amount; + totalSupply -= _amount; + } + + function _update(suint256 _reserve0, suint256 _reserve1) internal { + reserve0 = _reserve0; + reserve1 = _reserve1; + } + + /// @notice Calculates amount of token0 received after a liquidation + /// @dev Uses constant product formula to determine price impact + /// @param _amount0 Amount of token0 to be liquidated + /// @return Average price of token0 denominated in token1 + function calcSwapOutput(suint256 _amount0) external view onlyOwner returns (uint256) { + /* XY = K + (X+dX)(Y-dY) = K + dY = Y - K / (X+dX) + dY = YdX/(X+dX) = Y(1-X/(X+dX)) + + Return dY = dX * Y / (X+dX) + */ + + return uint256(reserve1 / (reserve0 / _amount0 + suint(1))); + } + + /// @notice Calculates required input amount for desired output + /// @dev Reverse calculation of constant product formula + /// @param _tokenOut Address of token to receive + /// @param _amount Desired output amount + /// @return amountIn of input token required to achieve desired output + function calcSwapInput(address _tokenOut, suint256 _amount) public view onlyOwner returns (uint256) { + /* XY = K + (X+dX)(Y-dY) = K + dY = Y - K / (X+dX) + dY = YdX/(X+dX) = Y(1-X/(X+dX)) + + Return dX = X dY / (Y-dY) + */ + suint256 amountIn = _tokenOut == address(token1) + ? reserve0 / (reserve1 / _amount - suint(1)) + : reserve1 / (reserve0 / _amount - suint(1)); + + return uint256(amountIn); + } + + /// @notice Adds liquidity to the AMM pool + /// @dev Maintains price ratio for existing pools + /// @param _amount0 Amount of token0 to add + /// @param _amount1 Amount of token1 to add + /// @param originalSender Address to receive LP tokens + function addLiquidity(suint256 _amount0, suint256 _amount1, address originalSender) external onlyOwner { + token0.transferFrom(saddress(msg.sender), saddress(this), _amount0); + token1.transferFrom(saddress(msg.sender), saddress(this), _amount1); + + if (reserve0 > suint256(0) || reserve1 > suint256(0)) { + require( + reserve0 * _amount1 == reserve1 * _amount0, //preserving price + "x / y != dx / dy" + ); + } + // if i wanted to put usdc into the pool, first swap until the ratios + + suint256 shares = totalSupply == suint(0) + ? _sqrt(_amount0 * _amount1) + : _min((_amount0 * totalSupply) / reserve0, (_amount1 * totalSupply) / reserve1); + + require(shares > suint256(0), "No shares to mint"); + _mint(originalSender, shares); + + // recalculate k + _update(suint256(token0.balanceOf()), suint256(token1.balanceOf())); + } + + /// @notice Removes liquidity from the AMM pool + /// @dev Burns LP tokens and returns underlying assets + /// @param _shares Amount of LP tokens to burn + /// @param originalSender Address that owns the LP tokens + function removeLiquidity(suint256 _shares, address originalSender) external onlyOwner { + require(balanceOf[originalSender] > _shares, "Insufficient shares"); + suint256 amount0 = (_shares * reserve0) / totalSupply; + suint256 amount1 = (_shares * reserve1) / totalSupply; + require(amount0 > suint256(0) && amount1 > suint256(0), "amount0 or amount1 = 0"); + + _burn(originalSender, _shares); // burn LP shares + _update(reserve0 - amount0, reserve1 - amount1); + + token0.transfer(saddress(msg.sender), amount0); + token1.transfer(saddress(msg.sender), suint(amount1)); + } + + /// @notice Executes token swap using constant product formula + /// @dev Updates reserves after swap completion + /// @param _tokenIn Address of input token + /// @param _amountIn Amount of input token + function swap(saddress _tokenIn, suint256 _amountIn) external onlyOwner { + require(_amountIn > suint(0), "Invalid amount to swap"); + require(_tokenIn == saddress(token0) || _tokenIn == saddress(token1), "Invalid token"); + + bool isToken0 = _tokenIn == saddress(token0); + + (ISRC20 tokenIn, ISRC20 tokenOut, suint256 reserveIn, suint256 reserveOut) = + isToken0 ? (token0, token1, reserve0, reserve1) : (token1, token0, reserve1, reserve0); + + tokenIn.transferFrom(saddress(msg.sender), saddress(this), _amountIn); + + suint256 amountOut = reserveOut * _amountIn / (reserveIn + _amountIn); // still shielded + + tokenOut.approve(saddress(this), amountOut); + tokenOut.transferFrom(saddress(this), saddress(msg.sender), amountOut); + + _update(suint256(token0.balanceOf()), suint256(token1.balanceOf())); + } + + /// @notice Executes token swap by taking token out of the pool. + /// This is ONLY called within operatorWithdraw, where the owed balance + /// is first transferred to the AMM. + /// @dev Updates reserves after swap completion. + function swapOut(saddress _tokenOut, suint256 _amountOut) external onlyOwner { + require(_tokenOut == saddress(token0) || _tokenOut == saddress(token1), "Invalid token"); + + bool isToken0 = _tokenOut == saddress(token0); + + (ISRC20 tokenOwed, ISRC20 tokenRm, suint256 reserveRm) = + isToken0 ? (token1, token0, reserve0) : (token0, token1, reserve1); + + require(_amountOut <= reserveRm, "Invalid amount to extract."); + + suint256 amountOwed = suint256(calcSwapInput(address(tokenRm), _amountOut)); + + tokenOwed.transferFrom(saddress(msg.sender), saddress(this), amountOwed); + + tokenRm.approve(saddress(this), _amountOut); + tokenRm.transferFrom(saddress(this), saddress(msg.sender), _amountOut); + + _update(suint256(token0.balanceOf()), suint256(token1.balanceOf())); + } + + /// @notice Calculates square root using binary search + /// @dev Used for initial LP token minting + /// @param y Value to find square root of + /// @return z Square root of input value + function _sqrt(suint256 y) private pure returns (suint256 z) { + if (y < suint256(3)) { + z = y; + suint256 x = y / suint256(2) + suint256(1); + while (x < z) { + z = x; + x = (y / x + x) / suint256(2); + } + } else if (y != suint256(0)) { + z = suint256(1); + } + } + + /// @notice Returns minimum of two values + /// @param x First value + /// @param y Second value + /// @return Smaller of the two inputs + function _min(suint256 x, suint256 y) private pure returns (suint256) { + return x <= y ? x : y; + } +} diff --git a/level/src/SRC20.sol b/level/src/SRC20.sol new file mode 100644 index 0000000..8d2c77f --- /dev/null +++ b/level/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/level/src/WDGSRC20.sol b/level/src/WDGSRC20.sol new file mode 100644 index 0000000..3b8b2a5 --- /dev/null +++ b/level/src/WDGSRC20.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import {ISRC20} from "./SRC20.sol"; + +/*////////////////////////////////////////////////////////////// +// WDGSRC20 Contract +//////////////////////////////////////////////////////////////*/ + +/// @title WDGSRC20 - Privacy-Preserving Restricted Transfer Token +/// @notice An ERC20-like token implementation that uses shielded data types and restricts transfers +/// @dev Implements transfer restrictions and uses `saddress` and `suint256` types for privacy +/// @dev Transfers are only allowed by trusted contracts or after a time-based unlock +abstract contract WDGSRC20 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; + + /// @notice Duration in blocks before public transfers are enabled + /// @dev After this block height, transfers become permissionless + suint256 transferUnlockTime; + uint256 public constant BLOCKS_PER_EPOCH = 7200; // about a day + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + } + + /*////////////////////////////////////////////////////////////// + SRC20 LOGIC + Includes Transfer Restrictions + //////////////////////////////////////////////////////////////*/ + + /// @notice Retrieves the caller's token balance + /// @dev Only callable by whitelisted addresses or after unlock time + /// @return Current balance of the caller + function balanceOf() public view virtual whitelisted returns (uint256) { + return uint256(balance[saddress(msg.sender)]); + } + + function trustedBalanceOf(saddress account) public view virtual returns (uint256) { + require(isTrusted(), "Only trusted addresses can call this function"); + return uint256(balance[account]); + } + + /// @notice Approves another address to spend tokens + /// @param spender Address to approve + /// @param amount Amount of tokens to approve + /// @return success Always returns true + function approve(saddress spender, suint256 amount) public virtual returns (bool) { + allowance[saddress(msg.sender)][spender] = amount; + return true; + } + + /// @notice Transfers tokens to another address + /// @dev Only callable by whitelisted addresses or after unlock time + /// @param to Recipient address + /// @param amount Amount to transfer + /// @return success Always returns true + function transfer(saddress to, suint256 amount) public virtual whitelisted 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; + } + + /// @notice Transfers tokens on behalf of another address + /// @dev Only callable by whitelisted addresses or after unlock time + /// @dev Trusted contracts can transfer unlimited amounts without approval + /// @param from Source address + /// @param to Destination address + /// @param amount Amount to transfer + /// @return success Always returns true + function transferFrom(saddress from, saddress to, suint256 amount) public virtual whitelisted returns (bool) { + suint256 allowed = allowance[from][saddress(msg.sender)]; // Saves gas for limited approvals. + if (isTrusted()) { + allowed = suint256(type(uint256).max); + } + + if (allowed != suint256(type(uint256).max)) { + allowance[from][saddress(msg.sender)] = allowed - amount; + } + + balance[from] -= amount; + unchecked { + balance[to] += amount; + } + return true; + } + + /// @notice Creates new tokens + /// @dev Only callable by trusted contracts + /// @param to Recipient of the minted tokens + /// @param amount Amount to mint + function mint(saddress to, suint256 amount) public virtual { + require(isTrusted()); + totalSupply += amount; + unchecked { + balance[to] += amount; + } + } + + /// @notice Destroys tokens + /// @dev Only callable by trusted contracts + /// @param from Address to burn from + /// @param amount Amount to burn + function burn(saddress from, suint256 amount) public virtual { + require(isTrusted(), "Not authorized to burn"); + require(suint256(balanceOf()) >= amount, "Insufficient balance to burn"); + totalSupply -= amount; + balance[from] -= amount; + } + + /*////////////////////////////////////////////////////////////// + Trusted Address Logic + //////////////////////////////////////////////////////////////*/ + + address public trustedDePinServiceAddress; + address public trustedAMMAddress; + + function getTrustedDepinServiceAddress() public view returns (address) { + return trustedDePinServiceAddress; + } + + /// @notice Sets the DePIN service contract address + /// @dev Can only be set once + /// @param _trustedDePinService Address of the DePIN service contract + function setTrustedDePinServiceAddress(address _trustedDePinService) external { + require(trustedDePinServiceAddress == address(0), "Address already set"); + trustedDePinServiceAddress = _trustedDePinService; + } + + function setTrustedAMMAddress(address _trustedAMMAddress) external { + require(trustedAMMAddress == address(0), "AMM address already set"); + trustedAMMAddress = _trustedAMMAddress; + } + + /// @notice Checks if caller is a trusted contract + /// @return True if caller is either the DePIN service or AMM contract + function isTrusted() public view returns (bool) { + return msg.sender == trustedDePinServiceAddress || msg.sender == trustedAMMAddress; + } + + function setTransferUnlockTime(suint256 _transferUnlockTime) external { + require(msg.sender == trustedDePinServiceAddress, "Not authorized to set unlock time"); + transferUnlockTime = _transferUnlockTime; + } + + /// @notice Restricts function access to trusted contracts or after unlock time + /// @dev Used as a modifier for transfer-related functions, all addresses are whitelisted after unlock period + modifier whitelisted() { + require( + isTrusted() || suint256(block.number) % suint256(BLOCKS_PER_EPOCH) > transferUnlockTime, + "Only trusted addresses can call this function" + ); + _; + } + + //////////////////////////////////////////////////////////////*/ +} diff --git a/level/src/WidgetInterface.sol b/level/src/WidgetInterface.sol new file mode 100644 index 0000000..116653d --- /dev/null +++ b/level/src/WidgetInterface.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT License +pragma solidity ^0.8.13; + +import {ISRC20} from "./SRC20.sol"; +import {WDGSRC20} from "./WDGSRC20.sol"; +import {PermissionedAMM} from "./PermissionedAMM.sol"; + +/// @title WidgetInterface - DePIN Operator Reward Price Floor Protocol +/// @notice This contract implements a price floor mechanism for DePIN operator rewards, +/// ensuring operators can liquidate their rewards at a minimum guaranteed price. +/// @dev Uses an AMM to maintain a price floor and implements epoch-based withdrawal limits +/// to manage protocol liquidity +contract WidgetInterface { + address public rewardOracle; + uint256 public constant BLOCKS_PER_EPOCH = 7200; // about a day + suint256 private maxWithdrawalPerEpoch; // the max usdc that can be withdrawn per epoch + + PermissionedAMM public amm; + WDGSRC20 public WDG; + ISRC20 public USDC; + + mapping(saddress => suint256) epochWithdrawalAmt; + mapping(saddress => suint256) lastWithdrawalEpoch; + + modifier onlyOracle() { + require(msg.sender == rewardOracle, "Only the oracle can call this function"); + _; + } + + /// @notice Initializes the price floor protocol + /// @param _wdg Address of the operator reward token (WDG) + /// @param _usdc Address of the stablecoin used for payments/withdrawals + /// @param _rewardOracle Address authorized to distribute operator rewards + /// @param _maxWithdrawalPerEpoch Maximum USDC that can be withdrawn per epoch to manage protocol liquidity + constructor( + address _wdg, + address _usdc, + address _rewardOracle, + suint256 _maxWithdrawalPerEpoch, + suint256 _transferUnlockTime + ) { + rewardOracle = _rewardOracle; + WDG = WDGSRC20(_wdg); + USDC = ISRC20(_usdc); + maxWithdrawalPerEpoch = _maxWithdrawalPerEpoch; + amm = new PermissionedAMM(_wdg, _usdc); + + // set the wdg trusted addresses + WDG.setTrustedDePinServiceAddress(address(this)); + WDG.setTrustedAMMAddress(address(amm)); + WDG.setTransferUnlockTime(_transferUnlockTime); + } + + /// @notice Processes user payments for DePIN services + /// @dev Payments are used to maintain the price floor through token buybacks + /// @param usdcAmount Amount of USDC to pay for services + function payForService(suint256 usdcAmount) public { + // transfer USDC from user to this contract + // it is assumed the transfer is approved before calling this function + USDC.transferFrom(saddress(msg.sender), saddress(this), usdcAmount); + + // user payments are distributed to token holders / operators + // through token buybacks in the AMM + _serviceBuyback(usdcAmount); + + // + // PLACEHOLDER + // normally business logic would go here + // but this is a dummy function + // + } + + /// @notice Internal buyback mechanism to maintain price floor + /// @dev Converts service payments to WDG tokens and burns them, supporting token value + /// @param usdcAmount Amount of USDC to use for buyback + function _serviceBuyback(suint256 usdcAmount) internal { + // 1) swap USDC into WDG through the AMM + USDC.approve(saddress(amm), usdcAmount); + amm.swap(saddress(USDC), usdcAmount); + // 2) and burn the WDG token that is swapped out + WDG.burn(saddress(this), suint(WDG.balanceOf())); // assumed there is no reason for this contract to have a WDG balance + } + + /// @notice Distributes reward tokens to operators for their services + /// @dev Only callable by the oracle which determines reward distribution + /// @param operator Address of the DePIN operator + /// @param amount Amount of WDG tokens to mint as reward + function allocateReward(saddress operator, suint256 amount) external onlyOracle { + WDG.mint(operator, amount); // double check this is the correct token + } + + /// @notice Checks operator's remaining withdrawal capacity for the current epoch + /// @dev Enforces epoch-based withdrawal limits to manage protocol liquidity + /// @return Maximum amount of USDC that can currently be withdrawn in current epoch + function calcWithdrawalCap() internal returns (suint256) { + // reset the withdrawal cap if the user has not withdrawn in the current epoch + suint256 currentEpoch = suint(block.number) / suint(BLOCKS_PER_EPOCH); + if (currentEpoch > lastWithdrawalEpoch[saddress(msg.sender)]) { + epochWithdrawalAmt[saddress(msg.sender)] = suint(0); + lastWithdrawalEpoch[saddress(msg.sender)] = currentEpoch; + } else { + require(epochWithdrawalAmt[saddress(msg.sender)] == suint(0), "Already withdrawn this period."); + } + + suint256 usdcBalance = suint256(amm.calcSwapOutput(suint256(WDG.trustedBalanceOf(saddress(msg.sender))))); + return _min(maxWithdrawalPerEpoch, usdcBalance); + } + + // user facing function to view their current withdrawal cap + function viewWithdrawalCap() public returns (uint256) { + return uint256(calcWithdrawalCap()); + } + + /// @notice Allows operators to liquidate their reward tokens at the guaranteed minimum price + /// @dev Converts WDG to USDC through AMM at the protocol-maintained price floor + /// @param _amount Amount of USDC to withdraw + function operatorWithdraw(suint256 _amount) public { + suint256 withdrawalCap = calcWithdrawalCap(); // max usdc that user can withdraw + require(_amount <= withdrawalCap, "Overdrafting daily withdrawal limit or insufficient balance."); + + // calculate and swap amount of wdg for usdc + suint256 amountWdgIn = suint256(amm.calcSwapInput(address(USDC), _amount)); + WDG.transferFrom(saddress(msg.sender), saddress(this), amountWdgIn); + + amm.swapOut(saddress(USDC), _amount); // + USDC.transfer(saddress(msg.sender), suint(USDC.balanceOf())); + // USDC balance for this contract should be zero except during operatorWithdraw calls + epochWithdrawalAmt[saddress(msg.sender)] += _amount; + } + + /// @notice Utility function to return the minimum of two values + /// @param x First value + /// @param y Second value + /// @return Minimum of x and y + function _min(suint256 x, suint256 y) private pure returns (suint256) { + return x <= y ? x : y; + } +} diff --git a/level/test/AMM.t.sol b/level/test/AMM.t.sol new file mode 100644 index 0000000..c7b8828 --- /dev/null +++ b/level/test/AMM.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT License +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; + +import {PermissionedAMM} from "../src/PermissionedAMM.sol"; +import {WDGSRC20} from "../src/WDGSRC20.sol"; +import {WidgetInterface} from "../src/WidgetInterface.sol"; +import {ISRC20} from "../src/SRC20.sol"; +import {MockSRC20, MockWDGSRC20} from "./utils/MockSrc20.sol"; + +// todo: test only owner can swap, only owner can view, correct calcSwapInput + +contract TestAMM is Test { + address ammOwnerAddress = address(0x1); + WDGSRC20 public WDG; + MockSRC20 public USDC; + PermissionedAMM public amm; + + function setUp() public { + USDC = new MockSRC20("USDC", "USDC", 18); + WDG = new MockWDGSRC20("WDG", "WDG", 18); + + vm.prank(ammOwnerAddress); + amm = new PermissionedAMM(address(WDG), address(USDC)); + + WDG.setTrustedDePinServiceAddress(ammOwnerAddress); + WDG.setTrustedAMMAddress(address(amm)); + + vm.prank(ammOwnerAddress); + WDG.setTransferUnlockTime(suint(7100)); + + vm.prank(ammOwnerAddress); + WDG.mint(saddress(ammOwnerAddress), suint(1000 ether)); + vm.prank(ammOwnerAddress); + USDC.mint(saddress(ammOwnerAddress), suint(1000 ether)); + + vm.prank(ammOwnerAddress); + WDG.approve(saddress(amm), suint(1000 ether)); + vm.prank(ammOwnerAddress); + USDC.approve(saddress(amm), suint(1000 ether)); + + vm.prank(ammOwnerAddress); + amm.addLiquidity(suint(1000 ether), suint(1000 ether), ammOwnerAddress); + + assertEq(amm.owner(), ammOwnerAddress, "Owner should be set"); + } + + function testOnlyOwnerView() public { + address user1 = address(0x2); + vm.prank(ammOwnerAddress); + WDG.mint(saddress(user1), suint(100)); + + vm.prank(user1); + vm.expectRevert(); + amm.calcSwapInput(address(USDC), suint(10)); + } + + function testOnlyOwnerSwap() public { + address user2 = address(0x3); + vm.prank(ammOwnerAddress); + WDG.mint(saddress(user2), suint(100)); + + vm.prank(user2); + vm.expectRevert(); + amm.swap(saddress(WDG), suint(10)); + + vm.prank(user2); + vm.expectRevert(); + amm.swapOut(saddress(USDC), suint(10)); + } + + function testSwap() public { + // check swap rate is correct + vm.prank(ammOwnerAddress); + WDG.mint(saddress(ammOwnerAddress), suint(250 ether)); + + vm.prank(ammOwnerAddress); + amm.swap(saddress(WDG), suint(250 ether)); // should give back 20 usdc + + vm.prank(ammOwnerAddress); + suint256 usdcBal = suint256(USDC.balanceOf()); + assertTrue(usdcBal == suint256(200 ether), "Swap amount incorrect."); + } + + function testSwapOut() public { + vm.prank(ammOwnerAddress); + WDG.mint(saddress(ammOwnerAddress), suint256(200)); + + vm.prank(ammOwnerAddress); + amm.swapOut(saddress(USDC), suint256(100)); + + vm.prank(ammOwnerAddress); + suint256 usdcBal = suint256(USDC.balanceOf()); + assertTrue(usdcBal == suint256(100), "Does not swap to correct amount."); + + // must revert if not enough balance for swap. + vm.prank(ammOwnerAddress); + vm.expectRevert(); + amm.swapOut(saddress(USDC), suint(101)); + } +} diff --git a/level/test/WidgetInterface.t.sol b/level/test/WidgetInterface.t.sol new file mode 100644 index 0000000..a2a33ab --- /dev/null +++ b/level/test/WidgetInterface.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT License +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; + +import {PermissionedAMM} from "../src/PermissionedAMM.sol"; +import {WDGSRC20} from "../src/WDGSRC20.sol"; +import {WidgetInterface} from "../src/WidgetInterface.sol"; +import {ISRC20} from "../src/SRC20.sol"; +import {MockSRC20, MockWDGSRC20} from "./utils/MockSrc20.sol"; + +contract TestInterface is Test { + address oracleAddress = address(0x1); + WDGSRC20 public WDG; + MockSRC20 public USDC; + WidgetInterface public service; + PermissionedAMM public amm; + + function setUp() public { + USDC = new MockSRC20("USDC", "USDC", 18); + WDG = new MockWDGSRC20("WDG", "WDG", 18); + service = new WidgetInterface(address(WDG), address(USDC), oracleAddress, suint(100), suint(7100)); + + amm = service.amm(); + // set up the amm + vm.prank(address(service)); + WDG.mint(saddress(service), suint(1000 ether)); + USDC.mint(saddress(service), suint(1000 ether)); + + vm.prank(address(service)); + WDG.approve(saddress(amm), suint(1000 ether)); + vm.prank(address(service)); + USDC.approve(saddress(amm), suint(1000 ether)); + + vm.prank(address(service)); + amm.addLiquidity(suint(1000 ether), suint(1000 ether), address(service)); // service balances are 0 + + assertEq(amm.owner(), address(service), "Owner should be service"); + } + + function testTransfer() public { + vm.prank(address(amm)); + uint256 ammBalanceInit = WDG.balanceOf(); + + vm.prank(address(service)); + WDG.mint(saddress(service), suint(1 ether)); + vm.prank(address(service)); + WDG.transfer(saddress(amm), suint(1 ether)); + + vm.prank(address(amm)); + uint256 ammBalanceFin = WDG.balanceOf(); + + assertEq(ammBalanceFin - ammBalanceInit, 1 ether, "Incorrect amount transferred."); + } + + function testTransferFail() public { + address user1 = address(0x2); + vm.prank(address(service)); + WDG.mint(saddress(user1), suint(100 ether)); + vm.prank(user1); + + vm.expectRevert(); + WDG.transfer(saddress(service), suint(20 ether)); + } + + function testPayForService() public { + address user2 = address(0x3); + // check if wdg reserve changed + vm.prank(address(service)); + uint256 initWdgReserve = WDG.trustedBalanceOf(saddress(amm)); + + USDC.mint(saddress(user2), suint(20)); + vm.prank(user2); + USDC.approve(saddress(service), suint(20)); + + vm.prank(user2); + service.payForService(suint(20)); // check balance decreased + + vm.prank(address(service)); // + uint256 finWdgReserve = WDG.trustedBalanceOf(saddress(amm)); + + assertTrue(initWdgReserve > finWdgReserve, "Burn unsuccessful."); + + vm.prank(user2); + vm.expectRevert(); // not enough balance + service.payForService(suint(20)); + } + + function testAllocateReward() public { + vm.prank(oracleAddress); + // allocating reward to service to bypass whitelisting + service.allocateReward(saddress(service), suint(20 ether)); + + vm.prank(address(service)); + uint256 bal = WDG.balanceOf(); + + assertEq(bal, 20 ether, "Minted balances do not match"); + } + + function testAllocateRewardFail() public { + address user3 = address(0x4); + vm.prank(user3); + vm.expectRevert(); + service.allocateReward(saddress(user3), suint(20 ether)); + } + + function testoperatorWithdraw() public { + address user5 = address(0x6); + vm.prank(address(service)); + WDG.mint(saddress(user5), suint256(200)); + + vm.prank(user5); + uint256 wdCap = service.viewWithdrawalCap(); + assertEq(wdCap, 100, "Incorrect withdrawal cap."); + + vm.prank(user5); + service.operatorWithdraw(suint256(100)); + + vm.prank(user5); + uint256 newBalance = USDC.balanceOf(); + assertTrue(newBalance == 100, "Swap balance incorrect"); + + vm.prank(user5); + vm.expectRevert(); + service.viewWithdrawalCap(); + } +} diff --git a/level/test/utils/MockSrc20.sol b/level/test/utils/MockSrc20.sol new file mode 100644 index 0000000..22d11a9 --- /dev/null +++ b/level/test/utils/MockSrc20.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {SRC20} from "../../src/SRC20.sol"; +import {WDGSRC20} from "../../src/WDGSRC20.sol"; + +contract MockSRC20 is SRC20 { + constructor(string memory _name, string memory _symbol, uint8 _decimals) SRC20(_name, _symbol, _decimals) {} + + function mint(saddress to, suint256 value) public virtual { + _mint(to, value); + } + + function burn(saddress from, suint256 value) public virtual { + _burn(from, value); + } +} + +contract MockWDGSRC20 is WDGSRC20 { + constructor(string memory _name, string memory _symbol, uint8 _decimals) WDGSRC20(_name, _symbol, _decimals) {} +} diff --git a/walnut/lib/forge-std b/walnut/lib/forge-std deleted file mode 160000 index 3b20d60..0000000 --- a/walnut/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 diff --git a/walnut/script/Walnut.s.sol b/walnut/script/Walnut.s.sol deleted file mode 100644 index dece5b9..0000000 --- a/walnut/script/Walnut.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Walnut} from "../src/Walnut.sol"; - -contract WalnutScript is Script { - Walnut public walnut; - - function run() public { - uint256 deployerPrivateKey = vm.envUint("PRIVKEY"); - - vm.startBroadcast(deployerPrivateKey); - walnut = new Walnut(3, suint256(0)); - vm.stopBroadcast(); - } -} diff --git a/walnut/src/Walnut.sol b/walnut/src/Walnut.sol deleted file mode 100644 index 369ae43..0000000 --- a/walnut/src/Walnut.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT License -pragma solidity ^0.8.13; - -contract Walnut { - uint256 initialShellStrength; // The initial shell strength for resets. - uint256 shellStrength; // The current shell strength. - uint256 round; // The current round number. - - suint256 initialKernel; // The initial hidden kernel value for resets. - suint256 kernel; // The current hidden kernel value. - - // Tracks the number of hits per player per round. - mapping(uint256 => mapping(address => uint256)) hitsPerRound; - - // Events to log hits, shakes, and resets. - - // Event to log hits. - event Hit(uint256 indexed round, address indexed hitter, uint256 remaining); // Logged when a hit occurs. - // Event to log shakes. - event Shake(uint256 indexed round, address indexed shaker); // Logged when the Walnut is shaken. - // Event to log resets. - event Reset(uint256 indexed newRound, uint256 remainingShellStrength); - - constructor(uint256 _shellStrength, suint256 _kernel) { - initialShellStrength = _shellStrength; // Set the initial shell strength. - shellStrength = _shellStrength; // Initialize the shell strength. - - initialKernel = _kernel; // Set the initial kernel value. - kernel = _kernel; // Initialize the kernel value. - - round = 1; // Start with the first round. - } - - // Get the current shell strength. - function getShellStrength() public view returns (uint256) { - return shellStrength; - } - - // Hit the Walnut to reduce its shell strength. - function hit() public requireIntact { - shellStrength--; // Decrease the shell strength. - hitsPerRound[round][msg.sender]++; // Record the player's hit for the current round. - emit Hit(round, msg.sender, shellStrength); // Log the hit. - } - - // Shake the Walnut to increase the kernel value. - function shake(suint256 _numShakes) public requireIntact { - kernel += _numShakes; // Increment the kernel value. - emit Shake(round, msg.sender); // Log the shake. - } - - // Reset the Walnut for a new round. - function reset() public requireCracked { - shellStrength = initialShellStrength; // Reset the shell strength. - kernel = initialKernel; // Reset the kernel value. - round++; // Move to the next round. - emit Reset(round, shellStrength); // Log the reset. - } - - // Look at the kernel if the shell is cracked and the caller contributed. - function look() public view requireCracked onlyContributor returns (uint256) { - return uint256(kernel); // Return the kernel value. - } - - // Set the kernel to a specific value. - function set_number(suint _kernel) public { - kernel = _kernel; - } - - // Modifier to ensure the shell is fully cracked. - modifier requireCracked() { - require(shellStrength == 0, "SHELL_INTACT"); - _; - } - - // Modifier to ensure the shell is not cracked. - modifier requireIntact() { - require(shellStrength > 0, "SHELL_ALREADY_CRACKED"); - _; - } - - // Modifier to ensure the caller has contributed in the current round. - modifier onlyContributor() { - require(hitsPerRound[round][msg.sender] > 0, "NOT_A_CONTRIBUTOR"); - _; - } -} diff --git a/walnut/test/Walnut.t.sol b/walnut/test/Walnut.t.sol deleted file mode 100644 index 1b24060..0000000 --- a/walnut/test/Walnut.t.sol +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: MIT License -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Walnut} from "../src/Walnut.sol"; - -contract WalnutTest is Test { - Walnut public walnut; - - function setUp() public { - walnut = new Walnut(2, suint256(0)); - - saddress a = saddress(0x123); - console.log(address(a)); - } - - function test_Hit() public { - walnut.hit(); - walnut.hit(); - assertEq(walnut.look(), 0); - } - - function test_Shake() public { - walnut.shake(suint256(10)); - walnut.hit(); - walnut.hit(); - assertEq(walnut.look(), 10); - } - - function test_Reset() public { - walnut.hit(); - walnut.shake(suint256(2)); - walnut.hit(); - walnut.reset(); - assertEq(walnut.getShellStrength(), 2); // Shell strength should be reset to 2 - walnut.hit(); - walnut.shake(suint256(5)); - walnut.hit(); - assertEq(walnut.look(), 5); // Look should return 5 since the shell was reset - } - - function test_CannotHitWhenCracked() public { - walnut.hit(); - walnut.hit(); - vm.expectRevert("SHELL_ALREADY_CRACKED"); // Expect a revert when hitting a cracked shell - walnut.hit(); - } - - function test_CannotShakeWhenCracked() public { - walnut.hit(); - walnut.shake(suint256(1)); - walnut.shake(suint256(1)); - walnut.hit(); - vm.expectRevert("SHELL_ALREADY_CRACKED"); // Expect a revert when shaking a cracked shell - walnut.shake(suint256(1)); - } - - function test_CannotLookWhenIntact() public { - walnut.hit(); - walnut.shake(suint256(1)); - vm.expectRevert("SHELL_INTACT"); // Expect a revert when looking at an intact shell - walnut.look(); - } - - function test_CannotResetWhenIntact() public { - vm.expectRevert("SHELL_INTACT"); // Expect a revert when resetting an intact shell - walnut.reset(); - } - - function test_ManyActions() public { - uint256 shakes = 0; - for (uint256 i = 0; i < 50; i++) { - // Only shake if the walnut is still intact - if (walnut.getShellStrength() > 0) { - if (i % 25 == 0) { - walnut.hit(); - } else { - // Shake a random number of times between 1-3 - uint256 numShakes = (i % 3) + 1; - walnut.shake(suint256(numShakes)); - shakes += numShakes; - } - } - } - assertEq(walnut.look(), shakes); - } - - function test_RevertWhen_NonContributorTriesToLook() public { - // Address that will attempt to call 'look' without contributing - address nonContributor = address(0xabcd); - - // Ensure the shell is cracked - walnut.hit(); - walnut.shake(suint256(3)); - walnut.hit(); - - // Expect the 'look' function to revert with "NOT_A_CONTRIBUTOR" error - vm.prank(address(nonContributor)); - console.log(address(this)); - vm.expectRevert("NOT_A_CONTRIBUTOR"); - walnut.look(); - assertEq(walnut.look(), 3); - } - - function test_ContributorInRound2() public { - // Address that will become a contributor in round 2 - address contributorRound2 = address(0xabcd); - - // Round 1: Walnut broken by address(this) - walnut.hit(); // Hit 1 by address(this) - walnut.hit(); // Hit 2 by address(this) - assertEq(walnut.look(), 0); // Verify the walnut is cracked and look() works for address(this) - - // Reset the walnut, moving to round 2 - walnut.reset(); - - // Round 2: Walnut broken by contributorRound2 - vm.prank(contributorRound2); - walnut.hit(); // Hit 1 by contributorRound2 - - vm.prank(contributorRound2); - walnut.shake(suint256(5)); // Shake 5 times by contributorRound2 - - vm.prank(contributorRound2); - walnut.hit(); // Hit 2 by contributorRound2 - - // Verify contributorRound2 can call look() in round 2 - vm.prank(contributorRound2); - assertEq(walnut.look(), 5); // Expect the number to be 5 due to 5 shakes - - // Verify address(this) cannot call look() in round 2 - vm.expectRevert("NOT_A_CONTRIBUTOR"); - walnut.look(); - } -} From aa46f51d88c7d360415a22d142fb2c453bf1a574 Mon Sep 17 00:00:00 2001 From: topanisto Date: Fri, 7 Feb 2025 18:43:56 -0500 Subject: [PATCH 02/12] forge install: openzeppelin-contracts v5.2.0 --- .gitmodules | 5 ++++- level/lib/openzeppelin-contracts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 160000 level/lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 45fe273..febd1fc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "walnut/lib/forge-std"] path = walnut/lib/forge-std - url = https://github.com/foundry-rs/forge-std \ No newline at end of file + url = https://github.com/foundry-rs/forge-std +[submodule "level/lib/openzeppelin-contracts"] + path = level/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/level/lib/openzeppelin-contracts b/level/lib/openzeppelin-contracts new file mode 160000 index 0000000..acd4ff7 --- /dev/null +++ b/level/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 From cd33ce2546285196b5dc8bbd7471b0fbdbaeda9f Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 14:11:11 -0500 Subject: [PATCH 03/12] readme --- level/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 level/README.md diff --git a/level/README.md b/level/README.md new file mode 100644 index 0000000..0ff3abf --- /dev/null +++ b/level/README.md @@ -0,0 +1,19 @@ +# Level Protocol + +> Stabilize your depin service + +## Overview + +Level Protocol provides price stability for Decentralized Physical Infrastructure Networks (DePIN) to ensure consistent service quality and network growth. + +### Problem + +DePIN projects that rely on token incentives to bootstrap service operators are highly vulnerable to token price fluctuations. A sharp drop in token value can cause operators to exit the network entirely, triggering a feedback loop that rapidly degrades service quality and coverage. + +### Insight + +We can lighten the burden of token volatility on operators by always guaranteeing a minimum withdrawal value and a baseline price for the token. Network quality will be more stable and able to grow faster. + +### Solution + +Our project introduces a token with an obscured exchange rate, ensuring operators receive rewards above a base price set by the protocol each epoch, covering estimated operating costs. \ No newline at end of file From 29f91db4035c3edfeac82a99a73f00d2df047933 Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 14:25:17 -0500 Subject: [PATCH 04/12] forge-std back, comments for setTransferUnlockTime --- level/src/WDGSRC20.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/level/src/WDGSRC20.sol b/level/src/WDGSRC20.sol index 3b8b2a5..3230e64 100644 --- a/level/src/WDGSRC20.sol +++ b/level/src/WDGSRC20.sol @@ -159,6 +159,10 @@ abstract contract WDGSRC20 is ISRC20 { return msg.sender == trustedDePinServiceAddress || msg.sender == trustedAMMAddress; } + /// @notice Sets the time period before whitelisted actions are enabled + // for all addresses. Resets every epoch. + /// @dev Only callable by the trusted DePIN service contract + /// @param _transferUnlockTime Number of blocks within an epoch before transfers are allowed function setTransferUnlockTime(suint256 _transferUnlockTime) external { require(msg.sender == trustedDePinServiceAddress, "Not authorized to set unlock time"); transferUnlockTime = _transferUnlockTime; From c95011151115277bcc6448ddd14c57629b2ba48f Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 14:31:37 -0500 Subject: [PATCH 05/12] forge std --- walnut/lib/forge-std | 1 + 1 file changed, 1 insertion(+) create mode 160000 walnut/lib/forge-std diff --git a/walnut/lib/forge-std b/walnut/lib/forge-std new file mode 160000 index 0000000..3b20d60 --- /dev/null +++ b/walnut/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 From 75a60a65f7544c52543e19e26d92f1800dd6d303 Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 14:32:20 -0500 Subject: [PATCH 06/12] moving forge-std into correct folder --- walnut/lib/forge-std | 1 - 1 file changed, 1 deletion(-) delete mode 160000 walnut/lib/forge-std diff --git a/walnut/lib/forge-std b/walnut/lib/forge-std deleted file mode 160000 index 3b20d60..0000000 --- a/walnut/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 From f8230e78001a6d1b109290152529035dbb48327d Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 14:51:13 -0500 Subject: [PATCH 07/12] gitmodules --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index febd1fc..b110552 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,5 +1,5 @@ -[submodule "walnut/lib/forge-std"] - path = walnut/lib/forge-std +[submodule "level/lib/forge-std"] + path = level/lib/forge-std url = https://github.com/foundry-rs/forge-std [submodule "level/lib/openzeppelin-contracts"] path = level/lib/openzeppelin-contracts From f5da6462b22ff260f5aceb495ca3bd0c2ce0a8c1 Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 15:27:26 -0500 Subject: [PATCH 08/12] readme update --- level/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/level/README.md b/level/README.md index 0ff3abf..df983ba 100644 --- a/level/README.md +++ b/level/README.md @@ -12,7 +12,7 @@ DePIN projects that rely on token incentives to bootstrap service operators are ### Insight -We can lighten the burden of token volatility on operators by always guaranteeing a minimum withdrawal value and a baseline price for the token. Network quality will be more stable and able to grow faster. +We can lighten the burden of token volatility on operators by making the price not viewable, while still guaranteeing a minimum withdrawal value. Network quality will be more stable and able to grow faster. ### Solution From 52b9df072488cb76dee26b8aee0deb684a3bcd55 Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 15:45:44 -0500 Subject: [PATCH 09/12] one-liner to main readme --- README.md | 6 ++---- level/README.md | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5141240..bc4b380 100644 --- a/README.md +++ b/README.md @@ -15,10 +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. -2. **`Project 2 here`** - Description here. +1. **`LEVEL`** + Stabilize your DePIN service. ## Contributing diff --git a/level/README.md b/level/README.md index df983ba..f84af38 100644 --- a/level/README.md +++ b/level/README.md @@ -1,10 +1,8 @@ -# Level Protocol - -> Stabilize your depin service +# LEVEL Protocol ## Overview -Level Protocol provides price stability for Decentralized Physical Infrastructure Networks (DePIN) to ensure consistent service quality and network growth. +LEVEL provides price stability for Decentralized Physical Infrastructure Networks (DePIN) to ensure consistent service quality and network growth. ### Problem From ae3202898642b462fe51ea3c494a56ed0845d2ba Mon Sep 17 00:00:00 2001 From: topanisto Date: Sat, 8 Feb 2025 15:51:51 -0500 Subject: [PATCH 10/12] forge-std --- level/lib/forge-std | 1 + 1 file changed, 1 insertion(+) create mode 160000 level/lib/forge-std diff --git a/level/lib/forge-std b/level/lib/forge-std new file mode 160000 index 0000000..3b20d60 --- /dev/null +++ b/level/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 From 1e42853fc6288f9ceb0f44a1e0d7d6d6875969ed Mon Sep 17 00:00:00 2001 From: topanisto Date: Mon, 10 Feb 2025 13:59:58 -0500 Subject: [PATCH 11/12] readme and changes to filenames, also changed whitelisted to require unlocking after a *global* unlock time, not a timestamp every day --- level/README.md | 8 ++++---- level/src/{PermissionedAMM.sol => InternalAMM.sol} | 4 ++-- level/src/{WidgetInterface.sol => Level.sol} | 10 +++++----- level/src/WDGSRC20.sol | 2 +- level/test/AMM.t.sol | 8 ++++---- level/test/{WidgetInterface.t.sol => Level.t.sol} | 10 +++++----- 6 files changed, 21 insertions(+), 21 deletions(-) rename level/src/{PermissionedAMM.sol => InternalAMM.sol} (98%) rename level/src/{WidgetInterface.sol => Level.sol} (96%) rename level/test/{WidgetInterface.t.sol => Level.t.sol} (92%) diff --git a/level/README.md b/level/README.md index f84af38..2c18c63 100644 --- a/level/README.md +++ b/level/README.md @@ -2,16 +2,16 @@ ## Overview -LEVEL provides price stability for Decentralized Physical Infrastructure Networks (DePIN) to ensure consistent service quality and network growth. +LEVEL ensures stable network coverage for Decentralized Physical Infrastructure Networks (DePIN) independent of token price fluctuations during the initial bootstrapping phase. ### Problem -DePIN projects that rely on token incentives to bootstrap service operators are highly vulnerable to token price fluctuations. A sharp drop in token value can cause operators to exit the network entirely, triggering a feedback loop that rapidly degrades service quality and coverage. +DePIN projects that rely on token incentives to bootstrap service operators are highly vulnerable to token price fluctuations. When token prices surge, coverage rapidly expands, but can contract just as quickly during price downturns. This cyclical pattern makes it difficult to maintain consistent service quality and coverage. ### Insight -We can lighten the burden of token volatility on operators by making the price not viewable, while still guaranteeing a minimum withdrawal value. Network quality will be more stable and able to grow faster. +We can decouple network coverage from token price volatility by temporarily obscuring the exchange rate during the critical bootstrapping period, while still guaranteeing a minimum withdrawal value. This allows the network to establish stable coverage patterns unencumbered by speculative responses to token movements. ### Solution -Our project introduces a token with an obscured exchange rate, ensuring operators receive rewards above a base price set by the protocol each epoch, covering estimated operating costs. \ No newline at end of file +Our project introduces a token with an obscured exchange rate during the initial bootstrapping phase, ensuring operators receive rewards above a base price set by the protocol each epoch to covering estimated operating costs. \ No newline at end of file diff --git a/level/src/PermissionedAMM.sol b/level/src/InternalAMM.sol similarity index 98% rename from level/src/PermissionedAMM.sol rename to level/src/InternalAMM.sol index ab09470..f8482ef 100644 --- a/level/src/PermissionedAMM.sol +++ b/level/src/InternalAMM.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.13; import {ISRC20} from "./SRC20.sol"; import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; -/// @title PermissionedAMM - Restricted Access Automated Market Maker +/// @title InternalAMM - Restricted Access Automated Market Maker /// @notice A constant product AMM (x*y=k) that manages the liquidity pool for the DePIN price floor protocol /// @dev Uses shielded data types (suint256) for privacy-preserving calculations /// @dev All operations are restricted to the owner to so that even properties relevent to the caller, // e.g. price, can't be observed until the owner wishes to reveal them -contract PermissionedAMM is Ownable(msg.sender) { +contract InternalAMM is Ownable(msg.sender) { ISRC20 public token0; ISRC20 public token1; diff --git a/level/src/WidgetInterface.sol b/level/src/Level.sol similarity index 96% rename from level/src/WidgetInterface.sol rename to level/src/Level.sol index 116653d..a6322a8 100644 --- a/level/src/WidgetInterface.sol +++ b/level/src/Level.sol @@ -3,19 +3,19 @@ pragma solidity ^0.8.13; import {ISRC20} from "./SRC20.sol"; import {WDGSRC20} from "./WDGSRC20.sol"; -import {PermissionedAMM} from "./PermissionedAMM.sol"; +import {InternalAMM} from "./InternalAMM.sol"; -/// @title WidgetInterface - DePIN Operator Reward Price Floor Protocol +/// @title Level - DePIN Operator Reward Price Floor Protocol /// @notice This contract implements a price floor mechanism for DePIN operator rewards, /// ensuring operators can liquidate their rewards at a minimum guaranteed price. /// @dev Uses an AMM to maintain a price floor and implements epoch-based withdrawal limits /// to manage protocol liquidity -contract WidgetInterface { +contract Level { address public rewardOracle; uint256 public constant BLOCKS_PER_EPOCH = 7200; // about a day suint256 private maxWithdrawalPerEpoch; // the max usdc that can be withdrawn per epoch - PermissionedAMM public amm; + InternalAMM public amm; WDGSRC20 public WDG; ISRC20 public USDC; @@ -43,7 +43,7 @@ contract WidgetInterface { WDG = WDGSRC20(_wdg); USDC = ISRC20(_usdc); maxWithdrawalPerEpoch = _maxWithdrawalPerEpoch; - amm = new PermissionedAMM(_wdg, _usdc); + amm = new InternalAMM(_wdg, _usdc); // set the wdg trusted addresses WDG.setTrustedDePinServiceAddress(address(this)); diff --git a/level/src/WDGSRC20.sol b/level/src/WDGSRC20.sol index 3230e64..a7ef236 100644 --- a/level/src/WDGSRC20.sol +++ b/level/src/WDGSRC20.sol @@ -172,7 +172,7 @@ abstract contract WDGSRC20 is ISRC20 { /// @dev Used as a modifier for transfer-related functions, all addresses are whitelisted after unlock period modifier whitelisted() { require( - isTrusted() || suint256(block.number) % suint256(BLOCKS_PER_EPOCH) > transferUnlockTime, + isTrusted() || suint256(block.number) > transferUnlockTime, "Only trusted addresses can call this function" ); _; diff --git a/level/test/AMM.t.sol b/level/test/AMM.t.sol index c7b8828..a4980de 100644 --- a/level/test/AMM.t.sol +++ b/level/test/AMM.t.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; -import {PermissionedAMM} from "../src/PermissionedAMM.sol"; +import {InternalAMM} from "../src/InternalAMM.sol"; import {WDGSRC20} from "../src/WDGSRC20.sol"; -import {WidgetInterface} from "../src/WidgetInterface.sol"; +import {Level} from "../src/Level.sol"; import {ISRC20} from "../src/SRC20.sol"; import {MockSRC20, MockWDGSRC20} from "./utils/MockSrc20.sol"; @@ -15,14 +15,14 @@ contract TestAMM is Test { address ammOwnerAddress = address(0x1); WDGSRC20 public WDG; MockSRC20 public USDC; - PermissionedAMM public amm; + InternalAMM public amm; function setUp() public { USDC = new MockSRC20("USDC", "USDC", 18); WDG = new MockWDGSRC20("WDG", "WDG", 18); vm.prank(ammOwnerAddress); - amm = new PermissionedAMM(address(WDG), address(USDC)); + amm = new InternalAMM(address(WDG), address(USDC)); WDG.setTrustedDePinServiceAddress(ammOwnerAddress); WDG.setTrustedAMMAddress(address(amm)); diff --git a/level/test/WidgetInterface.t.sol b/level/test/Level.t.sol similarity index 92% rename from level/test/WidgetInterface.t.sol rename to level/test/Level.t.sol index a2a33ab..919996e 100644 --- a/level/test/WidgetInterface.t.sol +++ b/level/test/Level.t.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; -import {PermissionedAMM} from "../src/PermissionedAMM.sol"; +import {InternalAMM} from "../src/InternalAMM.sol"; import {WDGSRC20} from "../src/WDGSRC20.sol"; -import {WidgetInterface} from "../src/WidgetInterface.sol"; +import {Level} from "../src/Level.sol"; import {ISRC20} from "../src/SRC20.sol"; import {MockSRC20, MockWDGSRC20} from "./utils/MockSrc20.sol"; @@ -13,13 +13,13 @@ contract TestInterface is Test { address oracleAddress = address(0x1); WDGSRC20 public WDG; MockSRC20 public USDC; - WidgetInterface public service; - PermissionedAMM public amm; + Level public service; + InternalAMM public amm; function setUp() public { USDC = new MockSRC20("USDC", "USDC", 18); WDG = new MockWDGSRC20("WDG", "WDG", 18); - service = new WidgetInterface(address(WDG), address(USDC), oracleAddress, suint(100), suint(7100)); + service = new Level(address(WDG), address(USDC), oracleAddress, suint(100), suint(7100)); amm = service.amm(); // set up the amm From 4317d645680d7a628ade360c8aefecd3fd5577e3 Mon Sep 17 00:00:00 2001 From: topanisto Date: Mon, 10 Feb 2025 14:22:36 -0500 Subject: [PATCH 12/12] dev docs changes, removing trusted prefix --- level/src/Level.sol | 27 ++++++++++++++++----------- level/src/WDGSRC20.sol | 29 ++++++++++++++--------------- level/test/AMM.t.sol | 4 ++-- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/level/src/Level.sol b/level/src/Level.sol index a6322a8..032915c 100644 --- a/level/src/Level.sol +++ b/level/src/Level.sol @@ -6,10 +6,9 @@ import {WDGSRC20} from "./WDGSRC20.sol"; import {InternalAMM} from "./InternalAMM.sol"; /// @title Level - DePIN Operator Reward Price Floor Protocol -/// @notice This contract implements a price floor mechanism for DePIN operator rewards, -/// ensuring operators can liquidate their rewards at a minimum guaranteed price. -/// @dev Uses an AMM to maintain a price floor and implements epoch-based withdrawal limits -/// to manage protocol liquidity +/// @notice This contract implements a minimum price guarantee mechanism for DePIN operator rewards, +/// ensuring operators can liquidate their rewards at a guaranteed minimum price. +/// @dev Uses an AMM to provide price guarantees and implements epoch-based withdrawal limits contract Level { address public rewardOracle; uint256 public constant BLOCKS_PER_EPOCH = 7200; // about a day @@ -46,13 +45,13 @@ contract Level { amm = new InternalAMM(_wdg, _usdc); // set the wdg trusted addresses - WDG.setTrustedDePinServiceAddress(address(this)); - WDG.setTrustedAMMAddress(address(amm)); + WDG.setDepinServiceAddress(address(this)); + WDG.setAMMAddress(address(amm)); WDG.setTransferUnlockTime(_transferUnlockTime); } /// @notice Processes user payments for DePIN services - /// @dev Payments are used to maintain the price floor through token buybacks + /// @dev Payments are used to support the price guarantee through token buybacks /// @param usdcAmount Amount of USDC to pay for services function payForService(suint256 usdcAmount) public { // transfer USDC from user to this contract @@ -70,7 +69,7 @@ contract Level { // } - /// @notice Internal buyback mechanism to maintain price floor + /// @notice Internal buyback mechanism to support price guarantees /// @dev Converts service payments to WDG tokens and burns them, supporting token value /// @param usdcAmount Amount of USDC to use for buyback function _serviceBuyback(suint256 usdcAmount) internal { @@ -91,6 +90,8 @@ contract Level { /// @notice Checks operator's remaining withdrawal capacity for the current epoch /// @dev Enforces epoch-based withdrawal limits to manage protocol liquidity + /// @dev TODO: Future versions should decouple withdrawal caps from token sales to allow + /// operators to manage their token exposure without affecting their withdrawal limits /// @return Maximum amount of USDC that can currently be withdrawn in current epoch function calcWithdrawalCap() internal returns (suint256) { // reset the withdrawal cap if the user has not withdrawn in the current epoch @@ -106,13 +107,17 @@ contract Level { return _min(maxWithdrawalPerEpoch, usdcBalance); } - // user facing function to view their current withdrawal cap + /// @notice Returns the maximum amount of USDC an operator can currently withdraw + /// @dev Provides a view into the operator's withdrawal capacity for the current epoch + /// without modifying state. Useful for UIs and off-chain calculations. + /// @return The maximum amount of USDC that can be withdrawn in the current epoch, + /// limited by both the epoch withdrawal cap and the operator's WDG balance function viewWithdrawalCap() public returns (uint256) { return uint256(calcWithdrawalCap()); } - /// @notice Allows operators to liquidate their reward tokens at the guaranteed minimum price - /// @dev Converts WDG to USDC through AMM at the protocol-maintained price floor + /// @notice Allows operators to liquidate their reward tokens at the guaranteed price + /// @dev Converts WDG to USDC through AMM at the protocol-guaranteed price /// @param _amount Amount of USDC to withdraw function operatorWithdraw(suint256 _amount) public { suint256 withdrawalCap = calcWithdrawalCap(); // max usdc that user can withdraw diff --git a/level/src/WDGSRC20.sol b/level/src/WDGSRC20.sol index a7ef236..0a592d5 100644 --- a/level/src/WDGSRC20.sol +++ b/level/src/WDGSRC20.sol @@ -133,30 +133,30 @@ abstract contract WDGSRC20 is ISRC20 { Trusted Address Logic //////////////////////////////////////////////////////////////*/ - address public trustedDePinServiceAddress; - address public trustedAMMAddress; + address public depinServiceAddress; + address public AMMAddress; - function getTrustedDepinServiceAddress() public view returns (address) { - return trustedDePinServiceAddress; + function getDepinServiceAddress() public view returns (address) { + return depinServiceAddress; } /// @notice Sets the DePIN service contract address /// @dev Can only be set once - /// @param _trustedDePinService Address of the DePIN service contract - function setTrustedDePinServiceAddress(address _trustedDePinService) external { - require(trustedDePinServiceAddress == address(0), "Address already set"); - trustedDePinServiceAddress = _trustedDePinService; + /// @param _depinServiceAddress Address of the DePIN service contract + function setDepinServiceAddress(address _depinServiceAddress) external { + require(depinServiceAddress == address(0), "Address already set"); + depinServiceAddress = _depinServiceAddress; } - function setTrustedAMMAddress(address _trustedAMMAddress) external { - require(trustedAMMAddress == address(0), "AMM address already set"); - trustedAMMAddress = _trustedAMMAddress; + function setAMMAddress(address _AMMAddress) external { + require(AMMAddress == address(0), "AMM address already set"); + AMMAddress = _AMMAddress; } /// @notice Checks if caller is a trusted contract /// @return True if caller is either the DePIN service or AMM contract function isTrusted() public view returns (bool) { - return msg.sender == trustedDePinServiceAddress || msg.sender == trustedAMMAddress; + return msg.sender == depinServiceAddress || msg.sender == AMMAddress; } /// @notice Sets the time period before whitelisted actions are enabled @@ -164,7 +164,7 @@ abstract contract WDGSRC20 is ISRC20 { /// @dev Only callable by the trusted DePIN service contract /// @param _transferUnlockTime Number of blocks within an epoch before transfers are allowed function setTransferUnlockTime(suint256 _transferUnlockTime) external { - require(msg.sender == trustedDePinServiceAddress, "Not authorized to set unlock time"); + require(msg.sender == depinServiceAddress, "Not authorized to set unlock time"); transferUnlockTime = _transferUnlockTime; } @@ -172,8 +172,7 @@ abstract contract WDGSRC20 is ISRC20 { /// @dev Used as a modifier for transfer-related functions, all addresses are whitelisted after unlock period modifier whitelisted() { require( - isTrusted() || suint256(block.number) > transferUnlockTime, - "Only trusted addresses can call this function" + isTrusted() || suint256(block.number) > transferUnlockTime, "Only trusted addresses can call this function" ); _; } diff --git a/level/test/AMM.t.sol b/level/test/AMM.t.sol index a4980de..37a968f 100644 --- a/level/test/AMM.t.sol +++ b/level/test/AMM.t.sol @@ -24,8 +24,8 @@ contract TestAMM is Test { vm.prank(ammOwnerAddress); amm = new InternalAMM(address(WDG), address(USDC)); - WDG.setTrustedDePinServiceAddress(ammOwnerAddress); - WDG.setTrustedAMMAddress(address(amm)); + WDG.setDepinServiceAddress(ammOwnerAddress); + WDG.setAMMAddress(address(amm)); vm.prank(ammOwnerAddress); WDG.setTransferUnlockTime(suint(7100));