From 93ee8de67248866f26ebc5040d3e6853171da09b Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Tue, 3 Jan 2023 17:31:15 -0500 Subject: [PATCH] format --- .prettierrc.yml | 6 - .vscode/settings.json | 2 +- README.md | 3 +- foundry.toml | 11 + src/core/ERC20.sol | 247 ++- src/core/Factory.sol | 117 +- src/core/ImmutableState.sol | 38 +- src/core/JumpRate.sol | 68 +- src/core/Lendgine.sol | 331 ++-- src/core/Pair.sol | 167 +- src/core/ReentrancyGuard.sol | 14 +- src/core/interfaces/IFactory.sol | 41 + src/core/interfaces/IImmutableState.sol | 24 + src/core/interfaces/IJumpRate.sol | 10 +- src/core/interfaces/ILendgine.sol | 50 + src/core/interfaces/IPair.sol | 26 + .../interfaces/callback/IMintCallback.sol | 23 +- .../interfaces/callback/IPairMintCallback.sol | 10 +- .../interfaces/callback/ISwapCallback.sol | 14 +- src/core/libraries/Position.sol | 138 +- src/core/libraries/PositionMath.sol | 20 +- src/libraries/Balance.sol | 20 +- src/libraries/FullMath.sol | 224 ++- src/libraries/SafeCast.sol | 22 +- src/libraries/SafeTransferLib.sol | 177 ++- src/periphery/LendgineRouter.sol | 479 +++--- src/periphery/LiquidityManager.sol | 387 +++-- src/periphery/Payment.sol | 65 + src/periphery/SwapHelper.sol | 182 +-- .../interfaces/IUniswapV2Factory.sol | 22 +- .../UniswapV2/interfaces/IUniswapV2Pair.sol | 113 +- .../UniswapV2/libraries/UniswapV2Library.sol | 132 +- .../interfaces/IUniswapV3Factory.sol | 114 +- .../UniswapV3/interfaces/IUniswapV3Pool.sol | 16 +- .../callback/IUniswapV3SwapCallback.sol | 24 +- .../interfaces/pool/IUniswapV3PoolActions.sol | 180 +-- .../pool/IUniswapV3PoolDerivedState.sol | 65 +- .../interfaces/pool/IUniswapV3PoolEvents.sol | 204 ++- .../pool/IUniswapV3PoolImmutables.sol | 46 +- .../pool/IUniswapV3PoolOwnerActions.sol | 32 +- .../interfaces/pool/IUniswapV3PoolState.sol | 202 +-- .../UniswapV3/libraries/PoolAddress.sol | 73 +- .../UniswapV3/libraries/TickMath.sol | 16 +- src/periphery/base/Payment.sol | 74 - src/periphery/interfaces/IWETH9.sol | 8 +- src/periphery/libraries/LendgineAddress.sol | 48 +- test/AccrueInterestTest.t.sol | 141 +- test/AccruePositionInterestTest.t.sol | 90 +- test/BurnTest.t.sol | 496 +++--- test/CollectTest.t.sol | 118 +- test/DepositTest.t.sol | 447 +++--- test/FactoryTest.t.sol | 194 +-- test/ImmutableStateTest.t.sol | 26 +- test/LendgineRouterTest.t.sol | 1400 +++++++++-------- test/LiquidityManagerTest.t.sol | 1091 +++++++------ test/MintTest.t.sol | 344 ++-- test/PrecisionTest.t.sol | 66 +- test/SwapTest.t.sol | 124 +- test/WithdrawTest.t.sol | 346 ++-- test/utils/CallbackHelper.sol | 108 +- test/utils/TestHelper.sol | 226 ++- test/utils/mocks/MockERC20.sol | 14 +- 62 files changed, 4764 insertions(+), 4752 deletions(-) create mode 100644 src/core/interfaces/IFactory.sol create mode 100644 src/core/interfaces/IImmutableState.sol create mode 100644 src/core/interfaces/ILendgine.sol create mode 100644 src/core/interfaces/IPair.sol create mode 100644 src/periphery/Payment.sol delete mode 100644 src/periphery/base/Payment.sol diff --git a/.prettierrc.yml b/.prettierrc.yml index bd56d83..a1ecdbb 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -5,9 +5,3 @@ singleQuote: false tabWidth: 2 trailingComma: "all" useTabs: false - -overrides: - - files: "*.sol" - options: - compiler: "0.8.17" - tabWidth: 4 diff --git a/.vscode/settings.json b/.vscode/settings.json index 35d160b..d559cc9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,7 @@ // editor settings // "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, - "solidity.formatter": "prettier", // This is the default so it might be missing. + "solidity.formatter": "forge", // This is the default so it might be missing. "[solidity]": { "editor.defaultFormatter": "JuanBlanco.solidity" } diff --git a/README.md b/README.md index e258d56..add22e8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -Find safe transfer library -find fixed point math lib that supports powu \ No newline at end of file +Find safe transfer library find fixed point math lib that supports powu diff --git a/foundry.toml b/foundry.toml index 9e20fd0..83101c2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,6 +16,17 @@ test = "test" fuzz = { runs = 1_000 } verbosity = 4 +[fmt] +bracket_spacing = true +int_types = "long" +line_length = 120 +multiline_func_header = "all" +number_underscore = "thousands" +quote_style = "double" +tab_width = 2 +variable_override_spacing = false +wrap_comments = true + [rpc_endpoints] arbitrum = "${RPC_URL_ARBITRUM}" goerli = "${RPC_URL_GOERLI}" diff --git a/src/core/ERC20.sol b/src/core/ERC20.sol index 32b46f8..b9876c2 100644 --- a/src/core/ERC20.sol +++ b/src/core/ERC20.sol @@ -3,189 +3,188 @@ pragma solidity >=0.8.0; /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. /// @author Kyle Scott (https://github.com/numoen/core/blob/master/src/ERC20.sol) -/// @author Modified from Solmate v6 (https://github.com/transmissions11/solmate/blob/a9e3ea26a2dc73bfa87f0cb189687d029028e0c5/src/tokens/ERC20.sol) +/// @author Modified from Solmate v6 +/// (https://github.com/transmissions11/solmate/blob/a9e3ea26a2dc73bfa87f0cb189687d029028e0c5/src/tokens/ERC20.sol) /// and Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) /// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. abstract contract ERC20 { - /*/////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event Transfer(address indexed from, address indexed to, uint256 amount); + event Transfer(address indexed from, address indexed to, uint256 amount); - event Approval(address indexed owner, address indexed spender, uint256 amount); + event Approval(address indexed owner, address indexed spender, uint256 amount); - /*/////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// METADATA STORAGE //////////////////////////////////////////////////////////////*/ - string public constant name = "Numoen Replicating Derivative"; + string public constant name = "Numoen Replicating Derivative"; - string public constant symbol = "NRD"; + string public constant symbol = "NRD"; - uint8 public constant decimals = 18; + uint8 public constant decimals = 18; - /*/////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// ERC20 STORAGE //////////////////////////////////////////////////////////////*/ - uint256 public totalSupply; + uint256 public totalSupply; - mapping(address => uint256) public balanceOf; + mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; + mapping(address => mapping(address => uint256)) public allowance; - /*/////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// EIP-2612 STORAGE //////////////////////////////////////////////////////////////*/ - bytes32 public constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - uint256 internal immutable INITIAL_CHAIN_ID; + uint256 internal immutable INITIAL_CHAIN_ID; - bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; + bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; - mapping(address => uint256) public nonces; + mapping(address => uint256) public nonces; - /*/////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor() { - INITIAL_CHAIN_ID = block.chainid; - INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); - } + constructor() { + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + } - /*/////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// ERC20 LOGIC //////////////////////////////////////////////////////////////*/ - /// @dev changed visibility to external - function approve(address spender, uint256 amount) external virtual returns (bool) { - allowance[msg.sender][spender] = amount; - - emit Approval(msg.sender, spender, amount); - - return true; - } + /// @dev changed visibility to external + function approve(address spender, uint256 amount) external virtual returns (bool) { + allowance[msg.sender][spender] = amount; - /// @dev changed visibility to external - function transfer(address to, uint256 amount) external virtual returns (bool) { - balanceOf[msg.sender] -= amount; + emit Approval(msg.sender, spender, amount); - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to] += amount; - } + return true; + } - emit Transfer(msg.sender, to, amount); + /// @dev changed visibility to external + function transfer(address to, uint256 amount) external virtual returns (bool) { + balanceOf[msg.sender] -= amount; - return true; + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; } - /// @dev changed visibility to external - function transferFrom( - address from, - address to, - uint256 amount - ) external virtual returns (bool) { - uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + emit Transfer(msg.sender, to, amount); - if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + return true; + } - balanceOf[from] -= amount; + /// @dev changed visibility to external + function transferFrom(address from, address to, uint256 amount) external virtual returns (bool) { + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to] += amount; - } + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; - emit Transfer(from, to, amount); + balanceOf[from] -= amount; - return true; + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; } - /*/////////////////////////////////////////////////////////////// - EIP-2612 LOGIC - //////////////////////////////////////////////////////////////*/ + emit Transfer(from, to, amount); - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external virtual { - require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); - - // Unchecked because the only math done is incrementing - // the owner's nonce which cannot realistically overflow. - unchecked { - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) - ) - ); - - address recoveredAddress = ecrecover(digest, v, r, s); - - require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); - - allowance[recoveredAddress][spender] = value; - } - - emit Approval(owner, spender, value); - } + return true; + } - function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { - return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); - } + /*/////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ - function computeDomainSeparator() internal view virtual returns (bytes32) { - return - keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name)), - keccak256("1"), - block.chainid, - address(this) - ) - ); + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + external + virtual + { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) + ) + ); + + address recoveredAddress = ecrecover(digest, v, r, s); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); + + allowance[recoveredAddress][spender] = value; } - /*/////////////////////////////////////////////////////////////// + emit Approval(owner, spender, value); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// INTERNAL MINT/BURN LOGIC //////////////////////////////////////////////////////////////*/ - function _mint(address to, uint256 amount) internal virtual { - totalSupply += amount; - - // Cannot overflow because the sum of all user - // balances can't exceed the max uint256 value. - unchecked { - balanceOf[to] += amount; - } + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; - emit Transfer(address(0), to, amount); + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; } - function _burn(address from, uint256 amount) internal virtual { - balanceOf[from] -= amount; + emit Transfer(address(0), to, amount); + } - // Cannot underflow because a user's balance - // will never be larger than the total supply. - unchecked { - totalSupply -= amount; - } + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; - emit Transfer(from, address(0), amount); + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; } + + emit Transfer(from, address(0), amount); + } } diff --git a/src/core/Factory.sol b/src/core/Factory.sol index 55ad173..9dfab6c 100644 --- a/src/core/Factory.sol +++ b/src/core/Factory.sol @@ -3,84 +3,87 @@ pragma solidity ^0.8.4; import { Lendgine } from "./Lendgine.sol"; -contract Factory { - /*////////////////////////////////////////////////////////////// +import { IFactory } from "./interfaces/IFactory.sol"; + +contract Factory is IFactory { + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event LendgineCreated( - address indexed token0, - address indexed token1, - uint256 token0Exp, - uint256 token1Exp, - uint256 indexed upperBound, - address lendgine - ); + event LendgineCreated( + address indexed token0, + address indexed token1, + uint256 token0Exp, + uint256 token1Exp, + uint256 indexed upperBound, + address lendgine + ); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - error SameTokenError(); + error SameTokenError(); - error ZeroAddressError(); + error ZeroAddressError(); - error DeployedError(); + error DeployedError(); - error ScaleError(); + error ScaleError(); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// FACTORY STORAGE //////////////////////////////////////////////////////////////*/ - mapping(address => mapping(address => mapping(uint256 => mapping(uint256 => mapping(uint256 => address))))) - public getLendgine; + /// @inheritdoc IFactory + mapping(address => mapping(address => mapping(uint256 => mapping(uint256 => mapping(uint256 => address))))) + public + override getLendgine; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// TEMPORARY DEPLOY STORAGE //////////////////////////////////////////////////////////////*/ - struct Parameters { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - } + struct Parameters { + address token0; + address token1; + uint128 token0Exp; + uint128 token1Exp; + uint256 upperBound; + } - Parameters public parameters; + /// @inheritdoc IFactory + Parameters public override parameters; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// FACTORY LOGIC //////////////////////////////////////////////////////////////*/ - function createLendgine( - address token0, - address token1, - uint8 token0Exp, - uint8 token1Exp, - uint256 upperBound - ) external returns (address lendgine) { - if (token0 == token1) revert SameTokenError(); - if (token0 == address(0) || token1 == address(0)) revert ZeroAddressError(); - if (getLendgine[token0][token1][token0Exp][token1Exp][upperBound] != address(0)) revert DeployedError(); - if (token0Exp > 18 || token0Exp < 6 || token1Exp > 18 || token1Exp < 6) revert ScaleError(); - - parameters = Parameters({ - token0: token0, - token1: token1, - token0Exp: token0Exp, - token1Exp: token1Exp, - upperBound: upperBound - }); - - lendgine = address( - new Lendgine{ salt: keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)) }() - ); - - delete parameters; - - getLendgine[token0][token1][token0Exp][token1Exp][upperBound] = lendgine; - emit LendgineCreated(token0, token1, token0Exp, token1Exp, upperBound, lendgine); - } + /// @inheritdoc IFactory + function createLendgine( + address token0, + address token1, + uint8 token0Exp, + uint8 token1Exp, + uint256 upperBound + ) + external + override + returns (address lendgine) + { + if (token0 == token1) revert SameTokenError(); + if (token0 == address(0) || token1 == address(0)) revert ZeroAddressError(); + if (getLendgine[token0][token1][token0Exp][token1Exp][upperBound] != address(0)) revert DeployedError(); + if (token0Exp > 18 || token0Exp < 6 || token1Exp > 18 || token1Exp < 6) revert ScaleError(); + + parameters = + Parameters({token0: token0, token1: token1, token0Exp: token0Exp, token1Exp: token1Exp, upperBound: upperBound}); + + lendgine = address(new Lendgine{ salt: keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)) }()); + + delete parameters; + + getLendgine[token0][token1][token0Exp][token1Exp][upperBound] = lendgine; + emit LendgineCreated(token0, token1, token0Exp, token1Exp, upperBound, lendgine); + } } diff --git a/src/core/ImmutableState.sol b/src/core/ImmutableState.sol index 352f4ad..f38c186 100644 --- a/src/core/ImmutableState.sol +++ b/src/core/ImmutableState.sol @@ -3,28 +3,36 @@ pragma solidity ^0.8.0; import { Factory } from "./Factory.sol"; -abstract contract ImmutableState { - address public immutable factory; +import { IImmutableState } from "./interfaces/IImmutableState.sol"; - address public immutable token0; +abstract contract ImmutableState is IImmutableState { + /// @inheritdoc IImmutableState + address public immutable factory; - address public immutable token1; + /// @inheritdoc IImmutableState + address public immutable token0; - uint256 public immutable token0Scale; + /// @inheritdoc IImmutableState + address public immutable token1; - uint256 public immutable token1Scale; + /// @inheritdoc IImmutableState + uint256 public immutable token0Scale; - uint256 public immutable upperBound; + /// @inheritdoc IImmutableState + uint256 public immutable token1Scale; - constructor() { - factory = msg.sender; + /// @inheritdoc IImmutableState + uint256 public immutable upperBound; - uint256 _token0Exp; - uint256 _token1Exp; + constructor() { + factory = msg.sender; - (token0, token1, _token0Exp, _token1Exp, upperBound) = Factory(msg.sender).parameters(); + uint128 _token0Exp; + uint128 _token1Exp; - token0Scale = 10**(18 - _token0Exp); - token1Scale = 10**(18 - _token1Exp); - } + (token0, token1, _token0Exp, _token1Exp, upperBound) = Factory(msg.sender).parameters(); + + token0Scale = 10 ** (18 - _token0Exp); + token1Scale = 10 ** (18 - _token1Exp); + } } diff --git a/src/core/JumpRate.sol b/src/core/JumpRate.sol index 876b826..5e734cd 100644 --- a/src/core/JumpRate.sol +++ b/src/core/JumpRate.sol @@ -4,43 +4,41 @@ pragma solidity ^0.8.0; import { IJumpRate } from "./interfaces/IJumpRate.sol"; abstract contract JumpRate is IJumpRate { - uint256 public constant override kink = 0.8 ether; - - uint256 public constant override multiplier = 1.375 ether; - - uint256 public constant override jumpMultiplier = 44.5 ether; - - function getBorrowRate(uint256 borrowedLiquidity, uint256 totalLiquidity) - public - pure - override - returns (uint256 rate) - { - uint256 util = utilizationRate(borrowedLiquidity, totalLiquidity); - - if (util <= kink) { - return (util * multiplier) / 1e18; - } else { - uint256 normalRate = (kink * multiplier) / 1e18; - uint256 excessUtil = util - kink; - return ((excessUtil * jumpMultiplier) / 1e18) + normalRate; - } - } + uint256 public constant override kink = 0.8 ether; - function getSupplyRate(uint256 borrowedLiquidity, uint256 totalLiquidity) - external - pure - override - returns (uint256 rate) - { - uint256 util = utilizationRate(borrowedLiquidity, totalLiquidity); - uint256 borrowRate = getBorrowRate(borrowedLiquidity, totalLiquidity); + uint256 public constant override multiplier = 1.375 ether; - return (borrowRate * util) / 1e18; - } + uint256 public constant override jumpMultiplier = 44.5 ether; + + function getBorrowRate(uint256 borrowedLiquidity, uint256 totalLiquidity) public pure override returns (uint256 rate) { + uint256 util = utilizationRate(borrowedLiquidity, totalLiquidity); - function utilizationRate(uint256 borrowedLiquidity, uint256 totalLiquidity) private pure returns (uint256 rate) { - if (totalLiquidity == 0) return 0; - return (borrowedLiquidity * 1e18) / totalLiquidity; + if (util <= kink) { + return (util * multiplier) / 1e18; + } else { + uint256 normalRate = (kink * multiplier) / 1e18; + uint256 excessUtil = util - kink; + return ((excessUtil * jumpMultiplier) / 1e18) + normalRate; } + } + + function getSupplyRate( + uint256 borrowedLiquidity, + uint256 totalLiquidity + ) + external + pure + override + returns (uint256 rate) + { + uint256 util = utilizationRate(borrowedLiquidity, totalLiquidity); + uint256 borrowRate = getBorrowRate(borrowedLiquidity, totalLiquidity); + + return (borrowRate * util) / 1e18; + } + + function utilizationRate(uint256 borrowedLiquidity, uint256 totalLiquidity) private pure returns (uint256 rate) { + if (totalLiquidity == 0) return 0; + return (borrowedLiquidity * 1e18) / totalLiquidity; + } } diff --git a/src/core/Lendgine.sol b/src/core/Lendgine.sol index cc8847a..12fa50c 100644 --- a/src/core/Lendgine.sol +++ b/src/core/Lendgine.sol @@ -14,246 +14,235 @@ import { SafeTransferLib } from "../libraries/SafeTransferLib.sol"; import { SafeCast } from "../libraries/SafeCast.sol"; contract Lendgine is ERC20, JumpRate, Pair { - using Position for mapping(address => Position.Info); - using Position for Position.Info; + using Position for mapping(address => Position.Info); + using Position for Position.Info; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event Mint(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); + event Mint(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); - event Burn(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); + event Burn(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); - event Deposit(address indexed sender, uint256 size, uint256 liquidity, address indexed to); + event Deposit(address indexed sender, uint256 size, uint256 liquidity, address indexed to); - event Withdraw(address indexed sender, uint256 size, uint256 liquidity, address indexed to); + event Withdraw(address indexed sender, uint256 size, uint256 liquidity, address indexed to); - event AccrueInterest(uint256 timeElapsed, uint256 collateral, uint256 liquidity); + event AccrueInterest(uint256 timeElapsed, uint256 collateral, uint256 liquidity); - event AccruePositionInterest(address indexed owner, uint256 rewardPerPosition); + event AccruePositionInterest(address indexed owner, uint256 rewardPerPosition); - event Collect(address indexed owner, address indexed to, uint256 amount); + event Collect(address indexed owner, address indexed to, uint256 amount); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - error InputError(); + error InputError(); - error CompleteUtilizationError(); + error CompleteUtilizationError(); - error InsufficientInputError(); + error InsufficientInputError(); - error InsufficientPositionError(); + error InsufficientPositionError(); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// LENDGINE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(address => Position.Info) public positions; + mapping(address => Position.Info) public positions; - uint256 public totalPositionSize; + uint256 public totalPositionSize; - uint256 public totalLiquidityBorrowed; + uint256 public totalLiquidityBorrowed; - uint256 public rewardPerPositionStored; + uint256 public rewardPerPositionStored; - /// @dev don't downsize because it takes up the last slot - uint256 public lastUpdate; + /// @dev don't downsize because it takes up the last slot + uint256 public lastUpdate; - function mint( - address to, - uint256 collateral, - bytes calldata data - ) external nonReentrant returns (uint256 shares) { - _accrueInterest(); + function mint(address to, uint256 collateral, bytes calldata data) external nonReentrant returns (uint256 shares) { + _accrueInterest(); - uint256 liquidity = convertCollateralToLiquidity(collateral); - shares = convertLiquidityToShare(liquidity); + uint256 liquidity = convertCollateralToLiquidity(collateral); + shares = convertLiquidityToShare(liquidity); - if (collateral == 0 || liquidity == 0 || shares == 0) revert InputError(); - if (liquidity > totalLiquidity) revert CompleteUtilizationError(); - if (totalSupply > 0 && totalLiquidityBorrowed == 0) revert CompleteUtilizationError(); + if (collateral == 0 || liquidity == 0 || shares == 0) revert InputError(); + if (liquidity > totalLiquidity) revert CompleteUtilizationError(); + // next check is for the case when liquidity is borrowed but then was completely accrued + if (totalSupply > 0 && totalLiquidityBorrowed == 0) revert CompleteUtilizationError(); - // update state - totalLiquidityBorrowed += liquidity; - (uint256 amount0, uint256 amount1) = burn(to, liquidity); - _mint(to, shares); + // update state + totalLiquidityBorrowed += liquidity; + (uint256 amount0, uint256 amount1) = burn(to, liquidity); + _mint(to, shares); - uint256 balanceBefore = Balance.balance(token1); - IMintCallback(msg.sender).mintCallback(collateral, amount0, amount1, liquidity, data); - uint256 balanceAfter = Balance.balance(token1); + uint256 balanceBefore = Balance.balance(token1); + IMintCallback(msg.sender).mintCallback(collateral, amount0, amount1, liquidity, data); + uint256 balanceAfter = Balance.balance(token1); - if (balanceAfter < balanceBefore + collateral) revert InsufficientInputError(); + if (balanceAfter < balanceBefore + collateral) revert InsufficientInputError(); - emit Mint(msg.sender, collateral, shares, liquidity, to); - } + emit Mint(msg.sender, collateral, shares, liquidity, to); + } - function burn(address to, bytes calldata data) external nonReentrant returns (uint256 collateral) { - _accrueInterest(); + function burn(address to, bytes calldata data) external nonReentrant returns (uint256 collateral) { + _accrueInterest(); - // calc shares and liquidity - uint256 shares = balanceOf[address(this)]; - uint256 liquidity = convertShareToLiquidity(shares); - collateral = convertLiquidityToCollateral(liquidity); + // calc shares and liquidity + uint256 shares = balanceOf[address(this)]; + uint256 liquidity = convertShareToLiquidity(shares); + collateral = convertLiquidityToCollateral(liquidity); - if (collateral == 0 || liquidity == 0 || shares == 0) revert InputError(); + if (collateral == 0 || liquidity == 0 || shares == 0) revert InputError(); - // update state - totalLiquidityBorrowed -= liquidity; - _burn(address(this), shares); - SafeTransferLib.safeTransfer(token1, to, collateral); // optimistically transfer - mint(liquidity, data); + // update state + totalLiquidityBorrowed -= liquidity; + _burn(address(this), shares); + SafeTransferLib.safeTransfer(token1, to, collateral); // optimistically transfer + mint(liquidity, data); - emit Burn(msg.sender, collateral, shares, liquidity, to); - } + emit Burn(msg.sender, collateral, shares, liquidity, to); + } - function deposit( - address to, - uint256 liquidity, - bytes calldata data - ) external nonReentrant returns (uint256 size) { - _accrueInterest(); + function deposit(address to, uint256 liquidity, bytes calldata data) external nonReentrant returns (uint256 size) { + _accrueInterest(); - uint256 _totalPositionSize = totalPositionSize; // SLOAD - uint256 totalLiquiditySupplied = totalLiquidity + totalLiquidityBorrowed; + uint256 _totalPositionSize = totalPositionSize; // SLOAD + uint256 totalLiquiditySupplied = totalLiquidity + totalLiquidityBorrowed; - // calculate position - size = Position.convertLiquidityToPosition(liquidity, totalLiquiditySupplied, _totalPositionSize); + // calculate position + size = Position.convertLiquidityToPosition(liquidity, totalLiquiditySupplied, _totalPositionSize); - // validate inputs - if (liquidity == 0 || size == 0) revert InputError(); - // TODO: what if interest has been fully accrued + // validate inputs + if (liquidity == 0 || size == 0) revert InputError(); + // next check is for the case when liquidity is borrowed but then was completely accrued + if (totalLiquiditySupplied == 0 && totalPositionSize > 0) revert CompleteUtilizationError(); - // update state - positions.update(to, SafeCast.toInt256(size), rewardPerPositionStored); - totalPositionSize = _totalPositionSize + size; - mint(liquidity, data); + // update state + positions.update(to, SafeCast.toInt256(size), rewardPerPositionStored); + totalPositionSize = _totalPositionSize + size; + mint(liquidity, data); - emit Deposit(msg.sender, size, liquidity, to); - } + emit Deposit(msg.sender, size, liquidity, to); + } - function withdraw(address to, uint256 size) - external - nonReentrant - returns ( - uint256 amount0, - uint256 amount1, - uint256 liquidity - ) - { - _accrueInterest(); - - uint256 _totalPositionSize = totalPositionSize; // SLOAD - uint256 _totalLiquidity = totalLiquidity; // SLOAD - uint256 totalLiquiditySupplied = _totalLiquidity + totalLiquidityBorrowed; - - // read position - Position.Info memory positionInfo = positions.get(msg.sender); - liquidity = Position.convertPositionToLiquidity(size, totalLiquiditySupplied, _totalPositionSize); - - // validate inputs - if (liquidity == 0 || size == 0) revert InputError(); - - // check position - if (size > positionInfo.size) revert InsufficientPositionError(); - if (liquidity > _totalLiquidity) revert CompleteUtilizationError(); - - // update state - positions.update(msg.sender, -SafeCast.toInt256(size), rewardPerPositionStored); - totalPositionSize -= size; - (amount0, amount1) = burn(to, liquidity); - - emit Withdraw(msg.sender, size, liquidity, to); - } + function withdraw( + address to, + uint256 size + ) + external + nonReentrant + returns (uint256 amount0, uint256 amount1, uint256 liquidity) + { + _accrueInterest(); - function accrueInterest() external nonReentrant { - _accrueInterest(); - } + uint256 _totalPositionSize = totalPositionSize; // SLOAD + uint256 _totalLiquidity = totalLiquidity; // SLOAD + uint256 totalLiquiditySupplied = _totalLiquidity + totalLiquidityBorrowed; - function accruePositionInterest() external nonReentrant { - _accrueInterest(); - _accruePositionInterest(msg.sender); - } + // read position + Position.Info memory positionInfo = positions[msg.sender]; + liquidity = Position.convertPositionToLiquidity(size, totalLiquiditySupplied, _totalPositionSize); + + // validate inputs + if (liquidity == 0 || size == 0) revert InputError(); + + // check position + if (size > positionInfo.size) revert InsufficientPositionError(); + if (liquidity > _totalLiquidity) revert CompleteUtilizationError(); + + // update state + positions.update(msg.sender, -SafeCast.toInt256(size), rewardPerPositionStored); + totalPositionSize -= size; + (amount0, amount1) = burn(to, liquidity); + + emit Withdraw(msg.sender, size, liquidity, to); + } - function collect(address to, uint256 collateralRequested) external nonReentrant returns (uint256 collateral) { - Position.Info storage position = positions.get(msg.sender); - uint256 tokensOwed = position.tokensOwed; // SLOAD + function accrueInterest() external nonReentrant { + _accrueInterest(); + } - collateral = collateralRequested > tokensOwed ? tokensOwed : collateralRequested; + function accruePositionInterest() external nonReentrant { + _accrueInterest(); + _accruePositionInterest(msg.sender); + } - if (collateral > 0) { - position.tokensOwed = tokensOwed - collateral; - SafeTransferLib.safeTransfer(token1, to, collateral); - } + function collect(address to, uint256 collateralRequested) external nonReentrant returns (uint256 collateral) { + Position.Info storage position = positions[msg.sender]; // SLOAD + uint256 tokensOwed = position.tokensOwed; - emit Collect(msg.sender, to, collateral); + collateral = collateralRequested > tokensOwed ? tokensOwed : collateralRequested; + + if (collateral > 0) { + position.tokensOwed = tokensOwed - collateral; // SSTORE + SafeTransferLib.safeTransfer(token1, to, collateral); } - /*////////////////////////////////////////////////////////////// + emit Collect(msg.sender, to, collateral); + } + + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - function convertLiquidityToShare(uint256 liquidity) public view returns (uint256) { - uint256 _totalLiquidityBorrowed = totalLiquidityBorrowed; // SLOAD - return - _totalLiquidityBorrowed == 0 ? liquidity : FullMath.mulDiv(liquidity, totalSupply, _totalLiquidityBorrowed); - } + function convertLiquidityToShare(uint256 liquidity) public view returns (uint256) { + uint256 _totalLiquidityBorrowed = totalLiquidityBorrowed; // SLOAD + return _totalLiquidityBorrowed == 0 ? liquidity : FullMath.mulDiv(liquidity, totalSupply, _totalLiquidityBorrowed); + } - function convertShareToLiquidity(uint256 shares) public view returns (uint256) { - return FullMath.mulDiv(totalLiquidityBorrowed, shares, totalSupply); - } + function convertShareToLiquidity(uint256 shares) public view returns (uint256) { + return FullMath.mulDiv(totalLiquidityBorrowed, shares, totalSupply); + } - function convertCollateralToLiquidity(uint256 collateral) public view returns (uint256) { - return FullMath.mulDiv(collateral * token1Scale, 1e18, 2 * upperBound); - } + function convertCollateralToLiquidity(uint256 collateral) public view returns (uint256) { + return FullMath.mulDiv(collateral * token1Scale, 1e18, 2 * upperBound); + } - function convertLiquidityToCollateral(uint256 liquidity) public view returns (uint256) { - return FullMath.mulDiv(liquidity, 2 * upperBound, 1e18) / token1Scale; - } + function convertLiquidityToCollateral(uint256 liquidity) public view returns (uint256) { + return FullMath.mulDiv(liquidity, 2 * upperBound, 1e18) / token1Scale; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// INTERNAL INTEREST LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Helper function for accruing lendgine interest - function _accrueInterest() private { - if (totalSupply == 0 || totalLiquidityBorrowed == 0) { - lastUpdate = block.timestamp; - return; - } + /// @notice Helper function for accruing lendgine interest + function _accrueInterest() private { + if (totalSupply == 0 || totalLiquidityBorrowed == 0) { + lastUpdate = block.timestamp; + return; + } - uint256 timeElapsed = block.timestamp - lastUpdate; - if (timeElapsed == 0) return; + uint256 timeElapsed = block.timestamp - lastUpdate; + if (timeElapsed == 0) return; - uint256 _totalLiquidityBorrowed = totalLiquidityBorrowed; // SLOAD - uint256 totalLiquiditySupplied = totalLiquidity + _totalLiquidityBorrowed; // SLOAD + uint256 _totalLiquidityBorrowed = totalLiquidityBorrowed; // SLOAD + uint256 totalLiquiditySupplied = totalLiquidity + _totalLiquidityBorrowed; // SLOAD - uint256 borrowRate = getBorrowRate(_totalLiquidityBorrowed, totalLiquiditySupplied); + uint256 borrowRate = getBorrowRate(_totalLiquidityBorrowed, totalLiquiditySupplied); - uint256 dilutionLPRequested = (FullMath.mulDiv(borrowRate, _totalLiquidityBorrowed, 1e18) * timeElapsed) / - 365 days; - uint256 dilutionLP = dilutionLPRequested > _totalLiquidityBorrowed - ? _totalLiquidityBorrowed - : dilutionLPRequested; - uint256 dilutionSpeculative = convertLiquidityToCollateral(dilutionLP); + uint256 dilutionLPRequested = (FullMath.mulDiv(borrowRate, _totalLiquidityBorrowed, 1e18) * timeElapsed) / 365 days; + uint256 dilutionLP = dilutionLPRequested > _totalLiquidityBorrowed ? _totalLiquidityBorrowed : dilutionLPRequested; + uint256 dilutionSpeculative = convertLiquidityToCollateral(dilutionLP); - totalLiquidityBorrowed = _totalLiquidityBorrowed - dilutionLP; - rewardPerPositionStored += FullMath.mulDiv(dilutionSpeculative, 1e18, totalPositionSize); - lastUpdate = block.timestamp; + totalLiquidityBorrowed = _totalLiquidityBorrowed - dilutionLP; + rewardPerPositionStored += FullMath.mulDiv(dilutionSpeculative, 1e18, totalPositionSize); + lastUpdate = block.timestamp; - emit AccrueInterest(timeElapsed, dilutionSpeculative, dilutionLP); - } + emit AccrueInterest(timeElapsed, dilutionSpeculative, dilutionLP); + } - /// @notice Helper function for accruing interest to a position - /// @dev Assume the global interest is up to date - /// @param owner The address that this position belongs to - function _accruePositionInterest(address owner) private { - uint256 _rewardPerPositionStored = rewardPerPositionStored; // SLOAD + /// @notice Helper function for accruing interest to a position + /// @dev Assume the global interest is up to date + /// @param owner The address that this position belongs to + function _accruePositionInterest(address owner) private { + uint256 _rewardPerPositionStored = rewardPerPositionStored; // SLOAD - positions.update(owner, 0, _rewardPerPositionStored); + positions.update(owner, 0, _rewardPerPositionStored); - emit AccruePositionInterest(owner, _rewardPerPositionStored); - } + emit AccruePositionInterest(owner, _rewardPerPositionStored); + } } diff --git a/src/core/Pair.sol b/src/core/Pair.sol index 51ef395..ba4fa9e 100644 --- a/src/core/Pair.sol +++ b/src/core/Pair.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.4; import { ImmutableState } from "./ImmutableState.sol"; import { ReentrancyGuard } from "./ReentrancyGuard.sol"; +import { IPair } from "./interfaces/IPair.sol"; import { IPairMintCallback } from "./interfaces/callback/IPairMintCallback.sol"; import { ISwapCallback } from "./interfaces/callback/ISwapCallback.sol"; @@ -12,130 +13,128 @@ import { FullMath } from "../libraries/FullMath.sol"; import { SafeCast } from "../libraries/SafeCast.sol"; import { SafeTransferLib } from "../libraries/SafeTransferLib.sol"; -abstract contract Pair is ImmutableState, ReentrancyGuard { - /*////////////////////////////////////////////////////////////// +abstract contract Pair is ImmutableState, ReentrancyGuard, IPair { + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event Mint(uint256 amount0In, uint256 amount1In, uint256 liquidity); + event Mint(uint256 amount0In, uint256 amount1In, uint256 liquidity); - event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to); + event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to); - event Swap(uint256 amount0Out, uint256 amount1Out, uint256 amount0In, uint256 amount1In, address indexed to); + event Swap(uint256 amount0Out, uint256 amount1Out, uint256 amount0In, uint256 amount1In, address indexed to); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - error InvariantError(); + error InvariantError(); - error InsufficientOutputError(); + error InsufficientOutputError(); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - uint120 public reserve0; + /// @inheritdoc IPair + uint120 public override reserve0; - uint120 public reserve1; + /// @inheritdoc IPair + uint120 public override reserve1; - uint256 public totalLiquidity; + /// @inheritdoc IPair + uint256 public override totalLiquidity; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// PAIR LOGIC //////////////////////////////////////////////////////////////*/ - function invariant( - uint256 amount0, - uint256 amount1, - uint256 liquidity - ) public view returns (bool) { - if (liquidity == 0) return (amount0 == 0 && amount1 == 0); + /// @inheritdoc IPair + function invariant(uint256 amount0, uint256 amount1, uint256 liquidity) public view override returns (bool) { + if (liquidity == 0) return (amount0 == 0 && amount1 == 0); - uint256 scale0 = FullMath.mulDiv(amount0, 1e18, liquidity) * token0Scale; - uint256 scale1 = FullMath.mulDiv(amount1, 1e18, liquidity) * token1Scale; + uint256 scale0 = FullMath.mulDiv(amount0, 1e18, liquidity) * token0Scale; + uint256 scale1 = FullMath.mulDiv(amount1, 1e18, liquidity) * token1Scale; - if (scale1 > 2 * upperBound) revert InvariantError(); + if (scale1 > 2 * upperBound) revert InvariantError(); - uint256 a = scale0 * 1e18; - uint256 b = scale1 * upperBound; - uint256 c = (scale1 * scale1) / 4; - uint256 d = upperBound * upperBound; + uint256 a = scale0 * 1e18; + uint256 b = scale1 * upperBound; + uint256 c = (scale1 * scale1) / 4; + uint256 d = upperBound * upperBound; - return a + b >= c + d; - } - - /// @dev assumes liquidity is non-zero - function mint(uint256 liquidity, bytes calldata data) internal { - uint120 _reserve0 = reserve0; // SLOAD - uint120 _reserve1 = reserve1; // SLOAD - uint256 _totalLiquidity = totalLiquidity; // SLOAD - - uint256 balance0Before = Balance.balance(token0); - uint256 balance1Before = Balance.balance(token1); - IPairMintCallback(msg.sender).pairMintCallback(liquidity, data); - uint256 amount0In = Balance.balance(token0) - balance0Before; - uint256 amount1In = Balance.balance(token1) - balance1Before; + return a + b >= c + d; + } - if (!invariant(_reserve0 + amount0In, _reserve1 + amount1In, _totalLiquidity + liquidity)) - revert InvariantError(); + /// @dev assumes liquidity is non-zero + function mint(uint256 liquidity, bytes calldata data) internal { + uint120 _reserve0 = reserve0; // SLOAD + uint120 _reserve1 = reserve1; // SLOAD + uint256 _totalLiquidity = totalLiquidity; // SLOAD - reserve0 = _reserve0 + SafeCast.toUint120(amount0In); // SSTORE - reserve1 = _reserve1 + SafeCast.toUint120(amount1In); // SSTORE - totalLiquidity = _totalLiquidity + liquidity; // SSTORE + uint256 balance0Before = Balance.balance(token0); + uint256 balance1Before = Balance.balance(token1); + IPairMintCallback(msg.sender).pairMintCallback(liquidity, data); + uint256 amount0In = Balance.balance(token0) - balance0Before; + uint256 amount1In = Balance.balance(token1) - balance1Before; - emit Mint(amount0In, amount1In, liquidity); + if (!invariant(_reserve0 + amount0In, _reserve1 + amount1In, _totalLiquidity + liquidity)) { + revert InvariantError(); } - /// @dev assumes liquidity is non-zero - function burn(address to, uint256 liquidity) internal returns (uint256 amount0, uint256 amount1) { - uint120 _reserve0 = reserve0; // SLOAD - uint120 _reserve1 = reserve1; // SLOAD - uint256 _totalLiquidity = totalLiquidity; // SLOAD + reserve0 = _reserve0 + SafeCast.toUint120(amount0In); // SSTORE + reserve1 = _reserve1 + SafeCast.toUint120(amount1In); // SSTORE + totalLiquidity = _totalLiquidity + liquidity; // SSTORE - amount0 = FullMath.mulDiv(_reserve0, liquidity, _totalLiquidity); - amount1 = FullMath.mulDiv(_reserve1, liquidity, _totalLiquidity); - if (amount0 == 0 && amount1 == 0) revert InsufficientOutputError(); + emit Mint(amount0In, amount1In, liquidity); + } - if (amount0 > 0) SafeTransferLib.safeTransfer(token0, to, amount0); - if (amount1 > 0) SafeTransferLib.safeTransfer(token1, to, amount1); + /// @dev assumes liquidity is non-zero + function burn(address to, uint256 liquidity) internal returns (uint256 amount0, uint256 amount1) { + uint120 _reserve0 = reserve0; // SLOAD + uint120 _reserve1 = reserve1; // SLOAD + uint256 _totalLiquidity = totalLiquidity; // SLOAD - // Extra check of the invariant - if (!invariant(_reserve0 - amount0, _reserve1 - amount1, _totalLiquidity - liquidity)) revert InvariantError(); + amount0 = FullMath.mulDiv(_reserve0, liquidity, _totalLiquidity); + amount1 = FullMath.mulDiv(_reserve1, liquidity, _totalLiquidity); + if (amount0 == 0 && amount1 == 0) revert InsufficientOutputError(); - reserve0 = _reserve0 - SafeCast.toUint120(amount0); // SSTORE - reserve1 = _reserve1 - SafeCast.toUint120(amount1); // SSTORE - totalLiquidity = _totalLiquidity - liquidity; // SSTORE + if (amount0 > 0) SafeTransferLib.safeTransfer(token0, to, amount0); + if (amount1 > 0) SafeTransferLib.safeTransfer(token1, to, amount1); - emit Burn(amount0, amount1, liquidity, to); - } + // Extra check of the invariant + if (!invariant(_reserve0 - amount0, _reserve1 - amount1, _totalLiquidity - liquidity)) revert InvariantError(); - function swap( - address to, - uint256 amount0Out, - uint256 amount1Out, - bytes calldata data - ) external nonReentrant { - if (amount0Out == 0 && amount1Out == 0) revert InsufficientOutputError(); + reserve0 = _reserve0 - SafeCast.toUint120(amount0); // SSTORE + reserve1 = _reserve1 - SafeCast.toUint120(amount1); // SSTORE + totalLiquidity = _totalLiquidity - liquidity; // SSTORE - uint120 _reserve0 = reserve0; // SLOAD - uint120 _reserve1 = reserve1; // SLOAD + emit Burn(amount0, amount1, liquidity, to); + } - if (amount0Out > 0) SafeTransferLib.safeTransfer(token0, to, amount0Out); - if (amount1Out > 0) SafeTransferLib.safeTransfer(token1, to, amount1Out); + /// @inheritdoc IPair + function swap(address to, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external override nonReentrant { + if (amount0Out == 0 && amount1Out == 0) revert InsufficientOutputError(); - uint256 balance0Before = Balance.balance(token0); - uint256 balance1Before = Balance.balance(token1); - ISwapCallback(msg.sender).swapCallback(amount0Out, amount1Out, data); - uint256 amount0In = Balance.balance(token0) - balance0Before; - uint256 amount1In = Balance.balance(token1) - balance1Before; + uint120 _reserve0 = reserve0; // SLOAD + uint120 _reserve1 = reserve1; // SLOAD - if (!invariant(_reserve0 + amount0In - amount0Out, _reserve1 + amount1In - amount1Out, totalLiquidity)) - revert InvariantError(); + if (amount0Out > 0) SafeTransferLib.safeTransfer(token0, to, amount0Out); + if (amount1Out > 0) SafeTransferLib.safeTransfer(token1, to, amount1Out); - reserve0 = _reserve0 + SafeCast.toUint120(amount0In) - SafeCast.toUint120(amount0Out); // SSTORE - reserve1 = _reserve1 + SafeCast.toUint120(amount1In) - SafeCast.toUint120(amount1Out); // SSTORE + uint256 balance0Before = Balance.balance(token0); + uint256 balance1Before = Balance.balance(token1); + ISwapCallback(msg.sender).swapCallback(amount0Out, amount1Out, data); + uint256 amount0In = Balance.balance(token0) - balance0Before; + uint256 amount1In = Balance.balance(token1) - balance1Before; - emit Swap(amount0Out, amount1Out, amount0In, amount1In, to); + if (!invariant(_reserve0 + amount0In - amount0Out, _reserve1 + amount1In - amount1Out, totalLiquidity)) { + revert InvariantError(); } + + reserve0 = _reserve0 + SafeCast.toUint120(amount0In) - SafeCast.toUint120(amount0Out); // SSTORE + reserve1 = _reserve1 + SafeCast.toUint120(amount1In) - SafeCast.toUint120(amount1Out); // SSTORE + + emit Swap(amount0Out, amount1Out, amount0In, amount1In, to); + } } diff --git a/src/core/ReentrancyGuard.sol b/src/core/ReentrancyGuard.sol index 5c4f021..dc463ef 100644 --- a/src/core/ReentrancyGuard.sol +++ b/src/core/ReentrancyGuard.sol @@ -6,15 +6,15 @@ pragma solidity >=0.8.0; /// @author Modified from OpenZeppelin /// (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) abstract contract ReentrancyGuard { - uint16 private locked = 1; + uint16 private locked = 1; - modifier nonReentrant() virtual { - require(locked == 1, "REENTRANCY"); + modifier nonReentrant() virtual { + require(locked == 1, "REENTRANCY"); - locked = 2; + locked = 2; - _; + _; - locked = 1; - } + locked = 1; + } } diff --git a/src/core/interfaces/IFactory.sol b/src/core/interfaces/IFactory.sol new file mode 100644 index 0000000..c5f54a2 --- /dev/null +++ b/src/core/interfaces/IFactory.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.5.0; + +/// @notice Manages the recording and creation of Numoen markets +/// @author Kyle Scott (https://github.com/numoen/contracts-mono/blob/master/src/Factory.sol) +/// @author Modified from Uniswap (https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol) +/// and Primitive (https://github.com/primitivefinance/rmm-core/blob/main/contracts/PrimitiveFactory.sol) +interface IFactory { + /// @notice Returns the lendgine address for a given pair of tokens and upper bound + /// @dev returns address 0 if it doesn't exist + function getLendgine( + address token0, + address token1, + uint256 token0Exp, + uint256 token1Exp, + uint256 upperBound + ) + external + view + returns (address lendgine); + + /// @notice Get the parameters to be used in constructing the lendgine, set + /// transiently during lendgine creation + /// @dev Called by the immutable state constructor to fetch the parameters of the lendgine + function parameters() + external + view + returns (address token0, address token1, uint128 token0Exp, uint128 token1Exp, uint256 upperBound); + + /// @notice Deploys a lendgine contract by transiently setting the parameters storage slots + /// and clearing it after the lendgine has been deployed + function createLendgine( + address token0, + address token1, + uint8 token0Exp, + uint8 token1Exp, + uint256 upperBound + ) + external + returns (address); +} diff --git a/src/core/interfaces/IImmutableState.sol b/src/core/interfaces/IImmutableState.sol new file mode 100644 index 0000000..d0b2267 --- /dev/null +++ b/src/core/interfaces/IImmutableState.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.5.0; + +/// @notice Immutable state interface +/// @author Kyle Scott (kyle@numoen.com) +interface IImmutableState { + /// @notice The contract that deployed the lendgine + function factory() external view returns (address); + + /// @notice The "numeraire" or "base" token in the pair + function token0() external view returns (address); + + /// @notice The "risky" or "speculative" token in the pair + function token1() external view returns (address); + + /// @notice Scale required to make token 0 18 decimals + function token0Scale() external view returns (uint256); + + /// @notice Scale required to make token 1 18 decimals + function token1Scale() external view returns (uint256); + + /// @notice Maximum exchange rate (token0/token1) + function upperBound() external view returns (uint256); +} diff --git a/src/core/interfaces/IJumpRate.sol b/src/core/interfaces/IJumpRate.sol index 6fcfc5d..d5ed344 100644 --- a/src/core/interfaces/IJumpRate.sol +++ b/src/core/interfaces/IJumpRate.sol @@ -6,13 +6,13 @@ pragma solidity >=0.5.0; /// @author Modified from Compound /// (https://github.com/compound-finance/compound-protocol/blob/master/contracts/JumpRateModel.sol) interface IJumpRate { - function kink() external view returns (uint256 kink); + function kink() external view returns (uint256 kink); - function multiplier() external view returns (uint256 multiplier); + function multiplier() external view returns (uint256 multiplier); - function jumpMultiplier() external view returns (uint256 jumpMultiplier); + function jumpMultiplier() external view returns (uint256 jumpMultiplier); - function getBorrowRate(uint256 borrowedLiquidity, uint256 totalLiquidity) external view returns (uint256 rate); + function getBorrowRate(uint256 borrowedLiquidity, uint256 totalLiquidity) external view returns (uint256 rate); - function getSupplyRate(uint256 borrowedLiquidity, uint256 totalLiquidity) external view returns (uint256 rate); + function getSupplyRate(uint256 borrowedLiquidity, uint256 totalLiquidity) external view returns (uint256 rate); } diff --git a/src/core/interfaces/ILendgine.sol b/src/core/interfaces/ILendgine.sol new file mode 100644 index 0000000..985ea59 --- /dev/null +++ b/src/core/interfaces/ILendgine.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.5.0; + +import { IPair } from "./IPair.sol"; + +interface ILendgine is IPair { + /// @notice + function positions(address) external view returns (uint256, uint256, uint256); + + function totalPositionSize() external view returns (uint256); + + function totalLiquidityBorrowed() external view returns (uint256); + + function rewardPerPositionStored() external view returns (uint256); + + function lastUpdate() external view returns (uint256); + + function mint(address to, uint256 collateral, bytes calldata data) external returns (uint256 shares); + + function burn(address to, bytes calldata data) external returns (uint256 collateral); + + function deposit(address to, uint256 liquidity, bytes calldata data) external returns (uint256 size); + + function withdraw(address to, uint256 size) external returns (uint256 amount0, uint256 amount1, uint256 liquidity); + + function accrueInterest() external; + + function accruePositionInterest() external; + + function collect(address to, uint256 collateralRequested) external returns (uint256 collateral); + + // function convertLiquidityToShare(uint256 liquidity) public view returns (uint256) { + // uint256 _totalLiquidityBorrowed = totalLiquidityBorrowed; // SLOAD + // return + // _totalLiquidityBorrowed == 0 ? liquidity : FullMath.mulDiv(liquidity, totalSupply, + // _totalLiquidityBorrowed); + // } + + // function convertShareToLiquidity(uint256 shares) public view returns (uint256) { + // return FullMath.mulDiv(totalLiquidityBorrowed, shares, totalSupply); + // } + + // function convertCollateralToLiquidity(uint256 collateral) public view returns (uint256) { + // return FullMath.mulDiv(collateral * token1Scale, 1e18, 2 * upperBound); + // } + + // function convertLiquidityToCollateral(uint256 liquidity) public view returns (uint256) { + // return FullMath.mulDiv(liquidity, 2 * upperBound, 1e18) / token1Scale; + // } +} diff --git a/src/core/interfaces/IPair.sol b/src/core/interfaces/IPair.sol new file mode 100644 index 0000000..e3454e3 --- /dev/null +++ b/src/core/interfaces/IPair.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.5.0; + +import { IImmutableState } from "./IImmutableState.sol"; + +/// @notice AMM implementing the capped power invariant +/// @author Kyle Scott (kyle@numoen.com) +interface IPair is IImmutableState { + /// @notice The amount of token0 in the pair + function reserve0() external view returns (uint120); + + /// @notice The amount of token1 in the pair + function reserve1() external view returns (uint120); + + /// @notice The total amount of liquidity shares in the pair + function totalLiquidity() external view returns (uint256); + + /// @notice The implementation of the capped power invariant + /// @return valid True if the invariant is satisfied + function invariant(uint256 amount0, uint256 amount1, uint256 liquidity) external view returns (bool); + + /// @notice Exchange between token0 and token1, either accepts or rejects the proposed trade + /// @param data The data to be passed through to the callback + /// @dev A callback is invoked on the caller + function swap(address to, uint256 amount0Out, uint256 amount1Out, bytes calldata data) external; +} diff --git a/src/core/interfaces/callback/IMintCallback.sol b/src/core/interfaces/callback/IMintCallback.sol index 826bba2..ee2fb06 100644 --- a/src/core/interfaces/callback/IMintCallback.sol +++ b/src/core/interfaces/callback/IMintCallback.sol @@ -2,15 +2,16 @@ pragma solidity >=0.5.0; interface IMintCallback { - /// @notice Called to `msg.sender` after executing a mint via Lendgine - /// @dev In the implementation you must pay the speculative tokens owed for the mint. - /// The caller of this method must be checked to be a Lendgine deployed by the canonical Factory. - /// @param data Any data passed through by the caller via the Mint call - function mintCallback( - uint256 collateral, - uint256 amount0, - uint256 amount1, - uint256 liquidity, - bytes calldata data - ) external; + /// @notice Called to `msg.sender` after executing a mint via Lendgine + /// @dev In the implementation you must pay the speculative tokens owed for the mint. + /// The caller of this method must be checked to be a Lendgine deployed by the canonical Factory. + /// @param data Any data passed through by the caller via the Mint call + function mintCallback( + uint256 collateral, + uint256 amount0, + uint256 amount1, + uint256 liquidity, + bytes calldata data + ) + external; } diff --git a/src/core/interfaces/callback/IPairMintCallback.sol b/src/core/interfaces/callback/IPairMintCallback.sol index 0cd5a93..5a5c135 100644 --- a/src/core/interfaces/callback/IPairMintCallback.sol +++ b/src/core/interfaces/callback/IPairMintCallback.sol @@ -2,9 +2,9 @@ pragma solidity >=0.5.0; interface IPairMintCallback { - /// @notice Called to `msg.sender` after executing a mint via Pair - /// @dev In the implementation you must pay the pool tokens owed for the mint. - /// The caller of this method must be checked to be a Pair deployed by the canonical Factory. - /// @param data Any data passed through by the caller via the Mint call - function pairMintCallback(uint256 liquidity, bytes calldata data) external; + /// @notice Called to `msg.sender` after executing a mint via Pair + /// @dev In the implementation you must pay the pool tokens owed for the mint. + /// The caller of this method must be checked to be a Pair deployed by the canonical Factory. + /// @param data Any data passed through by the caller via the Mint call + function pairMintCallback(uint256 liquidity, bytes calldata data) external; } diff --git a/src/core/interfaces/callback/ISwapCallback.sol b/src/core/interfaces/callback/ISwapCallback.sol index 271915c..0fe1e62 100644 --- a/src/core/interfaces/callback/ISwapCallback.sol +++ b/src/core/interfaces/callback/ISwapCallback.sol @@ -2,13 +2,9 @@ pragma solidity >=0.5.0; interface ISwapCallback { - /// @notice Called to `msg.sender` after executing a swap via Pair - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// The caller of this method must be checked to be a Pair deployed by the canonical Factory. - /// @param data Any data passed through by the caller via the Swap call - function swapCallback( - uint256 amount0Out, - uint256 amount1Out, - bytes calldata data - ) external; + /// @notice Called to `msg.sender` after executing a swap via Pair + /// @dev In the implementation you must pay the pool tokens owed for the swap. + /// The caller of this method must be checked to be a Pair deployed by the canonical Factory. + /// @param data Any data passed through by the caller via the Swap call + function swapCallback(uint256 amount0Out, uint256 amount1Out, bytes calldata data) external; } diff --git a/src/core/libraries/Position.sol b/src/core/libraries/Position.sol index 4c55b5c..11e672d 100644 --- a/src/core/libraries/Position.sol +++ b/src/core/libraries/Position.sol @@ -8,94 +8,90 @@ import { FullMath } from "../../libraries/FullMath.sol"; /// @author Kyle Scott (https://github.com/Numoen/core/blob/master/src/libraries/Position.sol) /// @author Modified from Uniswap (https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Position.sol) library Position { - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - error NoPositionError(); + /// @notice Error for trying to update a position with no size + error NoPositionError(); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// POSITION STRUCT //////////////////////////////////////////////////////////////*/ - /** - * @param size The size of the position - * @param rewardPerPositionPaid The reward per unit of size as of the last update to position or tokensOwed - * @param tokensOwed The fees owed to the position owner in `speculative` tokens - */ - struct Info { - uint256 size; - uint256 rewardPerPositionPaid; - uint256 tokensOwed; - } + /** + * @param size The size of the position + * @param rewardPerPositionPaid The reward per unit of size as of the last update to position or tokensOwed + * @param tokensOwed The fees owed to the position owner in `speculative` tokens + */ + struct Info { + uint256 size; + uint256 rewardPerPositionPaid; + uint256 tokensOwed; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// POSITION LOGIC //////////////////////////////////////////////////////////////*/ - /// @notice Helper function for determining the amount of tokens owed to a position - /// @dev Assumes the global interest is up to date - function update( - mapping(address => Position.Info) storage self, - address owner, - int256 sizeDelta, - uint256 rewardPerPosition - ) internal { - Position.Info storage positionInfo = self[owner]; - Position.Info memory _positionInfo = positionInfo; - - uint256 tokensOwed; - if (_positionInfo.size > 0) { - tokensOwed = newTokensOwed(_positionInfo, rewardPerPosition); - } + /// @notice Helper function for updating a position by increasing/decreasing its size or accruing interest + function update( + mapping(address => Position.Info) storage self, + address owner, + int256 sizeDelta, + uint256 rewardPerPosition + ) + internal + { + Position.Info storage positionInfo = self[owner]; + Position.Info memory _positionInfo = positionInfo; - uint256 sizeNext; - if (sizeDelta == 0) { - if (_positionInfo.size == 0) revert NoPositionError(); - sizeNext = _positionInfo.size; - } else { - sizeNext = PositionMath.addDelta(_positionInfo.size, sizeDelta); - } - - if (sizeDelta != 0) positionInfo.size = sizeNext; - positionInfo.rewardPerPositionPaid = rewardPerPosition; - if (tokensOwed > 0) positionInfo.tokensOwed = _positionInfo.tokensOwed + tokensOwed; + uint256 tokensOwed; + if (_positionInfo.size > 0) { + tokensOwed = newTokensOwed(_positionInfo, rewardPerPosition); } - /// @notice Helper function for determining the amount of tokens owed to a position - function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) { - return FullMath.mulDiv(position.size, rewardPerPosition - position.rewardPerPositionPaid, 1 ether); + uint256 sizeNext; + if (sizeDelta == 0) { + if (_positionInfo.size == 0) revert NoPositionError(); + sizeNext = _positionInfo.size; + } else { + sizeNext = PositionMath.addDelta(_positionInfo.size, sizeDelta); } - function convertLiquidityToPosition( - uint256 liquidity, - uint256 totalLiquiditySupplied, - uint256 totalPositionSize - ) internal pure returns (uint256) { - return - totalLiquiditySupplied == 0 - ? liquidity - : FullMath.mulDiv(liquidity, totalPositionSize, totalLiquiditySupplied); - } + if (sizeDelta != 0) positionInfo.size = sizeNext; + positionInfo.rewardPerPositionPaid = rewardPerPosition; + if (tokensOwed > 0) positionInfo.tokensOwed = _positionInfo.tokensOwed + tokensOwed; + } - function convertPositionToLiquidity( - uint256 position, - uint256 totalLiquiditySupplied, - uint256 totalPositionSize - ) internal pure returns (uint256) { - return FullMath.mulDiv(position, totalLiquiditySupplied, totalPositionSize); - } + /// @notice Helper function for determining the amount of tokens owed to a position + /// @param rewardPerPosition The global accrued interest + function newTokensOwed(Position.Info memory position, uint256 rewardPerPosition) internal pure returns (uint256) { + return FullMath.mulDiv(position.size, rewardPerPosition - position.rewardPerPositionPaid, 1 ether); + } - /*////////////////////////////////////////////////////////////// - POSITION VIEW - //////////////////////////////////////////////////////////////*/ + function convertLiquidityToPosition( + uint256 liquidity, + uint256 totalLiquiditySupplied, + uint256 totalPositionSize + ) + internal + pure + returns (uint256) + { + return + totalLiquiditySupplied == 0 ? liquidity : FullMath.mulDiv(liquidity, totalPositionSize, totalLiquiditySupplied); + } - /// @notice Return a position identified by its owner and tick - function get(mapping(address => Info) storage self, address owner) - internal - view - returns (Position.Info storage position) - { - position = self[owner]; - } + function convertPositionToLiquidity( + uint256 position, + uint256 totalLiquiditySupplied, + uint256 totalPositionSize + ) + internal + pure + returns (uint256) + { + return FullMath.mulDiv(position, totalLiquiditySupplied, totalPositionSize); + } } diff --git a/src/core/libraries/PositionMath.sol b/src/core/libraries/PositionMath.sol index 0ede692..f25bbac 100644 --- a/src/core/libraries/PositionMath.sol +++ b/src/core/libraries/PositionMath.sol @@ -5,15 +5,15 @@ pragma solidity >=0.5.0; /// @author Kyle Scott (https://github.com/Numoen/core/blob/master/src/libraries/PositionMath.sol) /// @author Modified from Uniswap (https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LiquidityMath.sol) library PositionMath { - /// @notice Add a signed size delta to size and revert if it overflows or underflows - /// @param x The size before change - /// @param y The delta by which size should be changed - /// @return z The sizes delta - function addDelta(uint256 x, int256 y) internal pure returns (uint256 z) { - if (y < 0) { - require((z = x - uint256(-y)) < x, "LS"); - } else { - require((z = x + uint256(y)) >= x, "LA"); - } + /// @notice Add a signed size delta to size and revert if it overflows or underflows + /// @param x The size before change + /// @param y The delta by which size should be changed + /// @return z The sizes delta + function addDelta(uint256 x, int256 y) internal pure returns (uint256 z) { + if (y < 0) { + require((z = x - uint256(-y)) < x, "LS"); + } else { + require((z = x + uint256(y)) >= x, "LA"); } + } } diff --git a/src/libraries/Balance.sol b/src/libraries/Balance.sol index f7a83b7..eeb4887 100644 --- a/src/libraries/Balance.sol +++ b/src/libraries/Balance.sol @@ -1,14 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.4; +/// @notice Library for safely and cheaply reading balances +/// @author Kyle Scott (kyle@numoen.com) +/// @author Modified from UniswapV3Pool +/// (https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L140-L145) library Balance { - error BalanceReturnError(); + error BalanceReturnError(); - function balance(address token) internal view returns (uint256) { - (bool success, bytes memory data) = token.staticcall( - abi.encodeWithSelector(bytes4(keccak256(bytes("balanceOf(address)"))), address(this)) - ); - if (!success || data.length < 32) revert BalanceReturnError(); - return abi.decode(data, (uint256)); - } + /// @notice Determine the callers balance of the specified token + function balance(address token) internal view returns (uint256) { + (bool success, bytes memory data) = + token.staticcall(abi.encodeWithSelector(bytes4(keccak256(bytes("balanceOf(address)"))), address(this))); + if (!success || data.length < 32) revert BalanceReturnError(); + return abi.decode(data, (uint256)); + } } diff --git a/src/libraries/FullMath.sol b/src/libraries/FullMath.sol index 38a138c..58ab181 100644 --- a/src/libraries/FullMath.sol +++ b/src/libraries/FullMath.sol @@ -1,137 +1,129 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -/** - * @dev https://github.com/Uniswap/uniswap-v3-core/blob/v1.0.0/contracts/libraries/FullMath.sol - * Added `unchecked` and changed line 76 for being compatible in solidity 0.8 - */ - // solhint-disable max-line-length /// @title Contains 512-bit math functions -/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision -/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits +/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of +/// precision +/// @author Muffin (https://github.com/muffinfi/muffin/blob/master/contracts/libraries/math/FullMath.sol) +/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 +/// bits library FullMath { - /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 - /// @param a The multiplicand - /// @param b The multiplier - /// @param denominator The divisor - /// @return result The 256-bit result - /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv - function mulDiv( - uint256 a, - uint256 b, - uint256 denominator - ) internal pure returns (uint256 result) { - unchecked { - // 512-bit multiply [prod1 prod0] = a * b - // Compute the product mod 2**256 and mod 2**256 - 1 - // then use the Chinese Remainder Theorem to reconstruct - // the 512 bit result. The result is stored in two 256 - // variables such that product = prod1 * 2**256 + prod0 - uint256 prod0; // Least significant 256 bits of the product - uint256 prod1; // Most significant 256 bits of the product - assembly { - let mm := mulmod(a, b, not(0)) - prod0 := mul(a, b) - prod1 := sub(sub(mm, prod0), lt(mm, prod0)) - } + /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or + /// denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv + function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = a * b + // Compute the product mod 2**256 and mod 2**256 - 1 + // then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2**256 + prod0 + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(a, b, not(0)) + prod0 := mul(a, b) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } - // Handle non-overflow cases, 256 by 256 division - if (prod1 == 0) { - require(denominator > 0); - assembly { - result := div(prod0, denominator) - } - return result; - } + // Handle non-overflow cases, 256 by 256 division + if (prod1 == 0) { + require(denominator > 0); + assembly { + result := div(prod0, denominator) + } + return result; + } - // Make sure the result is less than 2**256. - // Also prevents denominator == 0 - require(denominator > prod1); + // Make sure the result is less than 2**256. + // Also prevents denominator == 0 + require(denominator > prod1); - /////////////////////////////////////////////// - // 512 by 256 division. - /////////////////////////////////////////////// + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// - // Make division exact by subtracting the remainder from [prod1 prod0] - // Compute remainder using mulmod - uint256 remainder; - assembly { - remainder := mulmod(a, b, denominator) - } - // Subtract 256 bit number from 512 bit number - assembly { - prod1 := sub(prod1, gt(remainder, prod0)) - prod0 := sub(prod0, remainder) - } + // Make division exact by subtracting the remainder from [prod1 prod0] + // Compute remainder using mulmod + uint256 remainder; + assembly { + remainder := mulmod(a, b, denominator) + } + // Subtract 256 bit number from 512 bit number + assembly { + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } - // Factor powers of two out of denominator - // Compute largest power of two divisor of denominator. - // Always >= 1. + // Factor powers of two out of denominator + // Compute largest power of two divisor of denominator. + // Always >= 1. - // [*] The next line is edited to be compatible with solidity 0.8 - // ref: https://ethereum.stackexchange.com/a/96646 - // original: uint256 twos = -denominator & denominator; - uint256 twos = denominator & (~denominator + 1); + // [*] The next line is edited to be compatible with solidity 0.8 + // ref: https://ethereum.stackexchange.com/a/96646 + // original: uint256 twos = -denominator & denominator; + uint256 twos = denominator & (~denominator + 1); - // Divide denominator by power of two - assembly { - denominator := div(denominator, twos) - } + // Divide denominator by power of two + assembly { + denominator := div(denominator, twos) + } - // Divide [prod1 prod0] by the factors of two - assembly { - prod0 := div(prod0, twos) - } - // Shift in bits from prod1 into prod0. For this we need - // to flip `twos` such that it is 2**256 / twos. - // If twos is zero, then it becomes one - assembly { - twos := add(div(sub(0, twos), twos), 1) - } - prod0 |= prod1 * twos; + // Divide [prod1 prod0] by the factors of two + assembly { + prod0 := div(prod0, twos) + } + // Shift in bits from prod1 into prod0. For this we need + // to flip `twos` such that it is 2**256 / twos. + // If twos is zero, then it becomes one + assembly { + twos := add(div(sub(0, twos), twos), 1) + } + prod0 |= prod1 * twos; - // Invert denominator mod 2**256 - // Now that denominator is an odd number, it has an inverse - // modulo 2**256 such that denominator * inv = 1 mod 2**256. - // Compute the inverse by starting with a seed that is correct - // correct for four bits. That is, denominator * inv = 1 mod 2**4 - uint256 inv = (3 * denominator) ^ 2; - // Now use Newton-Raphson iteration to improve the precision. - // Thanks to Hensel's lifting lemma, this also works in modular - // arithmetic, doubling the correct bits in each step. - inv *= 2 - denominator * inv; // inverse mod 2**8 - inv *= 2 - denominator * inv; // inverse mod 2**16 - inv *= 2 - denominator * inv; // inverse mod 2**32 - inv *= 2 - denominator * inv; // inverse mod 2**64 - inv *= 2 - denominator * inv; // inverse mod 2**128 - inv *= 2 - denominator * inv; // inverse mod 2**256 + // Invert denominator mod 2**256 + // Now that denominator is an odd number, it has an inverse + // modulo 2**256 such that denominator * inv = 1 mod 2**256. + // Compute the inverse by starting with a seed that is correct + // correct for four bits. That is, denominator * inv = 1 mod 2**4 + uint256 inv = (3 * denominator) ^ 2; + // Now use Newton-Raphson iteration to improve the precision. + // Thanks to Hensel's lifting lemma, this also works in modular + // arithmetic, doubling the correct bits in each step. + inv *= 2 - denominator * inv; // inverse mod 2**8 + inv *= 2 - denominator * inv; // inverse mod 2**16 + inv *= 2 - denominator * inv; // inverse mod 2**32 + inv *= 2 - denominator * inv; // inverse mod 2**64 + inv *= 2 - denominator * inv; // inverse mod 2**128 + inv *= 2 - denominator * inv; // inverse mod 2**256 - // Because the division is now exact we can divide by multiplying - // with the modular inverse of denominator. This will give us the - // correct result modulo 2**256. Since the precoditions guarantee - // that the outcome is less than 2**256, this is the final result. - // We don't need to compute the high bits of the result and prod1 - // is no longer required. - result = prod0 * inv; - return result; - } + // Because the division is now exact we can divide by multiplying + // with the modular inverse of denominator. This will give us the + // correct result modulo 2**256. Since the precoditions guarantee + // that the outcome is less than 2**256, this is the final result. + // We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inv; + return result; } + } - /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 - /// @param a The multiplicand - /// @param b The multiplier - /// @param denominator The divisor - /// @return result The 256-bit result - function mulDivRoundingUp( - uint256 a, - uint256 b, - uint256 denominator - ) internal pure returns (uint256 result) { - result = mulDiv(a, b, denominator); - if (mulmod(a, b, denominator) > 0) { - result++; - } + /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or + /// denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + function mulDivRoundingUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) { + result = mulDiv(a, b, denominator); + if (mulmod(a, b, denominator) > 0) { + result++; } + } } diff --git a/src/libraries/SafeCast.sol b/src/libraries/SafeCast.sol index 3a381dc..d118fb1 100644 --- a/src/libraries/SafeCast.sol +++ b/src/libraries/SafeCast.sol @@ -1,19 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.0; -/// @notice Casting library +/// @notice Library for safely and cheaply casting solidity types /// @author Kyle Scott (https://github.com/Numoen/core/blob/master/src/libraries/SafeCast.sol) /// @author Modified from Uniswap (https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SafeCast.sol) library SafeCast { - function toUint120(uint256 y) internal pure returns (uint120 z) { - require((z = uint120(y)) == y); - } + function toUint120(uint256 y) internal pure returns (uint120 z) { + require((z = uint120(y)) == y); + } - /// @notice Cast a uint256 to a int256, revert on overflow - /// @param y The uint256 to be casted - /// @return z The casted integer, now type int256 - function toInt256(uint256 y) internal pure returns (int256 z) { - require(y < 2**255); - z = int256(y); - } + /// @notice Cast a uint256 to a int256, revert on overflow + /// @param y The uint256 to be casted + /// @return z The casted integer, now type int256 + function toInt256(uint256 y) internal pure returns (int256 z) { + require(y < 2 ** 255); + z = int256(y); + } } diff --git a/src/libraries/SafeTransferLib.sol b/src/libraries/SafeTransferLib.sol index d66b65d..0ed12e9 100644 --- a/src/libraries/SafeTransferLib.sol +++ b/src/libraries/SafeTransferLib.sol @@ -1,117 +1,116 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.4; +pragma solidity >=0.8.0; -/// @dev Adapted from Rari's Solmate https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol -/// Edited from using error message to custom error for lower bytecode size. +// solhint-disable max-line-length /// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. -/// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol) -/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. +/// @author Muffin (https://github.com/muffinfi/muffin/blob/master/contracts/libraries/utils/SafeTransferLib.sol) +/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free +/// memory pointer. library SafeTransferLib { - error FailedTransferETH(); - error FailedTransfer(); - error FailedTransferFrom(); + error FailedTransferETH(); + error FailedTransfer(); + error FailedTransferFrom(); + error FailedApprove(); - /*/////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// ETH OPERATIONS //////////////////////////////////////////////////////////////*/ - function safeTransferETH(address to, uint256 amount) internal { - bool callStatus; + function safeTransferETH(address to, uint256 amount) internal { + bool callStatus; - assembly { - // Transfer the ETH and store if it succeeded or not. - callStatus := call(gas(), to, amount, 0, 0, 0, 0) - } - - if (!callStatus) revert FailedTransferETH(); + assembly { + // Transfer the ETH and store if it succeeded or not. + callStatus := call(gas(), to, amount, 0, 0, 0, 0) } - /*/////////////////////////////////////////////////////////////// + if (!callStatus) revert FailedTransferETH(); + } + + /*/////////////////////////////////////////////////////////////// ERC20 OPERATIONS //////////////////////////////////////////////////////////////*/ - function safeTransferFrom( - address token, - address from, - address to, - uint256 amount - ) internal { - bool callStatus; - - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) - - // Write the abi-encoded calldata to memory piece by piece: - mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) // Begin with the function selector. - mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "from" argument. - mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. - mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. - - // Call the token and store if it succeeded or not. - // We use 100 because the calldata length is 4 + 32 * 3. - callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0) - } - - if (!didLastOptionalReturnCallSucceed(callStatus)) revert FailedTransferFrom(); + function safeTransferFrom(address token, address from, address to, uint256 amount) internal { + bool callStatus; + + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata to memory piece by piece: + mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) // Begin with + // the function selector. + mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append + // the "from" argument. + mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append + // the "to" argument. + mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full + // 32 byte value. + + // Call the token and store if it succeeded or not. + // We use 100 because the calldata length is 4 + 32 * 3. + callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0) } - function safeTransfer( - address token, - address to, - uint256 amount - ) internal { - bool callStatus; + if (!didLastOptionalReturnCallSucceed(callStatus)) revert FailedTransferFrom(); + } - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) + function safeTransfer(address token, address to, uint256 amount) internal { + bool callStatus; - // Write the abi-encoded calldata to memory piece by piece: - mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) // Begin with the function selector. - mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument. - mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) - // Call the token and store if it succeeded or not. - // We use 68 because the calldata length is 4 + 32 * 2. - callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) - } + // Write the abi-encoded calldata to memory piece by piece: + mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) // Begin with + // the function selector. + mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append + // the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full + // 32 byte value. - if (!didLastOptionalReturnCallSucceed(callStatus)) revert FailedTransfer(); + // Call the token and store if it succeeded or not. + // We use 68 because the calldata length is 4 + 32 * 2. + callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) } - /*/////////////////////////////////////////////////////////////// + if (!didLastOptionalReturnCallSucceed(callStatus)) revert FailedTransfer(); + } + + /*/////////////////////////////////////////////////////////////// INTERNAL HELPER LOGIC //////////////////////////////////////////////////////////////*/ - function didLastOptionalReturnCallSucceed(bool callStatus) private pure returns (bool success) { - assembly { - // If the call reverted: - if iszero(callStatus) { - // Copy the revert message into memory. - returndatacopy(0, 0, returndatasize()) - - // Revert with the same message. - revert(0, returndatasize()) - } - - switch returndatasize() - case 32 { - // Copy the return data into memory. - returndatacopy(0, 0, returndatasize()) - - // Set success to whether it returned true. - success := iszero(iszero(mload(0))) - } - case 0 { - // There was no return data. - success := 1 - } - default { - // It returned some malformed input. - success := 0 - } - } + function didLastOptionalReturnCallSucceed(bool callStatus) private pure returns (bool success) { + assembly { + // If the call reverted: + if iszero(callStatus) { + // Copy the revert message into memory. + returndatacopy(0, 0, returndatasize()) + + // Revert with the same message. + revert(0, returndatasize()) + } + + switch returndatasize() + case 32 { + // Copy the return data into memory. + returndatacopy(0, 0, returndatasize()) + + // Set success to whether it returned true. + success := iszero(iszero(mload(0))) + } + case 0 { + // There was no return data. + success := 1 + } + default { + // It returned some malformed input. + success := 0 + } } + } } diff --git a/src/periphery/LendgineRouter.sol b/src/periphery/LendgineRouter.sol index b59a7a3..f478b60 100644 --- a/src/periphery/LendgineRouter.sol +++ b/src/periphery/LendgineRouter.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.4; import { Lendgine } from "../core/Lendgine.sol"; // TODO: use interface -import { Payment } from "./base/Payment.sol"; +import { Payment } from "./Payment.sol"; import { SwapHelper } from "./SwapHelper.sol"; import { FullMath } from "../libraries/FullMath.sol"; @@ -13,280 +13,267 @@ import { SafeCast } from "../libraries/SafeCast.sol"; import { SafeTransferLib } from "../libraries/SafeTransferLib.sol"; contract LendgineRouter is SwapHelper, Payment, IMintCallback, IPairMintCallback { - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event Mint(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); + event Mint(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); - event Burn(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); + event Burn(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - error LivelinessError(); + error LivelinessError(); - error ValidationError(); + error ValidationError(); - error AmountError(); + error AmountError(); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - address public immutable factory; + address public immutable factory; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor( - address _factory, - address _uniswapV2Factory, - address _uniswapV3Factory, - address _weth - ) SwapHelper(_uniswapV2Factory, _uniswapV3Factory) Payment(_weth) { - factory = _factory; - } - - /*////////////////////////////////////////////////////////////// + constructor( + address _factory, + address _uniswapV2Factory, + address _uniswapV3Factory, + address _weth + ) + SwapHelper(_uniswapV2Factory, _uniswapV3Factory) + Payment(_weth) + { + factory = _factory; + } + + /*////////////////////////////////////////////////////////////// LIVELINESS MODIFIER //////////////////////////////////////////////////////////////*/ - modifier checkDeadline(uint256 deadline) { - if (deadline < block.timestamp) revert LivelinessError(); - _; - } + modifier checkDeadline(uint256 deadline) { + if (deadline < block.timestamp) revert LivelinessError(); + _; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// MINT LOGIC //////////////////////////////////////////////////////////////*/ - struct MintCallbackData { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - uint256 collateralMax; - SwapType swapType; - bytes swapExtraData; - address payer; - } - - function mintCallback( - uint256 collateralTotal, - uint256 amount0, - uint256 amount1, - uint256, - bytes calldata data - ) external override { - MintCallbackData memory decoded = abi.decode(data, (MintCallbackData)); - - address lendgine = LendgineAddress.computeAddress( - factory, - decoded.token0, - decoded.token1, - decoded.token0Exp, - decoded.token1Exp, - decoded.upperBound - ); - if (lendgine != msg.sender) revert ValidationError(); - - // swap token0 to token1 - uint256 collateralSwap = swap( - decoded.swapType, - SwapParams({ - tokenIn: decoded.token0, - tokenOut: decoded.token1, - amount: SafeCast.toInt256(amount0), - recipient: msg.sender - }), - decoded.swapExtraData - ); - - // send token1 back - SafeTransferLib.safeTransfer(decoded.token1, msg.sender, amount1); - - // pull the rest of tokens from the user - uint256 collateralIn = collateralTotal - amount1 - collateralSwap; - if (collateralIn > decoded.collateralMax) revert AmountError(); - - pay(decoded.token1, decoded.payer, msg.sender, collateralIn); - } - - struct MintParams { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - uint256 amountIn; - uint256 amountBorrow; - uint256 sharesMin; - SwapType swapType; - bytes swapExtraData; - address recipient; - uint256 deadline; - } - - function mint(MintParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 shares) { - address lendgine = LendgineAddress.computeAddress( - factory, - params.token0, - params.token1, - params.token0Exp, - params.token1Exp, - params.upperBound - ); - - shares = Lendgine(lendgine).mint( - address(this), - params.amountIn + params.amountBorrow, - abi.encode( - MintCallbackData({ - token0: params.token0, - token1: params.token1, - token0Exp: params.token0Exp, - token1Exp: params.token1Exp, - upperBound: params.upperBound, - collateralMax: params.amountIn, - swapType: params.swapType, - swapExtraData: params.swapExtraData, - payer: msg.sender - }) - ) - ); - if (shares < params.sharesMin) revert AmountError(); - - Lendgine(lendgine).transfer(params.recipient, shares); - - emit Mint(msg.sender, lendgine, params.amountIn, shares, params.recipient); - } - - /*////////////////////////////////////////////////////////////// + struct MintCallbackData { + address token0; + address token1; + uint256 token0Exp; + uint256 token1Exp; + uint256 upperBound; + uint256 collateralMax; + SwapType swapType; + bytes swapExtraData; + address payer; + } + + function mintCallback( + uint256 collateralTotal, + uint256 amount0, + uint256 amount1, + uint256, + bytes calldata data + ) + external + override + { + MintCallbackData memory decoded = abi.decode(data, (MintCallbackData)); + + address lendgine = LendgineAddress.computeAddress( + factory, decoded.token0, decoded.token1, decoded.token0Exp, decoded.token1Exp, decoded.upperBound + ); + if (lendgine != msg.sender) revert ValidationError(); + + // swap token0 to token1 + uint256 collateralSwap = swap( + decoded.swapType, + SwapParams({ + tokenIn: decoded.token0, + tokenOut: decoded.token1, + amount: SafeCast.toInt256(amount0), + recipient: msg.sender + }), + decoded.swapExtraData + ); + + // send token1 back + SafeTransferLib.safeTransfer(decoded.token1, msg.sender, amount1); + + // pull the rest of tokens from the user + uint256 collateralIn = collateralTotal - amount1 - collateralSwap; + if (collateralIn > decoded.collateralMax) revert AmountError(); + + pay(decoded.token1, decoded.payer, msg.sender, collateralIn); + } + + struct MintParams { + address token0; + address token1; + uint256 token0Exp; + uint256 token1Exp; + uint256 upperBound; + uint256 amountIn; + uint256 amountBorrow; + uint256 sharesMin; + SwapType swapType; + bytes swapExtraData; + address recipient; + uint256 deadline; + } + + function mint(MintParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 shares) { + address lendgine = LendgineAddress.computeAddress( + factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound + ); + + shares = Lendgine(lendgine).mint( + address(this), + params.amountIn + params.amountBorrow, + abi.encode( + MintCallbackData({ + token0: params.token0, + token1: params.token1, + token0Exp: params.token0Exp, + token1Exp: params.token1Exp, + upperBound: params.upperBound, + collateralMax: params.amountIn, + swapType: params.swapType, + swapExtraData: params.swapExtraData, + payer: msg.sender + }) + ) + ); + if (shares < params.sharesMin) revert AmountError(); + + Lendgine(lendgine).transfer(params.recipient, shares); + + emit Mint(msg.sender, lendgine, params.amountIn, shares, params.recipient); + } + + /*////////////////////////////////////////////////////////////// BURN LOGIC //////////////////////////////////////////////////////////////*/ - struct PairMintCallbackData { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - uint256 collateralMin; - uint256 amount0Min; - uint256 amount1Min; - SwapType swapType; - bytes swapExtraData; - address recipient; - } - - function pairMintCallback(uint256 liquidity, bytes calldata data) external override { - PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData)); - - address lendgine = LendgineAddress.computeAddress( - factory, - decoded.token0, - decoded.token1, - decoded.token0Exp, - decoded.token1Exp, - decoded.upperBound - ); - if (lendgine != msg.sender) revert ValidationError(); - - // determine amounts - uint256 r0 = Lendgine(msg.sender).reserve0(); - uint256 r1 = Lendgine(msg.sender).reserve1(); - uint256 totalLiquidity = Lendgine(msg.sender).totalLiquidity(); - - uint256 amount0; - uint256 amount1; - - if (totalLiquidity == 0) { - amount0 = decoded.amount0Min; - amount1 = decoded.amount1Min; - } else { - amount0 = FullMath.mulDivRoundingUp(liquidity, r0, totalLiquidity); - amount1 = FullMath.mulDivRoundingUp(liquidity, r1, totalLiquidity); - } - - if (amount0 < decoded.amount0Min || amount1 < decoded.amount1Min) revert AmountError(); - - // swap for token0 - uint256 collateralSwapped = swap( - decoded.swapType, - SwapParams({ - tokenIn: decoded.token1, - tokenOut: decoded.token0, - amount: -SafeCast.toInt256(amount0), - recipient: msg.sender - }), - decoded.swapExtraData - ); - - // pay token1 - SafeTransferLib.safeTransfer(decoded.token1, msg.sender, amount1); - - // determine remaining and send to recipient - uint256 collateralTotal = Lendgine(msg.sender).convertLiquidityToCollateral(liquidity); - uint256 collateralOut = collateralTotal - amount1 - collateralSwapped; - if (collateralOut < decoded.collateralMin) revert AmountError(); - - if (decoded.recipient != address(this)) - SafeTransferLib.safeTransfer(decoded.token1, decoded.recipient, collateralOut); - } - - struct BurnParams { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - uint256 shares; - uint256 collateralMin; - uint256 amount0Min; - uint256 amount1Min; - SwapType swapType; - bytes swapExtraData; - address recipient; - uint256 deadline; + struct PairMintCallbackData { + address token0; + address token1; + uint256 token0Exp; + uint256 token1Exp; + uint256 upperBound; + uint256 collateralMin; + uint256 amount0Min; + uint256 amount1Min; + SwapType swapType; + bytes swapExtraData; + address recipient; + } + + function pairMintCallback(uint256 liquidity, bytes calldata data) external override { + PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData)); + + address lendgine = LendgineAddress.computeAddress( + factory, decoded.token0, decoded.token1, decoded.token0Exp, decoded.token1Exp, decoded.upperBound + ); + if (lendgine != msg.sender) revert ValidationError(); + + // determine amounts + uint256 r0 = Lendgine(msg.sender).reserve0(); + uint256 r1 = Lendgine(msg.sender).reserve1(); + uint256 totalLiquidity = Lendgine(msg.sender).totalLiquidity(); + + uint256 amount0; + uint256 amount1; + + if (totalLiquidity == 0) { + amount0 = decoded.amount0Min; + amount1 = decoded.amount1Min; + } else { + amount0 = FullMath.mulDivRoundingUp(liquidity, r0, totalLiquidity); + amount1 = FullMath.mulDivRoundingUp(liquidity, r1, totalLiquidity); } - function burn(BurnParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 amount) { - address lendgine = LendgineAddress.computeAddress( - factory, - params.token0, - params.token1, - params.token0Exp, - params.token1Exp, - params.upperBound - ); - - address recipient = params.recipient == address(0) ? address(this) : params.recipient; - - Lendgine(lendgine).transferFrom(msg.sender, lendgine, params.shares); - - amount = Lendgine(lendgine).burn( - address(this), - abi.encode( - PairMintCallbackData({ - token0: params.token0, - token1: params.token1, - token0Exp: params.token0Exp, - token1Exp: params.token1Exp, - upperBound: params.upperBound, - collateralMin: params.collateralMin, - amount0Min: params.amount0Min, - amount1Min: params.amount1Min, - swapType: params.swapType, - swapExtraData: params.swapExtraData, - recipient: recipient - }) - ) - ); - - emit Burn(msg.sender, lendgine, amount, params.shares, recipient); + if (amount0 < decoded.amount0Min || amount1 < decoded.amount1Min) revert AmountError(); + + // swap for token0 + uint256 collateralSwapped = swap( + decoded.swapType, + SwapParams({ + tokenIn: decoded.token1, + tokenOut: decoded.token0, + amount: -SafeCast.toInt256(amount0), + recipient: msg.sender + }), + decoded.swapExtraData + ); + + // pay token1 + SafeTransferLib.safeTransfer(decoded.token1, msg.sender, amount1); + + // determine remaining and send to recipient + uint256 collateralTotal = Lendgine(msg.sender).convertLiquidityToCollateral(liquidity); + uint256 collateralOut = collateralTotal - amount1 - collateralSwapped; + if (collateralOut < decoded.collateralMin) revert AmountError(); + + if (decoded.recipient != address(this)) { + SafeTransferLib.safeTransfer(decoded.token1, decoded.recipient, collateralOut); } + } + + struct BurnParams { + address token0; + address token1; + uint256 token0Exp; + uint256 token1Exp; + uint256 upperBound; + uint256 shares; + uint256 collateralMin; + uint256 amount0Min; + uint256 amount1Min; + SwapType swapType; + bytes swapExtraData; + address recipient; + uint256 deadline; + } + + function burn(BurnParams calldata params) external payable checkDeadline(params.deadline) returns (uint256 amount) { + address lendgine = LendgineAddress.computeAddress( + factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound + ); + + address recipient = params.recipient == address(0) ? address(this) : params.recipient; + + Lendgine(lendgine).transferFrom(msg.sender, lendgine, params.shares); + + amount = Lendgine(lendgine).burn( + address(this), + abi.encode( + PairMintCallbackData({ + token0: params.token0, + token1: params.token1, + token0Exp: params.token0Exp, + token1Exp: params.token1Exp, + upperBound: params.upperBound, + collateralMin: params.collateralMin, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + swapType: params.swapType, + swapExtraData: params.swapExtraData, + recipient: recipient + }) + ) + ); + + emit Burn(msg.sender, lendgine, amount, params.shares, recipient); + } } diff --git a/src/periphery/LiquidityManager.sol b/src/periphery/LiquidityManager.sol index 03f931a..d8ff692 100644 --- a/src/periphery/LiquidityManager.sol +++ b/src/periphery/LiquidityManager.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.4; import { Lendgine } from "../core/Lendgine.sol"; // TODO: use interface -import { Payment } from "./base/Payment.sol"; +import { Payment } from "./Payment.sol"; import { IPairMintCallback } from "../core/interfaces/callback/IPairMintCallback.sol"; @@ -10,261 +10,234 @@ import { FullMath } from "../libraries/FullMath.sol"; import { LendgineAddress } from "./libraries/LendgineAddress.sol"; contract LiquidityManager is Payment, IPairMintCallback { - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event AddLiquidity( - address indexed from, - address indexed lendgine, - uint256 liquidity, - uint256 size, - uint256 amount0, - uint256 amount1, - address indexed to - ); - - event RemoveLiquidity( - address indexed from, - address indexed lendgine, - uint256 liquidity, - uint256 size, - uint256 amount0, - uint256 amount1, - address indexed to - ); - - event Collect(address indexed from, address indexed lendgine, uint256 amount, address indexed to); - - /*////////////////////////////////////////////////////////////// + event AddLiquidity( + address indexed from, + address indexed lendgine, + uint256 liquidity, + uint256 size, + uint256 amount0, + uint256 amount1, + address indexed to + ); + + event RemoveLiquidity( + address indexed from, + address indexed lendgine, + uint256 liquidity, + uint256 size, + uint256 amount0, + uint256 amount1, + address indexed to + ); + + event Collect(address indexed from, address indexed lendgine, uint256 amount, address indexed to); + + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ - error LivelinessError(); + error LivelinessError(); - error AmountError(); + error AmountError(); - error ValidationError(); + error ValidationError(); - error PositionInvalidError(); + error PositionInvalidError(); - error CollectError(); + error CollectError(); - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - address public immutable factory; + address public immutable factory; - struct Position { - uint256 size; - uint256 rewardPerPositionPaid; - uint256 tokensOwed; - } + struct Position { + uint256 size; + uint256 rewardPerPositionPaid; + uint256 tokensOwed; + } - // owner to lendgine to position - mapping(address => mapping(address => Position)) public positions; + // owner to lendgine to position + mapping(address => mapping(address => Position)) public positions; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address _factory, address _weth) Payment(_weth) { - factory = _factory; - } + constructor(address _factory, address _weth) Payment(_weth) { + factory = _factory; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// LIVELINESS MODIFIER //////////////////////////////////////////////////////////////*/ - modifier checkDeadline(uint256 deadline) { - if (deadline < block.timestamp) revert LivelinessError(); - _; - } + modifier checkDeadline(uint256 deadline) { + if (deadline < block.timestamp) revert LivelinessError(); + _; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// CALLBACK //////////////////////////////////////////////////////////////*/ - struct PairMintCallbackData { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - uint256 amount0; - uint256 amount1; - address payer; - } + struct PairMintCallbackData { + address token0; + address token1; + uint256 token0Exp; + uint256 token1Exp; + uint256 upperBound; + uint256 amount0; + uint256 amount1; + address payer; + } + + function pairMintCallback(uint256, bytes calldata data) external { + PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData)); + + address lendgine = LendgineAddress.computeAddress( + factory, decoded.token0, decoded.token1, decoded.token0Exp, decoded.token1Exp, decoded.upperBound + ); + if (lendgine != msg.sender) revert ValidationError(); - function pairMintCallback(uint256, bytes calldata data) external { - PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData)); - - address lendgine = LendgineAddress.computeAddress( - factory, - decoded.token0, - decoded.token1, - decoded.token0Exp, - decoded.token1Exp, - decoded.upperBound - ); - if (lendgine != msg.sender) revert ValidationError(); - - if (decoded.amount0 > 0) pay(decoded.token0, decoded.payer, msg.sender, decoded.amount0); - if (decoded.amount1 > 0) pay(decoded.token1, decoded.payer, msg.sender, decoded.amount1); - } + if (decoded.amount0 > 0) pay(decoded.token0, decoded.payer, msg.sender, decoded.amount0); + if (decoded.amount1 > 0) pay(decoded.token1, decoded.payer, msg.sender, decoded.amount1); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// LIQUIDITY MANAGER LOGIC //////////////////////////////////////////////////////////////*/ - struct AddLiquidityParams { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - uint256 liquidity; - uint256 amount0Min; - uint256 amount1Min; - uint256 sizeMin; - address recipient; - uint256 deadline; - } + struct AddLiquidityParams { + address token0; + address token1; + uint256 token0Exp; + uint256 token1Exp; + uint256 upperBound; + uint256 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 sizeMin; + address recipient; + uint256 deadline; + } + + function addLiquidity(AddLiquidityParams calldata params) external payable checkDeadline(params.deadline) { + address lendgine = LendgineAddress.computeAddress( + factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound + ); - function addLiquidity(AddLiquidityParams calldata params) external payable checkDeadline(params.deadline) { - address lendgine = LendgineAddress.computeAddress( - factory, - params.token0, - params.token1, - params.token0Exp, - params.token1Exp, - params.upperBound - ); - - uint256 r0 = Lendgine(lendgine).reserve0(); - uint256 r1 = Lendgine(lendgine).reserve1(); - uint256 totalLiquidity = Lendgine(lendgine).totalLiquidity(); - - uint256 amount0; - uint256 amount1; - - if (totalLiquidity == 0) { - amount0 = params.amount0Min; - amount1 = params.amount1Min; - } else { - amount0 = FullMath.mulDivRoundingUp(params.liquidity, r0, totalLiquidity); - amount1 = FullMath.mulDivRoundingUp(params.liquidity, r1, totalLiquidity); - } - - if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError(); - - uint256 size = Lendgine(lendgine).deposit( - address(this), - params.liquidity, - abi.encode( - PairMintCallbackData({ - token0: params.token0, - token1: params.token1, - token0Exp: params.token0Exp, - token1Exp: params.token1Exp, - upperBound: params.upperBound, - amount0: amount0, - amount1: amount1, - payer: msg.sender - }) - ) - ); - if (size < params.sizeMin) revert AmountError(); - - Position memory position = positions[params.recipient][lendgine]; // SLOAD - - (, uint256 rewardPerPositionPaid, ) = Lendgine(lendgine).positions(address(this)); - position.tokensOwed += FullMath.mulDiv( - position.size, - rewardPerPositionPaid - position.rewardPerPositionPaid, - 1e18 - ); - position.rewardPerPositionPaid = rewardPerPositionPaid; - position.size += size; - - positions[params.recipient][lendgine] = position; // SSTORE - - emit AddLiquidity(msg.sender, lendgine, params.liquidity, size, amount0, amount1, params.recipient); - } + uint256 r0 = Lendgine(lendgine).reserve0(); + uint256 r1 = Lendgine(lendgine).reserve1(); + uint256 totalLiquidity = Lendgine(lendgine).totalLiquidity(); + + uint256 amount0; + uint256 amount1; - struct RemoveLiquidityParams { - address token0; - address token1; - uint256 token0Exp; - uint256 token1Exp; - uint256 upperBound; - uint256 size; - uint256 amount0Min; - uint256 amount1Min; - address recipient; - uint256 deadline; + if (totalLiquidity == 0) { + amount0 = params.amount0Min; + amount1 = params.amount1Min; + } else { + amount0 = FullMath.mulDivRoundingUp(params.liquidity, r0, totalLiquidity); + amount1 = FullMath.mulDivRoundingUp(params.liquidity, r1, totalLiquidity); } - function removeLiquidity(RemoveLiquidityParams calldata params) external payable checkDeadline(params.deadline) { - address lendgine = LendgineAddress.computeAddress( - factory, - params.token0, - params.token1, - params.token0Exp, - params.token1Exp, - params.upperBound - ); + if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError(); + + uint256 size = Lendgine(lendgine).deposit( + address(this), + params.liquidity, + abi.encode( + PairMintCallbackData({ + token0: params.token0, + token1: params.token1, + token0Exp: params.token0Exp, + token1Exp: params.token1Exp, + upperBound: params.upperBound, + amount0: amount0, + amount1: amount1, + payer: msg.sender + }) + ) + ); + if (size < params.sizeMin) revert AmountError(); + + Position memory position = positions[params.recipient][lendgine]; // SLOAD + + (, uint256 rewardPerPositionPaid,) = Lendgine(lendgine).positions(address(this)); + position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18); + position.rewardPerPositionPaid = rewardPerPositionPaid; + position.size += size; + + positions[params.recipient][lendgine] = position; // SSTORE + + emit AddLiquidity(msg.sender, lendgine, params.liquidity, size, amount0, amount1, params.recipient); + } + + struct RemoveLiquidityParams { + address token0; + address token1; + uint256 token0Exp; + uint256 token1Exp; + uint256 upperBound; + uint256 size; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + function removeLiquidity(RemoveLiquidityParams calldata params) external payable checkDeadline(params.deadline) { + address lendgine = LendgineAddress.computeAddress( + factory, params.token0, params.token1, params.token0Exp, params.token1Exp, params.upperBound + ); - address recipient = params.recipient == address(0) ? address(this) : params.recipient; + address recipient = params.recipient == address(0) ? address(this) : params.recipient; - (uint256 amount0, uint256 amount1, uint256 liquidity) = Lendgine(lendgine).withdraw(recipient, params.size); - if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError(); + (uint256 amount0, uint256 amount1, uint256 liquidity) = Lendgine(lendgine).withdraw(recipient, params.size); + if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert AmountError(); - Position memory position = positions[msg.sender][lendgine]; // SLOAD + Position memory position = positions[msg.sender][lendgine]; // SLOAD - (, uint256 rewardPerPositionPaid, ) = Lendgine(lendgine).positions(address(this)); - position.tokensOwed += FullMath.mulDiv( - position.size, - rewardPerPositionPaid - position.rewardPerPositionPaid, - 1e18 - ); - position.rewardPerPositionPaid = rewardPerPositionPaid; - position.size -= params.size; + (, uint256 rewardPerPositionPaid,) = Lendgine(lendgine).positions(address(this)); + position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18); + position.rewardPerPositionPaid = rewardPerPositionPaid; + position.size -= params.size; - positions[msg.sender][lendgine] = position; // SSTORE + positions[msg.sender][lendgine] = position; // SSTORE - emit RemoveLiquidity(msg.sender, lendgine, liquidity, params.size, amount0, amount1, recipient); - } + emit RemoveLiquidity(msg.sender, lendgine, liquidity, params.size, amount0, amount1, recipient); + } - struct CollectParams { - address lendgine; - address recipient; - uint256 amountRequested; - } + struct CollectParams { + address lendgine; + address recipient; + uint256 amountRequested; + } - function collect(CollectParams calldata params) external payable returns (uint256 amount) { - Lendgine(params.lendgine).accruePositionInterest(); + function collect(CollectParams calldata params) external payable returns (uint256 amount) { + Lendgine(params.lendgine).accruePositionInterest(); - address recipient = params.recipient == address(0) ? address(this) : params.recipient; + address recipient = params.recipient == address(0) ? address(this) : params.recipient; - Position memory position = positions[msg.sender][params.lendgine]; // SLOAD + Position memory position = positions[msg.sender][params.lendgine]; // SLOAD - (, uint256 rewardPerPositionPaid, ) = Lendgine(params.lendgine).positions(address(this)); - position.tokensOwed += FullMath.mulDiv( - position.size, - rewardPerPositionPaid - position.rewardPerPositionPaid, - 1e18 - ); - position.rewardPerPositionPaid = rewardPerPositionPaid; + (, uint256 rewardPerPositionPaid,) = Lendgine(params.lendgine).positions(address(this)); + position.tokensOwed += FullMath.mulDiv(position.size, rewardPerPositionPaid - position.rewardPerPositionPaid, 1e18); + position.rewardPerPositionPaid = rewardPerPositionPaid; - amount = params.amountRequested > position.tokensOwed ? position.tokensOwed : params.amountRequested; - position.tokensOwed -= amount; + amount = params.amountRequested > position.tokensOwed ? position.tokensOwed : params.amountRequested; + position.tokensOwed -= amount; - positions[msg.sender][params.lendgine] = position; // SSTORE + positions[msg.sender][params.lendgine] = position; // SSTORE - uint256 collectAmount = Lendgine(params.lendgine).collect(recipient, amount); - if (collectAmount != amount) revert CollectError(); // extra check for safety + uint256 collectAmount = Lendgine(params.lendgine).collect(recipient, amount); + if (collectAmount != amount) revert CollectError(); // extra check for safety - emit Collect(msg.sender, params.lendgine, amount, recipient); - } + emit Collect(msg.sender, params.lendgine, amount, recipient); + } } diff --git a/src/periphery/Payment.sol b/src/periphery/Payment.sol new file mode 100644 index 0000000..ef1b1c3 --- /dev/null +++ b/src/periphery/Payment.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.4; + +import { IWETH9 } from "./interfaces/IWETH9.sol"; + +import { SafeTransferLib } from "./../libraries/SafeTransferLib.sol"; +import { Balance } from "./../libraries/Balance.sol"; + +/// @title Payment contract +/// @author https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/PeripheryPayments.sol +/// @notice Functions to ease deposits and withdrawals of ETH +abstract contract Payment { + address public immutable weth; + + error InsufficientOutputError(); + + constructor(address _weth) { + weth = _weth; + } + + receive() external payable { + require(msg.sender == weth, "Not WETH9"); + } + + function unwrapWETH(uint256 amountMinimum, address recipient) public payable { + uint256 balanceWETH = Balance.balance(weth); + if (balanceWETH < amountMinimum) revert InsufficientOutputError(); + + if (balanceWETH > 0) { + IWETH9(weth).withdraw(balanceWETH); + SafeTransferLib.safeTransferETH(recipient, balanceWETH); + } + } + + function sweepToken(address token, uint256 amountMinimum, address recipient) public payable { + uint256 balanceToken = Balance.balance(token); + if (balanceToken < amountMinimum) revert InsufficientOutputError(); + + if (balanceToken > 0) { + SafeTransferLib.safeTransfer(token, recipient, balanceToken); + } + } + + function refundETH() external payable { + if (address(this).balance > 0) SafeTransferLib.safeTransferETH(msg.sender, address(this).balance); + } + + /// @param token The token to pay + /// @param payer The entity that must pay + /// @param recipient The entity that will receive payment + /// @param value The amount to pay + function pay(address token, address payer, address recipient, uint256 value) internal { + if (token == weth && address(this).balance >= value) { + // pay with WETH + IWETH9(weth).deposit{value: value}(); // wrap only what is needed to pay + SafeTransferLib.safeTransfer(weth, recipient, value); + } else if (payer == address(this)) { + // pay with tokens already in the contract (for the exact input multihop case) + SafeTransferLib.safeTransfer(token, recipient, value); + } else { + // pull payment + SafeTransferLib.safeTransferFrom(token, payer, recipient, value); + } + } +} diff --git a/src/periphery/SwapHelper.sol b/src/periphery/SwapHelper.sol index dc8c129..c88e54d 100644 --- a/src/periphery/SwapHelper.sol +++ b/src/periphery/SwapHelper.sol @@ -11,121 +11,103 @@ import { TickMath } from "./UniswapV3/libraries/TickMath.sol"; import { UniswapV2Library } from "./UniswapV2/libraries/UniswapV2Library.sol"; abstract contract SwapHelper is IUniswapV3SwapCallback { - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - address public immutable uniswapV2Factory; + address public immutable uniswapV2Factory; - address public immutable uniswapV3Factory; + address public immutable uniswapV3Factory; - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address _uniswapV2Factory, address _uniswapV3Factory) { - uniswapV2Factory = _uniswapV2Factory; - uniswapV3Factory = _uniswapV3Factory; - } + constructor(address _uniswapV2Factory, address _uniswapV3Factory) { + uniswapV2Factory = _uniswapV2Factory; + uniswapV3Factory = _uniswapV3Factory; + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// UNISWAPV3 SWAP CALLBACK //////////////////////////////////////////////////////////////*/ - function uniswapV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external override { - address tokenIn = abi.decode(data, (address)); - // no validation because this contract should hold no tokens - - SafeTransferLib.safeTransfer( - tokenIn, - msg.sender, - amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta) - ); - } + function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external override { + address tokenIn = abi.decode(data, (address)); + // no validation because this contract should hold no tokens + + SafeTransferLib.safeTransfer(tokenIn, msg.sender, amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta)); + } - /*////////////////////////////////////////////////////////////// + /*////////////////////////////////////////////////////////////// SWAP LOGIC //////////////////////////////////////////////////////////////*/ - enum SwapType { - UniswapV2, - UniswapV3 - } - - struct SwapParams { - address tokenIn; - address tokenOut; - int256 amount; // negative corresponds to exact out - address recipient; - } - - struct UniV3Data { - uint24 fee; - } - - function swap( - SwapType swapType, - SwapParams memory params, - bytes memory data - ) internal returns (uint256 amount) { - if (swapType == SwapType.UniswapV2) { - address pair = UniswapV2Library.pairFor(uniswapV2Factory, params.tokenIn, params.tokenOut); - - (uint256 reserveIn, uint256 reserveOut) = UniswapV2Library.getReserves( - uniswapV2Factory, - params.tokenIn, - params.tokenOut - ); - - amount = params.amount > 0 - ? UniswapV2Library.getAmountOut(uint256(params.amount), reserveIn, reserveOut) - : UniswapV2Library.getAmountIn(uint256(-params.amount), reserveIn, reserveOut); - - (uint256 amountIn, uint256 amountOut) = params.amount > 0 - ? (uint256(params.amount), amount) - : (amount, uint256(-params.amount)); - - (address token0, ) = UniswapV2Library.sortTokens(params.tokenIn, params.tokenOut); - (uint256 amount0Out, uint256 amount1Out) = params.tokenIn == token0 - ? (uint256(0), amountOut) - : (amountOut, uint256(0)); - - SafeTransferLib.safeTransfer(params.tokenIn, pair, amountIn); - IUniswapV2Pair(pair).swap(amount0Out, amount1Out, params.recipient, bytes("")); - } else { - UniV3Data memory uniV3Data = abi.decode(data, (UniV3Data)); - - // Borrowed logic from https://github.com/Uniswap/v3-periphery/blob/main/contracts/SwapRouter.sol - // exactInputInternal and exactOutputInternal - - bool zeroForOne = params.tokenIn < params.tokenOut; - - IUniswapV3Pool pool = IUniswapV3Pool( - PoolAddress.computeAddress( - uniswapV3Factory, - PoolAddress.getPoolKey(params.tokenIn, params.tokenOut, uniV3Data.fee) - ) - ); - - (int256 amount0, int256 amount1) = pool.swap( - params.recipient, - zeroForOne, - params.amount, - zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, - abi.encode(params.tokenIn) - ); - - if (params.amount > 0) { - amount = uint256(-(zeroForOne ? amount1 : amount0)); - } else { - int256 amountOutReceived; - (amount, amountOutReceived) = zeroForOne ? (uint256(amount0), amount1) : (uint256(amount1), amount0); - require(amountOutReceived == params.amount); - } - } + enum SwapType { + UniswapV2, + UniswapV3 + } + + struct SwapParams { + address tokenIn; + address tokenOut; + int256 amount; // negative corresponds to exact out + address recipient; + } + + struct UniV3Data { + uint24 fee; + } + + function swap(SwapType swapType, SwapParams memory params, bytes memory data) internal returns (uint256 amount) { + if (swapType == SwapType.UniswapV2) { + address pair = UniswapV2Library.pairFor(uniswapV2Factory, params.tokenIn, params.tokenOut); + + (uint256 reserveIn, uint256 reserveOut) = + UniswapV2Library.getReserves(uniswapV2Factory, params.tokenIn, params.tokenOut); + + amount = params.amount > 0 + ? UniswapV2Library.getAmountOut(uint256(params.amount), reserveIn, reserveOut) + : UniswapV2Library.getAmountIn(uint256(-params.amount), reserveIn, reserveOut); + + (uint256 amountIn, uint256 amountOut) = + params.amount > 0 ? (uint256(params.amount), amount) : (amount, uint256(-params.amount)); + + (address token0,) = UniswapV2Library.sortTokens(params.tokenIn, params.tokenOut); + (uint256 amount0Out, uint256 amount1Out) = + params.tokenIn == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0)); + + SafeTransferLib.safeTransfer(params.tokenIn, pair, amountIn); + IUniswapV2Pair(pair).swap(amount0Out, amount1Out, params.recipient, bytes("")); + } else { + UniV3Data memory uniV3Data = abi.decode(data, (UniV3Data)); + + // Borrowed logic from https://github.com/Uniswap/v3-periphery/blob/main/contracts/SwapRouter.sol + // exactInputInternal and exactOutputInternal + + bool zeroForOne = params.tokenIn < params.tokenOut; + + IUniswapV3Pool pool = IUniswapV3Pool( + PoolAddress.computeAddress( + uniswapV3Factory, PoolAddress.getPoolKey(params.tokenIn, params.tokenOut, uniV3Data.fee) + ) + ); + + (int256 amount0, int256 amount1) = pool.swap( + params.recipient, + zeroForOne, + params.amount, + zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, + abi.encode(params.tokenIn) + ); + + if (params.amount > 0) { + amount = uint256(-(zeroForOne ? amount1 : amount0)); + } else { + int256 amountOutReceived; + (amount, amountOutReceived) = zeroForOne ? (uint256(amount0), amount1) : (uint256(amount1), amount0); + require(amountOutReceived == params.amount); + } } + } } diff --git a/src/periphery/UniswapV2/interfaces/IUniswapV2Factory.sol b/src/periphery/UniswapV2/interfaces/IUniswapV2Factory.sol index 07d8f5e..d8943f6 100644 --- a/src/periphery/UniswapV2/interfaces/IUniswapV2Factory.sol +++ b/src/periphery/UniswapV2/interfaces/IUniswapV2Factory.sol @@ -1,25 +1,25 @@ pragma solidity >=0.5.0; interface IUniswapV2Factory { - event PairCreated(address indexed token0, address indexed token1, address pair, uint256); + event PairCreated(address indexed token0, address indexed token1, address pair, uint256); - function feeTo() external view returns (address); + function feeTo() external view returns (address); - function feeToSetter() external view returns (address); + function feeToSetter() external view returns (address); - function migrator() external view returns (address); + function migrator() external view returns (address); - function getPair(address tokenA, address tokenB) external view returns (address pair); + function getPair(address tokenA, address tokenB) external view returns (address pair); - function allPairs(uint256) external view returns (address pair); + function allPairs(uint256) external view returns (address pair); - function allPairsLength() external view returns (uint256); + function allPairsLength() external view returns (uint256); - function createPair(address tokenA, address tokenB) external returns (address pair); + function createPair(address tokenA, address tokenB) external returns (address pair); - function setFeeTo(address) external; + function setFeeTo(address) external; - function setFeeToSetter(address) external; + function setFeeToSetter(address) external; - function setMigrator(address) external; + function setMigrator(address) external; } diff --git a/src/periphery/UniswapV2/interfaces/IUniswapV2Pair.sol b/src/periphery/UniswapV2/interfaces/IUniswapV2Pair.sol index bbbe75e..f166dd2 100644 --- a/src/periphery/UniswapV2/interfaces/IUniswapV2Pair.sol +++ b/src/periphery/UniswapV2/interfaces/IUniswapV2Pair.sol @@ -1,96 +1,81 @@ pragma solidity >=0.5.0; interface IUniswapV2Pair { - event Approval(address indexed owner, address indexed spender, uint256 value); - event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); - function name() external pure returns (string memory); + function name() external pure returns (string memory); - function symbol() external pure returns (string memory); + function symbol() external pure returns (string memory); - function decimals() external pure returns (uint8); + function decimals() external pure returns (uint8); - function totalSupply() external view returns (uint256); + function totalSupply() external view returns (uint256); - function balanceOf(address owner) external view returns (uint256); + function balanceOf(address owner) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); - function approve(address spender, uint256 value) external returns (bool); + function approve(address spender, uint256 value) external returns (bool); - function transfer(address to, 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); + function transferFrom(address from, address to, uint256 value) external returns (bool); - function DOMAIN_SEPARATOR() external view returns (bytes32); + function DOMAIN_SEPARATOR() external view returns (bytes32); - function PERMIT_TYPEHASH() external pure returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); - function nonces(address owner) external view returns (uint256); + function nonces(address owner) external view returns (uint256); - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + external; - event Mint(address indexed sender, uint256 amount0, uint256 amount1); - event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); - event Swap( - address indexed sender, - uint256 amount0In, - uint256 amount1In, - uint256 amount0Out, - uint256 amount1Out, - address indexed to - ); - event Sync(uint112 reserve0, uint112 reserve1); + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); - function MINIMUM_LIQUIDITY() external pure returns (uint256); + function MINIMUM_LIQUIDITY() external pure returns (uint256); - function factory() external view returns (address); + function factory() external view returns (address); - function token0() external view returns (address); + function token0() external view returns (address); - function token1() external view returns (address); + function token1() external view returns (address); - function getReserves() - external - view - returns ( - uint112 reserve0, - uint112 reserve1, - uint32 blockTimestampLast - ); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); - function price0CumulativeLast() external view returns (uint256); + function price0CumulativeLast() external view returns (uint256); - function price1CumulativeLast() external view returns (uint256); + function price1CumulativeLast() external view returns (uint256); - function kLast() external view returns (uint256); + function kLast() external view returns (uint256); - function mint(address to) external returns (uint256 liquidity); + function mint(address to) external returns (uint256 liquidity); - function burn(address to) external returns (uint256 amount0, uint256 amount1); + function burn(address to) external returns (uint256 amount0, uint256 amount1); - function swap( - uint256 amount0Out, - uint256 amount1Out, - address to, - bytes calldata data - ) external; + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; - function skim(address to) external; + function skim(address to) external; - function sync() external; + function sync() external; - function initialize(address, address) external; + function initialize(address, address) external; } diff --git a/src/periphery/UniswapV2/libraries/UniswapV2Library.sol b/src/periphery/UniswapV2/libraries/UniswapV2Library.sol index 283b640..e43c8bb 100644 --- a/src/periphery/UniswapV2/libraries/UniswapV2Library.sol +++ b/src/periphery/UniswapV2/libraries/UniswapV2Library.sol @@ -3,71 +3,79 @@ pragma solidity >=0.5.0; import { IUniswapV2Pair } from "../interfaces/IUniswapV2Pair.sol"; library UniswapV2Library { - // returns sorted token addresses, used to handle return values from pairs sorted in this order - function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { - require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES"); - (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); - require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS"); - } + // returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES"); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS"); + } - // calculates the CREATE2 address for a pair without making any external calls - function pairFor( - address factory, - address tokenA, - address tokenB - ) internal pure returns (address pair) { - (address token0, address token1) = sortTokens(tokenA, tokenB); - pair = address( - uint160( // extra cast for newer solidity - uint256( - keccak256( - abi.encodePacked( - hex"ff", - factory, - keccak256(abi.encodePacked(token0, token1)), - hex"e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" // init code hash - ) - ) - ) + // calculates the CREATE2 address for a pair without making any external calls + function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = address( + uint160( // extra cast for newer solidity + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encodePacked(token0, token1)), + hex"e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303" // init code hash ) - ); - } + ) + ) + ) + ); + } - // fetches and sorts the reserves for a pair - function getReserves( - address factory, - address tokenA, - address tokenB - ) internal view returns (uint256 reserveA, uint256 reserveB) { - (address token0, ) = sortTokens(tokenA, tokenB); - (uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); - (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); - } + // fetches and sorts the reserves for a pair + function getReserves( + address factory, + address tokenA, + address tokenB + ) + internal + view + returns (uint256 reserveA, uint256 reserveB) + { + (address token0,) = sortTokens(tokenA, tokenB); + (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } - // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset - function getAmountOut( - uint256 amountIn, - uint256 reserveIn, - uint256 reserveOut - ) internal pure returns (uint256 amountOut) { - require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT"); - require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); - uint256 amountInWithFee = amountIn * 997; - uint256 numerator = amountInWithFee * reserveOut; - uint256 denominator = (reserveIn * 1000) + amountInWithFee; - amountOut = numerator / denominator; - } + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut( + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut + ) + internal + pure + returns (uint256 amountOut) + { + require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); + uint256 amountInWithFee = amountIn * 997; + uint256 numerator = amountInWithFee * reserveOut; + uint256 denominator = (reserveIn * 1000) + amountInWithFee; + amountOut = numerator / denominator; + } - // given an output amount of an asset and pair reserves, returns a required input amount of the other asset - function getAmountIn( - uint256 amountOut, - uint256 reserveIn, - uint256 reserveOut - ) internal pure returns (uint256 amountIn) { - require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT"); - require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); - uint256 numerator = reserveIn * amountOut * 1000; - uint256 denominator = (reserveOut - amountOut) * 997; - amountIn = (numerator / denominator) + 1; - } + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn( + uint256 amountOut, + uint256 reserveIn, + uint256 reserveOut + ) + internal + pure + returns (uint256 amountIn) + { + require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT"); + require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); + uint256 numerator = reserveIn * amountOut * 1000; + uint256 denominator = (reserveOut - amountOut) * 997; + amountIn = (numerator / denominator) + 1; + } } diff --git a/src/periphery/UniswapV3/interfaces/IUniswapV3Factory.sol b/src/periphery/UniswapV3/interfaces/IUniswapV3Factory.sol index 540cfdc..d366717 100644 --- a/src/periphery/UniswapV3/interfaces/IUniswapV3Factory.sol +++ b/src/periphery/UniswapV3/interfaces/IUniswapV3Factory.sol @@ -4,75 +4,63 @@ pragma solidity >=0.5.0; /// @title The interface for the Uniswap V3 Factory /// @notice The Uniswap V3 Factory facilitates creation of Uniswap V3 pools and control over the protocol fees interface IUniswapV3Factory { - /// @notice Emitted when the owner of the factory is changed - /// @param oldOwner The owner before the owner was changed - /// @param newOwner The owner after the owner was changed - event OwnerChanged(address indexed oldOwner, address indexed newOwner); + /// @notice Emitted when the owner of the factory is changed + /// @param oldOwner The owner before the owner was changed + /// @param newOwner The owner after the owner was changed + event OwnerChanged(address indexed oldOwner, address indexed newOwner); - /// @notice Emitted when a pool is created - /// @param token0 The first token of the pool by address sort order - /// @param token1 The second token of the pool by address sort order - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @param tickSpacing The minimum number of ticks between initialized ticks - /// @param pool The address of the created pool - event PoolCreated( - address indexed token0, - address indexed token1, - uint24 indexed fee, - int24 tickSpacing, - address pool - ); + /// @notice Emitted when a pool is created + /// @param token0 The first token of the pool by address sort order + /// @param token1 The second token of the pool by address sort order + /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip + /// @param tickSpacing The minimum number of ticks between initialized ticks + /// @param pool The address of the created pool + event PoolCreated( + address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool + ); - /// @notice Emitted when a new fee amount is enabled for pool creation via the factory - /// @param fee The enabled fee, denominated in hundredths of a bip - /// @param tickSpacing The minimum number of ticks between initialized ticks for pools created with the given fee - event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); + /// @notice Emitted when a new fee amount is enabled for pool creation via the factory + /// @param fee The enabled fee, denominated in hundredths of a bip + /// @param tickSpacing The minimum number of ticks between initialized ticks for pools created with the given fee + event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); - /// @notice Returns the current owner of the factory - /// @dev Can be changed by the current owner via setOwner - /// @return The address of the factory owner - function owner() external view returns (address); + /// @notice Returns the current owner of the factory + /// @dev Can be changed by the current owner via setOwner + /// @return The address of the factory owner + function owner() external view returns (address); - /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled - /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context - /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee - /// @return The tick spacing - function feeAmountTickSpacing(uint24 fee) external view returns (int24); + /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled + /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context + /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee + /// @return The tick spacing + function feeAmountTickSpacing(uint24 fee) external view returns (int24); - /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist - /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order - /// @param tokenA The contract address of either token0 or token1 - /// @param tokenB The contract address of the other token - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @return pool The pool address - function getPool( - address tokenA, - address tokenB, - uint24 fee - ) external view returns (address pool); + /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist + /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order + /// @param tokenA The contract address of either token0 or token1 + /// @param tokenB The contract address of the other token + /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip + /// @return pool The pool address + function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool); - /// @notice Creates a pool for the given two tokens and fee - /// @param tokenA One of the two tokens in the desired pool - /// @param tokenB The other of the two tokens in the desired pool - /// @param fee The desired fee for the pool - /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved - /// from the fee. The call will revert if the pool already exists, the fee is invalid, or the token arguments - /// are invalid. - /// @return pool The address of the newly created pool - function createPool( - address tokenA, - address tokenB, - uint24 fee - ) external returns (address pool); + /// @notice Creates a pool for the given two tokens and fee + /// @param tokenA One of the two tokens in the desired pool + /// @param tokenB The other of the two tokens in the desired pool + /// @param fee The desired fee for the pool + /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved + /// from the fee. The call will revert if the pool already exists, the fee is invalid, or the token arguments + /// are invalid. + /// @return pool The address of the newly created pool + function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); - /// @notice Updates the owner of the factory - /// @dev Must be called by the current owner - /// @param _owner The new owner of the factory - function setOwner(address _owner) external; + /// @notice Updates the owner of the factory + /// @dev Must be called by the current owner + /// @param _owner The new owner of the factory + function setOwner(address _owner) external; - /// @notice Enables a fee amount with the given tickSpacing - /// @dev Fee amounts may never be removed once enabled - /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6) - /// @param tickSpacing The spacing between ticks to be enforced for all pools created with the given fee amount - function enableFeeAmount(uint24 fee, int24 tickSpacing) external; + /// @notice Enables a fee amount with the given tickSpacing + /// @dev Fee amounts may never be removed once enabled + /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6) + /// @param tickSpacing The spacing between ticks to be enforced for all pools created with the given fee amount + function enableFeeAmount(uint24 fee, int24 tickSpacing) external; } diff --git a/src/periphery/UniswapV3/interfaces/IUniswapV3Pool.sol b/src/periphery/UniswapV3/interfaces/IUniswapV3Pool.sol index a2239bd..1999f7a 100644 --- a/src/periphery/UniswapV3/interfaces/IUniswapV3Pool.sol +++ b/src/periphery/UniswapV3/interfaces/IUniswapV3Pool.sol @@ -13,12 +13,10 @@ import "./pool/IUniswapV3PoolEvents.sol"; /// to the ERC20 specification /// @dev The pool interface is broken up into many smaller pieces interface IUniswapV3Pool is - IUniswapV3PoolImmutables, - IUniswapV3PoolState, - IUniswapV3PoolDerivedState, - IUniswapV3PoolActions, - IUniswapV3PoolOwnerActions, - IUniswapV3PoolEvents -{ - -} + IUniswapV3PoolImmutables, + IUniswapV3PoolState, + IUniswapV3PoolDerivedState, + IUniswapV3PoolActions, + IUniswapV3PoolOwnerActions, + IUniswapV3PoolEvents +{ } diff --git a/src/periphery/UniswapV3/interfaces/callback/IUniswapV3SwapCallback.sol b/src/periphery/UniswapV3/interfaces/callback/IUniswapV3SwapCallback.sol index 9f183b2..953c686 100644 --- a/src/periphery/UniswapV3/interfaces/callback/IUniswapV3SwapCallback.sol +++ b/src/periphery/UniswapV3/interfaces/callback/IUniswapV3SwapCallback.sol @@ -4,18 +4,14 @@ pragma solidity >=0.5.0; /// @title Callback for IUniswapV3PoolActions#swap /// @notice Any contract that calls IUniswapV3PoolActions#swap must implement this interface interface IUniswapV3SwapCallback { - /// @notice Called to `msg.sender` after executing a swap via IUniswapV3Pool#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory. - /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call - function uniswapV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external; + /// @notice Called to `msg.sender` after executing a swap via IUniswapV3Pool#swap. + /// @dev In the implementation you must pay the pool tokens owed for the swap. + /// The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory. + /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. + /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by + /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. + /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by + /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. + /// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call + function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external; } diff --git a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolActions.sol b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolActions.sol index 44fb61c..14be213 100644 --- a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolActions.sol +++ b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolActions.sol @@ -4,100 +4,100 @@ pragma solidity >=0.5.0; /// @title Permissionless pool actions /// @notice Contains pool methods that can be called by anyone interface IUniswapV3PoolActions { - /// @notice Sets the initial price for the pool - /// @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value - /// @param sqrtPriceX96 the initial sqrt price of the pool as a Q64.96 - function initialize(uint160 sqrtPriceX96) external; + /// @notice Sets the initial price for the pool + /// @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value + /// @param sqrtPriceX96 the initial sqrt price of the pool as a Q64.96 + function initialize(uint160 sqrtPriceX96) external; - /// @notice Adds liquidity for the given recipient/tickLower/tickUpper position - /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback - /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends - /// on tickLower, tickUpper, the amount of liquidity, and the current price. - /// @param recipient The address for which the liquidity will be created - /// @param tickLower The lower tick of the position in which to add liquidity - /// @param tickUpper The upper tick of the position in which to add liquidity - /// @param amount The amount of liquidity to mint - /// @param data Any data that should be passed through to the callback - /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback - /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback - function mint( - address recipient, - int24 tickLower, - int24 tickUpper, - uint128 amount, - bytes calldata data - ) external returns (uint256 amount0, uint256 amount1); + /// @notice Adds liquidity for the given recipient/tickLower/tickUpper position + /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback + /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends + /// on tickLower, tickUpper, the amount of liquidity, and the current price. + /// @param recipient The address for which the liquidity will be created + /// @param tickLower The lower tick of the position in which to add liquidity + /// @param tickUpper The upper tick of the position in which to add liquidity + /// @param amount The amount of liquidity to mint + /// @param data Any data that should be passed through to the callback + /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in + /// the callback + /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in + /// the callback + function mint( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount, + bytes calldata data + ) + external + returns (uint256 amount0, uint256 amount1); - /// @notice Collects tokens owed to a position - /// @dev Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity. - /// Collect must be called by the position owner. To withdraw only token0 or only token1, amount0Requested or - /// amount1Requested may be set to zero. To withdraw all tokens owed, caller may pass any value greater than the - /// actual tokens owed, e.g. type(uint128).max. Tokens owed may be from accumulated swap fees or burned liquidity. - /// @param recipient The address which should receive the fees collected - /// @param tickLower The lower tick of the position for which to collect fees - /// @param tickUpper The upper tick of the position for which to collect fees - /// @param amount0Requested How much token0 should be withdrawn from the fees owed - /// @param amount1Requested How much token1 should be withdrawn from the fees owed - /// @return amount0 The amount of fees collected in token0 - /// @return amount1 The amount of fees collected in token1 - function collect( - address recipient, - int24 tickLower, - int24 tickUpper, - uint128 amount0Requested, - uint128 amount1Requested - ) external returns (uint128 amount0, uint128 amount1); + /// @notice Collects tokens owed to a position + /// @dev Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity. + /// Collect must be called by the position owner. To withdraw only token0 or only token1, amount0Requested or + /// amount1Requested may be set to zero. To withdraw all tokens owed, caller may pass any value greater than the + /// actual tokens owed, e.g. type(uint128).max. Tokens owed may be from accumulated swap fees or burned liquidity. + /// @param recipient The address which should receive the fees collected + /// @param tickLower The lower tick of the position for which to collect fees + /// @param tickUpper The upper tick of the position for which to collect fees + /// @param amount0Requested How much token0 should be withdrawn from the fees owed + /// @param amount1Requested How much token1 should be withdrawn from the fees owed + /// @return amount0 The amount of fees collected in token0 + /// @return amount1 The amount of fees collected in token1 + function collect( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount0Requested, + uint128 amount1Requested + ) + external + returns (uint128 amount0, uint128 amount1); - /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position - /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0 - /// @dev Fees must be collected separately via a call to #collect - /// @param tickLower The lower tick of the position for which to burn liquidity - /// @param tickUpper The upper tick of the position for which to burn liquidity - /// @param amount How much liquidity to burn - /// @return amount0 The amount of token0 sent to the recipient - /// @return amount1 The amount of token1 sent to the recipient - function burn( - int24 tickLower, - int24 tickUpper, - uint128 amount - ) external returns (uint256 amount0, uint256 amount1); + /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position + /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0 + /// @dev Fees must be collected separately via a call to #collect + /// @param tickLower The lower tick of the position for which to burn liquidity + /// @param tickUpper The upper tick of the position for which to burn liquidity + /// @param amount How much liquidity to burn + /// @return amount0 The amount of token0 sent to the recipient + /// @return amount1 The amount of token1 sent to the recipient + function burn(int24 tickLower, int24 tickUpper, uint128 amount) external returns (uint256 amount0, uint256 amount1); - /// @notice Swap token0 for token1, or token1 for token0 - /// @dev The caller of this method receives a callback in the form of IUniswapV3SwapCallback#uniswapV3SwapCallback - /// @param recipient The address to receive the output of the swap - /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 - /// @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) - /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this - /// value after the swap. If one for zero, the price cannot be greater than this value after the swap - /// @param data Any data to be passed through to the callback - /// @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive - /// @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive - function swap( - address recipient, - bool zeroForOne, - int256 amountSpecified, - uint160 sqrtPriceLimitX96, - bytes calldata data - ) external returns (int256 amount0, int256 amount1); + /// @notice Swap token0 for token1, or token1 for token0 + /// @dev The caller of this method receives a callback in the form of IUniswapV3SwapCallback#uniswapV3SwapCallback + /// @param recipient The address to receive the output of the swap + /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 + /// @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), + /// or exact output (negative) + /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this + /// value after the swap. If one for zero, the price cannot be greater than this value after the swap + /// @param data Any data to be passed through to the callback + /// @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive + /// @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes calldata data + ) + external + returns (int256 amount0, int256 amount1); - /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback - /// @dev The caller of this method receives a callback in the form of IUniswapV3FlashCallback#uniswapV3FlashCallback - /// @dev Can be used to donate underlying tokens pro-rata to currently in-range liquidity providers by calling - /// with 0 amount{0,1} and sending the donation amount(s) from the callback - /// @param recipient The address which will receive the token0 and token1 amounts - /// @param amount0 The amount of token0 to send - /// @param amount1 The amount of token1 to send - /// @param data Any data to be passed through to the callback - function flash( - address recipient, - uint256 amount0, - uint256 amount1, - bytes calldata data - ) external; + /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback + /// @dev The caller of this method receives a callback in the form of IUniswapV3FlashCallback#uniswapV3FlashCallback + /// @dev Can be used to donate underlying tokens pro-rata to currently in-range liquidity providers by calling + /// with 0 amount{0,1} and sending the donation amount(s) from the callback + /// @param recipient The address which will receive the token0 and token1 amounts + /// @param amount0 The amount of token0 to send + /// @param amount1 The amount of token1 to send + /// @param data Any data to be passed through to the callback + function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external; - /// @notice Increase the maximum number of price and liquidity observations that this pool will store - /// @dev This method is no-op if the pool already has an observationCardinalityNext greater than or equal to - /// the input observationCardinalityNext. - /// @param observationCardinalityNext The desired minimum number of observations for the pool to store - function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; + /// @notice Increase the maximum number of price and liquidity observations that this pool will store + /// @dev This method is no-op if the pool already has an observationCardinalityNext greater than or equal to + /// the input observationCardinalityNext. + /// @param observationCardinalityNext The desired minimum number of observations for the pool to store + function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; } diff --git a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolDerivedState.sol b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolDerivedState.sol index eda3a00..da412ae 100644 --- a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolDerivedState.sol +++ b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolDerivedState.sol @@ -5,36 +5,39 @@ pragma solidity >=0.5.0; /// @notice Contains view functions to provide information about the pool that is computed rather than stored on the /// blockchain. The functions here may have variable gas costs. interface IUniswapV3PoolDerivedState { - /// @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block timestamp - /// @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one representing - /// the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted average tick, - /// you must call it with secondsAgos = [3600, 0]. - /// @dev The time weighted average tick represents the geometric time weighted average price of the pool, in - /// log base sqrt(1.0001) of token1 / token0. The TickMath library can be used to go from a tick value to a ratio. - /// @param secondsAgos From how long ago each cumulative tick and liquidity value should be returned - /// @return tickCumulatives Cumulative tick values as of each `secondsAgos` from the current block timestamp - /// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity-in-range value as of each `secondsAgos` from the current block - /// timestamp - function observe(uint32[] calldata secondsAgos) - external - view - returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); + /// @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block + /// timestamp + /// @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one + /// representing + /// the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted + /// average tick, + /// you must call it with secondsAgos = [3600, 0]. + /// @dev The time weighted average tick represents the geometric time weighted average price of the pool, in + /// log base sqrt(1.0001) of token1 / token0. The TickMath library can be used to go from a tick value to a ratio. + /// @param secondsAgos From how long ago each cumulative tick and liquidity value should be returned + /// @return tickCumulatives Cumulative tick values as of each `secondsAgos` from the current block timestamp + /// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity-in-range value as of each + /// `secondsAgos` from the current block + /// timestamp + function observe(uint32[] calldata secondsAgos) + external + view + returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); - /// @notice Returns a snapshot of the tick cumulative, seconds per liquidity and seconds inside a tick range - /// @dev Snapshots must only be compared to other snapshots, taken over a period for which a position existed. - /// I.e., snapshots cannot be compared if a position is not held for the entire period between when the first - /// snapshot is taken and the second snapshot is taken. - /// @param tickLower The lower tick of the range - /// @param tickUpper The upper tick of the range - /// @return tickCumulativeInside The snapshot of the tick accumulator for the range - /// @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range - /// @return secondsInside The snapshot of seconds per liquidity for the range - function snapshotCumulativesInside(int24 tickLower, int24 tickUpper) - external - view - returns ( - int56 tickCumulativeInside, - uint160 secondsPerLiquidityInsideX128, - uint32 secondsInside - ); + /// @notice Returns a snapshot of the tick cumulative, seconds per liquidity and seconds inside a tick range + /// @dev Snapshots must only be compared to other snapshots, taken over a period for which a position existed. + /// I.e., snapshots cannot be compared if a position is not held for the entire period between when the first + /// snapshot is taken and the second snapshot is taken. + /// @param tickLower The lower tick of the range + /// @param tickUpper The upper tick of the range + /// @return tickCumulativeInside The snapshot of the tick accumulator for the range + /// @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range + /// @return secondsInside The snapshot of seconds per liquidity for the range + function snapshotCumulativesInside( + int24 tickLower, + int24 tickUpper + ) + external + view + returns (int56 tickCumulativeInside, uint160 secondsPerLiquidityInsideX128, uint32 secondsInside); } diff --git a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolEvents.sol b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolEvents.sol index 9d915dd..7059fad 100644 --- a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolEvents.sol +++ b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolEvents.sol @@ -4,118 +4,110 @@ pragma solidity >=0.5.0; /// @title Events emitted by a pool /// @notice Contains all events emitted by the pool interface IUniswapV3PoolEvents { - /// @notice Emitted exactly once by a pool when #initialize is first called on the pool - /// @dev Mint/Burn/Swap cannot be emitted by the pool before Initialize - /// @param sqrtPriceX96 The initial sqrt price of the pool, as a Q64.96 - /// @param tick The initial tick of the pool, i.e. log base 1.0001 of the starting price of the pool - event Initialize(uint160 sqrtPriceX96, int24 tick); + /// @notice Emitted exactly once by a pool when #initialize is first called on the pool + /// @dev Mint/Burn/Swap cannot be emitted by the pool before Initialize + /// @param sqrtPriceX96 The initial sqrt price of the pool, as a Q64.96 + /// @param tick The initial tick of the pool, i.e. log base 1.0001 of the starting price of the pool + event Initialize(uint160 sqrtPriceX96, int24 tick); - /// @notice Emitted when liquidity is minted for a given position - /// @param sender The address that minted the liquidity - /// @param owner The owner of the position and recipient of any minted liquidity - /// @param tickLower The lower tick of the position - /// @param tickUpper The upper tick of the position - /// @param amount The amount of liquidity minted to the position range - /// @param amount0 How much token0 was required for the minted liquidity - /// @param amount1 How much token1 was required for the minted liquidity - event Mint( - address sender, - address indexed owner, - int24 indexed tickLower, - int24 indexed tickUpper, - uint128 amount, - uint256 amount0, - uint256 amount1 - ); + /// @notice Emitted when liquidity is minted for a given position + /// @param sender The address that minted the liquidity + /// @param owner The owner of the position and recipient of any minted liquidity + /// @param tickLower The lower tick of the position + /// @param tickUpper The upper tick of the position + /// @param amount The amount of liquidity minted to the position range + /// @param amount0 How much token0 was required for the minted liquidity + /// @param amount1 How much token1 was required for the minted liquidity + event Mint( + address sender, + address indexed owner, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount, + uint256 amount0, + uint256 amount1 + ); - /// @notice Emitted when fees are collected by the owner of a position - /// @dev Collect events may be emitted with zero amount0 and amount1 when the caller chooses not to collect fees - /// @param owner The owner of the position for which fees are collected - /// @param tickLower The lower tick of the position - /// @param tickUpper The upper tick of the position - /// @param amount0 The amount of token0 fees collected - /// @param amount1 The amount of token1 fees collected - event Collect( - address indexed owner, - address recipient, - int24 indexed tickLower, - int24 indexed tickUpper, - uint128 amount0, - uint128 amount1 - ); + /// @notice Emitted when fees are collected by the owner of a position + /// @dev Collect events may be emitted with zero amount0 and amount1 when the caller chooses not to collect fees + /// @param owner The owner of the position for which fees are collected + /// @param tickLower The lower tick of the position + /// @param tickUpper The upper tick of the position + /// @param amount0 The amount of token0 fees collected + /// @param amount1 The amount of token1 fees collected + event Collect( + address indexed owner, + address recipient, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount0, + uint128 amount1 + ); - /// @notice Emitted when a position's liquidity is removed - /// @dev Does not withdraw any fees earned by the liquidity position, which must be withdrawn via #collect - /// @param owner The owner of the position for which liquidity is removed - /// @param tickLower The lower tick of the position - /// @param tickUpper The upper tick of the position - /// @param amount The amount of liquidity to remove - /// @param amount0 The amount of token0 withdrawn - /// @param amount1 The amount of token1 withdrawn - event Burn( - address indexed owner, - int24 indexed tickLower, - int24 indexed tickUpper, - uint128 amount, - uint256 amount0, - uint256 amount1 - ); + /// @notice Emitted when a position's liquidity is removed + /// @dev Does not withdraw any fees earned by the liquidity position, which must be withdrawn via #collect + /// @param owner The owner of the position for which liquidity is removed + /// @param tickLower The lower tick of the position + /// @param tickUpper The upper tick of the position + /// @param amount The amount of liquidity to remove + /// @param amount0 The amount of token0 withdrawn + /// @param amount1 The amount of token1 withdrawn + event Burn( + address indexed owner, + int24 indexed tickLower, + int24 indexed tickUpper, + uint128 amount, + uint256 amount0, + uint256 amount1 + ); - /// @notice Emitted by the pool for any swaps between token0 and token1 - /// @param sender The address that initiated the swap call, and that received the callback - /// @param recipient The address that received the output of the swap - /// @param amount0 The delta of the token0 balance of the pool - /// @param amount1 The delta of the token1 balance of the pool - /// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96 - /// @param liquidity The liquidity of the pool after the swap - /// @param tick The log base 1.0001 of price of the pool after the swap - event Swap( - address indexed sender, - address indexed recipient, - int256 amount0, - int256 amount1, - uint160 sqrtPriceX96, - uint128 liquidity, - int24 tick - ); + /// @notice Emitted by the pool for any swaps between token0 and token1 + /// @param sender The address that initiated the swap call, and that received the callback + /// @param recipient The address that received the output of the swap + /// @param amount0 The delta of the token0 balance of the pool + /// @param amount1 The delta of the token1 balance of the pool + /// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96 + /// @param liquidity The liquidity of the pool after the swap + /// @param tick The log base 1.0001 of price of the pool after the swap + event Swap( + address indexed sender, + address indexed recipient, + int256 amount0, + int256 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick + ); - /// @notice Emitted by the pool for any flashes of token0/token1 - /// @param sender The address that initiated the swap call, and that received the callback - /// @param recipient The address that received the tokens from flash - /// @param amount0 The amount of token0 that was flashed - /// @param amount1 The amount of token1 that was flashed - /// @param paid0 The amount of token0 paid for the flash, which can exceed the amount0 plus the fee - /// @param paid1 The amount of token1 paid for the flash, which can exceed the amount1 plus the fee - event Flash( - address indexed sender, - address indexed recipient, - uint256 amount0, - uint256 amount1, - uint256 paid0, - uint256 paid1 - ); + /// @notice Emitted by the pool for any flashes of token0/token1 + /// @param sender The address that initiated the swap call, and that received the callback + /// @param recipient The address that received the tokens from flash + /// @param amount0 The amount of token0 that was flashed + /// @param amount1 The amount of token1 that was flashed + /// @param paid0 The amount of token0 paid for the flash, which can exceed the amount0 plus the fee + /// @param paid1 The amount of token1 paid for the flash, which can exceed the amount1 plus the fee + event Flash( + address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1, uint256 paid0, uint256 paid1 + ); - /// @notice Emitted by the pool for increases to the number of observations that can be stored - /// @dev observationCardinalityNext is not the observation cardinality until an observation is written at the index - /// just before a mint/swap/burn. - /// @param observationCardinalityNextOld The previous value of the next observation cardinality - /// @param observationCardinalityNextNew The updated value of the next observation cardinality - event IncreaseObservationCardinalityNext( - uint16 observationCardinalityNextOld, - uint16 observationCardinalityNextNew - ); + /// @notice Emitted by the pool for increases to the number of observations that can be stored + /// @dev observationCardinalityNext is not the observation cardinality until an observation is written at the index + /// just before a mint/swap/burn. + /// @param observationCardinalityNextOld The previous value of the next observation cardinality + /// @param observationCardinalityNextNew The updated value of the next observation cardinality + event IncreaseObservationCardinalityNext(uint16 observationCardinalityNextOld, uint16 observationCardinalityNextNew); - /// @notice Emitted when the protocol fee is changed by the pool - /// @param feeProtocol0Old The previous value of the token0 protocol fee - /// @param feeProtocol1Old The previous value of the token1 protocol fee - /// @param feeProtocol0New The updated value of the token0 protocol fee - /// @param feeProtocol1New The updated value of the token1 protocol fee - event SetFeeProtocol(uint8 feeProtocol0Old, uint8 feeProtocol1Old, uint8 feeProtocol0New, uint8 feeProtocol1New); + /// @notice Emitted when the protocol fee is changed by the pool + /// @param feeProtocol0Old The previous value of the token0 protocol fee + /// @param feeProtocol1Old The previous value of the token1 protocol fee + /// @param feeProtocol0New The updated value of the token0 protocol fee + /// @param feeProtocol1New The updated value of the token1 protocol fee + event SetFeeProtocol(uint8 feeProtocol0Old, uint8 feeProtocol1Old, uint8 feeProtocol0New, uint8 feeProtocol1New); - /// @notice Emitted when the collected protocol fees are withdrawn by the factory owner - /// @param sender The address that collects the protocol fees - /// @param recipient The address that receives the collected protocol fees - /// @param amount0 The amount of token0 protocol fees that is withdrawn - /// @param amount0 The amount of token1 protocol fees that is withdrawn - event CollectProtocol(address indexed sender, address indexed recipient, uint128 amount0, uint128 amount1); + /// @notice Emitted when the collected protocol fees are withdrawn by the factory owner + /// @param sender The address that collects the protocol fees + /// @param recipient The address that receives the collected protocol fees + /// @param amount0 The amount of token0 protocol fees that is withdrawn + /// @param amount0 The amount of token1 protocol fees that is withdrawn + event CollectProtocol(address indexed sender, address indexed recipient, uint128 amount0, uint128 amount1); } diff --git a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolImmutables.sol b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolImmutables.sol index c9beb15..0ba3c89 100644 --- a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolImmutables.sol +++ b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolImmutables.sol @@ -4,32 +4,32 @@ pragma solidity >=0.5.0; /// @title Pool state that never changes /// @notice These parameters are fixed for a pool forever, i.e., the methods will always return the same values interface IUniswapV3PoolImmutables { - /// @notice The contract that deployed the pool, which must adhere to the IUniswapV3Factory interface - /// @return The contract address - function factory() external view returns (address); + /// @notice The contract that deployed the pool, which must adhere to the IUniswapV3Factory interface + /// @return The contract address + function factory() external view returns (address); - /// @notice The first of the two tokens of the pool, sorted by address - /// @return The token contract address - function token0() external view returns (address); + /// @notice The first of the two tokens of the pool, sorted by address + /// @return The token contract address + function token0() external view returns (address); - /// @notice The second of the two tokens of the pool, sorted by address - /// @return The token contract address - function token1() external view returns (address); + /// @notice The second of the two tokens of the pool, sorted by address + /// @return The token contract address + function token1() external view returns (address); - /// @notice The pool's fee in hundredths of a bip, i.e. 1e-6 - /// @return The fee - function fee() external view returns (uint24); + /// @notice The pool's fee in hundredths of a bip, i.e. 1e-6 + /// @return The fee + function fee() external view returns (uint24); - /// @notice The pool tick spacing - /// @dev Ticks can only be used at multiples of this value, minimum of 1 and always positive - /// e.g.: a tickSpacing of 3 means ticks can be initialized every 3rd tick, i.e., ..., -6, -3, 0, 3, 6, ... - /// This value is an int24 to avoid casting even though it is always positive. - /// @return The tick spacing - function tickSpacing() external view returns (int24); + /// @notice The pool tick spacing + /// @dev Ticks can only be used at multiples of this value, minimum of 1 and always positive + /// e.g.: a tickSpacing of 3 means ticks can be initialized every 3rd tick, i.e., ..., -6, -3, 0, 3, 6, ... + /// This value is an int24 to avoid casting even though it is always positive. + /// @return The tick spacing + function tickSpacing() external view returns (int24); - /// @notice The maximum amount of position liquidity that can use any tick in the range - /// @dev This parameter is enforced per tick to prevent liquidity from overflowing a uint128 at any point, and - /// also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool - /// @return The max amount of liquidity per tick - function maxLiquidityPerTick() external view returns (uint128); + /// @notice The maximum amount of position liquidity that can use any tick in the range + /// @dev This parameter is enforced per tick to prevent liquidity from overflowing a uint128 at any point, and + /// also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool + /// @return The max amount of liquidity per tick + function maxLiquidityPerTick() external view returns (uint128); } diff --git a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolOwnerActions.sol b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolOwnerActions.sol index 2395ed3..e802db3 100644 --- a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolOwnerActions.sol +++ b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolOwnerActions.sol @@ -4,20 +4,22 @@ pragma solidity >=0.5.0; /// @title Permissioned pool actions /// @notice Contains pool methods that may only be called by the factory owner interface IUniswapV3PoolOwnerActions { - /// @notice Set the denominator of the protocol's % share of the fees - /// @param feeProtocol0 new protocol fee for token0 of the pool - /// @param feeProtocol1 new protocol fee for token1 of the pool - function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external; + /// @notice Set the denominator of the protocol's % share of the fees + /// @param feeProtocol0 new protocol fee for token0 of the pool + /// @param feeProtocol1 new protocol fee for token1 of the pool + function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external; - /// @notice Collect the protocol fee accrued to the pool - /// @param recipient The address to which collected protocol fees should be sent - /// @param amount0Requested The maximum amount of token0 to send, can be 0 to collect fees in only token1 - /// @param amount1Requested The maximum amount of token1 to send, can be 0 to collect fees in only token0 - /// @return amount0 The protocol fee collected in token0 - /// @return amount1 The protocol fee collected in token1 - function collectProtocol( - address recipient, - uint128 amount0Requested, - uint128 amount1Requested - ) external returns (uint128 amount0, uint128 amount1); + /// @notice Collect the protocol fee accrued to the pool + /// @param recipient The address to which collected protocol fees should be sent + /// @param amount0Requested The maximum amount of token0 to send, can be 0 to collect fees in only token1 + /// @param amount1Requested The maximum amount of token1 to send, can be 0 to collect fees in only token0 + /// @return amount0 The protocol fee collected in token0 + /// @return amount1 The protocol fee collected in token1 + function collectProtocol( + address recipient, + uint128 amount0Requested, + uint128 amount1Requested + ) + external + returns (uint128 amount0, uint128 amount1); } diff --git a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolState.sol b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolState.sol index 620256c..0a999ae 100644 --- a/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolState.sol +++ b/src/periphery/UniswapV3/interfaces/pool/IUniswapV3PoolState.sol @@ -5,112 +5,114 @@ pragma solidity >=0.5.0; /// @notice These methods compose the pool's state, and can change with any frequency including multiple times /// per transaction interface IUniswapV3PoolState { - /// @notice The 0th storage slot in the pool stores many values, and is exposed as a single method to save gas - /// when accessed externally. - /// @return sqrtPriceX96 The current price of the pool as a sqrt(token1/token0) Q64.96 value - /// tick The current tick of the pool, i.e. according to the last tick transition that was run. - /// This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick - /// boundary. - /// observationIndex The index of the last oracle observation that was written, - /// observationCardinality The current maximum number of observations stored in the pool, - /// observationCardinalityNext The next maximum number of observations, to be updated when the observation. - /// feeProtocol The protocol fee for both tokens of the pool. - /// Encoded as two 4 bit values, where the protocol fee of token1 is shifted 4 bits and the protocol fee of token0 - /// is the lower 4 bits. Used as the denominator of a fraction of the swap fee, e.g. 4 means 1/4th of the swap fee. - /// unlocked Whether the pool is currently locked to reentrancy - function slot0() - external - view - returns ( - uint160 sqrtPriceX96, - int24 tick, - uint16 observationIndex, - uint16 observationCardinality, - uint16 observationCardinalityNext, - uint8 feeProtocol, - bool unlocked - ); + /// @notice The 0th storage slot in the pool stores many values, and is exposed as a single method to save gas + /// when accessed externally. + /// @return sqrtPriceX96 The current price of the pool as a sqrt(token1/token0) Q64.96 value + /// tick The current tick of the pool, i.e. according to the last tick transition that was run. + /// This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick + /// boundary. + /// observationIndex The index of the last oracle observation that was written, + /// observationCardinality The current maximum number of observations stored in the pool, + /// observationCardinalityNext The next maximum number of observations, to be updated when the observation. + /// feeProtocol The protocol fee for both tokens of the pool. + /// Encoded as two 4 bit values, where the protocol fee of token1 is shifted 4 bits and the protocol fee of token0 + /// is the lower 4 bits. Used as the denominator of a fraction of the swap fee, e.g. 4 means 1/4th of the swap fee. + /// unlocked Whether the pool is currently locked to reentrancy + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ); - /// @notice The fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the pool - /// @dev This value can overflow the uint256 - function feeGrowthGlobal0X128() external view returns (uint256); + /// @notice The fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the + /// pool + /// @dev This value can overflow the uint256 + function feeGrowthGlobal0X128() external view returns (uint256); - /// @notice The fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the pool - /// @dev This value can overflow the uint256 - function feeGrowthGlobal1X128() external view returns (uint256); + /// @notice The fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the + /// pool + /// @dev This value can overflow the uint256 + function feeGrowthGlobal1X128() external view returns (uint256); - /// @notice The amounts of token0 and token1 that are owed to the protocol - /// @dev Protocol fees will never exceed uint128 max in either token - function protocolFees() external view returns (uint128 token0, uint128 token1); + /// @notice The amounts of token0 and token1 that are owed to the protocol + /// @dev Protocol fees will never exceed uint128 max in either token + function protocolFees() external view returns (uint128 token0, uint128 token1); - /// @notice The currently in range liquidity available to the pool - /// @dev This value has no relationship to the total liquidity across all ticks - function liquidity() external view returns (uint128); + /// @notice The currently in range liquidity available to the pool + /// @dev This value has no relationship to the total liquidity across all ticks + function liquidity() external view returns (uint128); - /// @notice Look up information about a specific tick in the pool - /// @param tick The tick to look up - /// @return liquidityGross the total amount of position liquidity that uses the pool either as tick lower or - /// tick upper, - /// liquidityNet how much liquidity changes when the pool price crosses the tick, - /// feeGrowthOutside0X128 the fee growth on the other side of the tick from the current tick in token0, - /// feeGrowthOutside1X128 the fee growth on the other side of the tick from the current tick in token1, - /// tickCumulativeOutside the cumulative tick value on the other side of the tick from the current tick - /// secondsPerLiquidityOutsideX128 the seconds spent per liquidity on the other side of the tick from the current tick, - /// secondsOutside the seconds spent on the other side of the tick from the current tick, - /// initialized Set to true if the tick is initialized, i.e. liquidityGross is greater than 0, otherwise equal to false. - /// Outside values can only be used if the tick is initialized, i.e. if liquidityGross is greater than 0. - /// In addition, these values are only relative and must be used only in comparison to previous snapshots for - /// a specific position. - function ticks(int24 tick) - external - view - returns ( - uint128 liquidityGross, - int128 liquidityNet, - uint256 feeGrowthOutside0X128, - uint256 feeGrowthOutside1X128, - int56 tickCumulativeOutside, - uint160 secondsPerLiquidityOutsideX128, - uint32 secondsOutside, - bool initialized - ); + /// @notice Look up information about a specific tick in the pool + /// @param tick The tick to look up + /// @return liquidityGross the total amount of position liquidity that uses the pool either as tick lower or + /// tick upper, + /// liquidityNet how much liquidity changes when the pool price crosses the tick, + /// feeGrowthOutside0X128 the fee growth on the other side of the tick from the current tick in token0, + /// feeGrowthOutside1X128 the fee growth on the other side of the tick from the current tick in token1, + /// tickCumulativeOutside the cumulative tick value on the other side of the tick from the current tick + /// secondsPerLiquidityOutsideX128 the seconds spent per liquidity on the other side of the tick from the current + /// tick, + /// secondsOutside the seconds spent on the other side of the tick from the current tick, + /// initialized Set to true if the tick is initialized, i.e. liquidityGross is greater than 0, otherwise equal to + /// false. + /// Outside values can only be used if the tick is initialized, i.e. if liquidityGross is greater than 0. + /// In addition, these values are only relative and must be used only in comparison to previous snapshots for + /// a specific position. + function ticks(int24 tick) + external + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128, + int56 tickCumulativeOutside, + uint160 secondsPerLiquidityOutsideX128, + uint32 secondsOutside, + bool initialized + ); - /// @notice Returns 256 packed tick initialized boolean values. See TickBitmap for more information - function tickBitmap(int16 wordPosition) external view returns (uint256); + /// @notice Returns 256 packed tick initialized boolean values. See TickBitmap for more information + function tickBitmap(int16 wordPosition) external view returns (uint256); - /// @notice Returns the information about a position by the position's key - /// @param key The position's key is a hash of a preimage composed by the owner, tickLower and tickUpper - /// @return _liquidity The amount of liquidity in the position, - /// Returns feeGrowthInside0LastX128 fee growth of token0 inside the tick range as of the last mint/burn/poke, - /// Returns feeGrowthInside1LastX128 fee growth of token1 inside the tick range as of the last mint/burn/poke, - /// Returns tokensOwed0 the computed amount of token0 owed to the position as of the last mint/burn/poke, - /// Returns tokensOwed1 the computed amount of token1 owed to the position as of the last mint/burn/poke - function positions(bytes32 key) - external - view - returns ( - uint128 _liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ); + /// @notice Returns the information about a position by the position's key + /// @param key The position's key is a hash of a preimage composed by the owner, tickLower and tickUpper + /// @return _liquidity The amount of liquidity in the position, + /// Returns feeGrowthInside0LastX128 fee growth of token0 inside the tick range as of the last mint/burn/poke, + /// Returns feeGrowthInside1LastX128 fee growth of token1 inside the tick range as of the last mint/burn/poke, + /// Returns tokensOwed0 the computed amount of token0 owed to the position as of the last mint/burn/poke, + /// Returns tokensOwed1 the computed amount of token1 owed to the position as of the last mint/burn/poke + function positions(bytes32 key) + external + view + returns ( + uint128 _liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); - /// @notice Returns data about a specific observation index - /// @param index The element of the observations array to fetch - /// @dev You most likely want to use #observe() instead of this method to get an observation as of some amount of time - /// ago, rather than at a specific index in the array. - /// @return blockTimestamp The timestamp of the observation, - /// Returns tickCumulative the tick multiplied by seconds elapsed for the life of the pool as of the observation timestamp, - /// Returns secondsPerLiquidityCumulativeX128 the seconds per in range liquidity for the life of the pool as of the observation timestamp, - /// Returns initialized whether the observation has been initialized and the values are safe to use - function observations(uint256 index) - external - view - returns ( - uint32 blockTimestamp, - int56 tickCumulative, - uint160 secondsPerLiquidityCumulativeX128, - bool initialized - ); + /// @notice Returns data about a specific observation index + /// @param index The element of the observations array to fetch + /// @dev You most likely want to use #observe() instead of this method to get an observation as of some amount of + /// time + /// ago, rather than at a specific index in the array. + /// @return blockTimestamp The timestamp of the observation, + /// Returns tickCumulative the tick multiplied by seconds elapsed for the life of the pool as of the observation + /// timestamp, + /// Returns secondsPerLiquidityCumulativeX128 the seconds per in range liquidity for the life of the pool as of the + /// observation timestamp, + /// Returns initialized whether the observation has been initialized and the values are safe to use + function observations(uint256 index) + external + view + returns (uint32 blockTimestamp, int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128, bool initialized); } diff --git a/src/periphery/UniswapV3/libraries/PoolAddress.sol b/src/periphery/UniswapV3/libraries/PoolAddress.sol index a746765..1263212 100644 --- a/src/periphery/UniswapV3/libraries/PoolAddress.sol +++ b/src/periphery/UniswapV3/libraries/PoolAddress.sol @@ -3,48 +3,41 @@ pragma solidity >=0.5.0; /// @title Provides functions for deriving a pool address from the factory, tokens, and the fee library PoolAddress { - bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; - /// @notice The identifying key of the pool - struct PoolKey { - address token0; - address token1; - uint24 fee; - } + /// @notice The identifying key of the pool + struct PoolKey { + address token0; + address token1; + uint24 fee; + } - /// @notice Returns PoolKey: the ordered tokens with the matched fee levels - /// @param tokenA The first token of a pool, unsorted - /// @param tokenB The second token of a pool, unsorted - /// @param fee The fee level of the pool - /// @return Poolkey The pool details with ordered token0 and token1 assignments - function getPoolKey( - address tokenA, - address tokenB, - uint24 fee - ) internal pure returns (PoolKey memory) { - if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); - return PoolKey({ token0: tokenA, token1: tokenB, fee: fee }); - } + /// @notice Returns PoolKey: the ordered tokens with the matched fee levels + /// @param tokenA The first token of a pool, unsorted + /// @param tokenB The second token of a pool, unsorted + /// @param fee The fee level of the pool + /// @return Poolkey The pool details with ordered token0 and token1 assignments + function getPoolKey(address tokenA, address tokenB, uint24 fee) internal pure returns (PoolKey memory) { + if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); + } - /// @notice Deterministically computes the pool address given the factory and PoolKey - /// @param factory The Uniswap V3 factory contract address - /// @param key The PoolKey - /// @return pool The contract address of the V3 pool - function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) { - require(key.token0 < key.token1); - pool = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"ff", - factory, - keccak256(abi.encode(key.token0, key.token1, key.fee)), - POOL_INIT_CODE_HASH - ) - ) - ) + /// @notice Deterministically computes the pool address given the factory and PoolKey + /// @param factory The Uniswap V3 factory contract address + /// @param key The PoolKey + /// @return pool The contract address of the V3 pool + function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) { + require(key.token0 < key.token1); + pool = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", factory, keccak256(abi.encode(key.token0, key.token1, key.fee)), POOL_INIT_CODE_HASH ) - ); - } + ) + ) + ) + ); + } } diff --git a/src/periphery/UniswapV3/libraries/TickMath.sol b/src/periphery/UniswapV3/libraries/TickMath.sol index bee1fda..91ad99f 100644 --- a/src/periphery/UniswapV3/libraries/TickMath.sol +++ b/src/periphery/UniswapV3/libraries/TickMath.sol @@ -5,13 +5,13 @@ pragma solidity >=0.5.0; /// @notice Computes sqrt price for ticks of size 1.0001, i.e. sqrt(1.0001^tick) as fixed point Q64.96 numbers. Supports /// prices between 2**-128 and 2**128 library TickMath { - /// @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128 - int24 internal constant MIN_TICK = -887272; - /// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 - int24 internal constant MAX_TICK = -MIN_TICK; + /// @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128 + int24 internal constant MIN_TICK = -887_272; + /// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 + int24 internal constant MAX_TICK = -MIN_TICK; - /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) - uint160 internal constant MIN_SQRT_RATIO = 4295128739; - /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) - uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; + /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) + uint160 internal constant MIN_SQRT_RATIO = 4_295_128_739; + /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) + uint160 internal constant MAX_SQRT_RATIO = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_342; } diff --git a/src/periphery/base/Payment.sol b/src/periphery/base/Payment.sol deleted file mode 100644 index 15dc72b..0000000 --- a/src/periphery/base/Payment.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.4; - -import { IWETH9 } from "../interfaces/IWETH9.sol"; - -import { SafeTransferLib } from "../../libraries/SafeTransferLib.sol"; -import { Balance } from "../../libraries/Balance.sol"; - -/// @title Payment contract -/// @author https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/PeripheryPayments.sol -/// @notice Functions to ease deposits and withdrawals of ETH -abstract contract Payment { - address public immutable weth; - - error InsufficientOutputError(); - - constructor(address _weth) { - weth = _weth; - } - - receive() external payable { - require(msg.sender == weth, "Not WETH9"); - } - - function unwrapWETH(uint256 amountMinimum, address recipient) public payable { - uint256 balanceWETH = Balance.balance(weth); - if (balanceWETH < amountMinimum) revert InsufficientOutputError(); - - if (balanceWETH > 0) { - IWETH9(weth).withdraw(balanceWETH); - SafeTransferLib.safeTransferETH(recipient, balanceWETH); - } - } - - function sweepToken( - address token, - uint256 amountMinimum, - address recipient - ) public payable { - uint256 balanceToken = Balance.balance(token); - if (balanceToken < amountMinimum) revert InsufficientOutputError(); - - if (balanceToken > 0) { - SafeTransferLib.safeTransfer(token, recipient, balanceToken); - } - } - - function refundETH() external payable { - if (address(this).balance > 0) SafeTransferLib.safeTransferETH(msg.sender, address(this).balance); - } - - /// @param token The token to pay - /// @param payer The entity that must pay - /// @param recipient The entity that will receive payment - /// @param value The amount to pay - function pay( - address token, - address payer, - address recipient, - uint256 value - ) internal { - if (token == weth && address(this).balance >= value) { - // pay with WETH - IWETH9(weth).deposit{ value: value }(); // wrap only what is needed to pay - SafeTransferLib.safeTransfer(weth, recipient, value); - } else if (payer == address(this)) { - // pay with tokens already in the contract (for the exact input multihop case) - SafeTransferLib.safeTransfer(token, recipient, value); - } else { - // pull payment - SafeTransferLib.safeTransferFrom(token, payer, recipient, value); - } - } -} diff --git a/src/periphery/interfaces/IWETH9.sol b/src/periphery/interfaces/IWETH9.sol index 4d05079..e6f15cd 100644 --- a/src/periphery/interfaces/IWETH9.sol +++ b/src/periphery/interfaces/IWETH9.sol @@ -4,9 +4,9 @@ pragma solidity >=0.8.6; /// @title Interface for WETH9 /// @author Primitive interface IWETH9 { - /// @notice Wraps ETH into WETH - function deposit() external payable; + /// @notice Wraps ETH into WETH + function deposit() external payable; - /// @notice Unwraps WETH into ETH - function withdraw(uint256) external; + /// @notice Unwraps WETH into ETH + function withdraw(uint256) external; } diff --git a/src/periphery/libraries/LendgineAddress.sol b/src/periphery/libraries/LendgineAddress.sol index 2a343e3..bbab344 100644 --- a/src/periphery/libraries/LendgineAddress.sol +++ b/src/periphery/libraries/LendgineAddress.sol @@ -4,27 +4,31 @@ pragma solidity >=0.5.0; import { Lendgine } from "../../core/Lendgine.sol"; library LendgineAddress { - function computeAddress( - address factory, - address token0, - address token1, - uint256 token0Exp, - uint256 token1Exp, - uint256 upperBound - ) internal pure returns (address lendgine) { - lendgine = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"ff", - factory, - keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)), - keccak256(type(Lendgine).creationCode) - ) - ) - ) + function computeAddress( + address factory, + address token0, + address token1, + uint256 token0Exp, + uint256 token1Exp, + uint256 upperBound + ) + internal + pure + returns (address lendgine) + { + lendgine = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encode(token0, token1, token0Exp, token1Exp, upperBound)), + keccak256(type(Lendgine).creationCode) ) - ); - } + ) + ) + ) + ); + } } diff --git a/test/AccrueInterestTest.t.sol b/test/AccrueInterestTest.t.sol index db43fdf..8b38451 100644 --- a/test/AccrueInterestTest.t.sol +++ b/test/AccrueInterestTest.t.sol @@ -6,73 +6,114 @@ import { Pair } from "../src/core/Pair.sol"; import { TestHelper } from "./utils/TestHelper.sol"; contract AccrueInterestTest is TestHelper { - event AccrueInterest(uint256 timeElapsed, uint256 collateral, uint256 liquidity); + event AccrueInterest(uint256 timeElapsed, uint256 collateral, uint256 liquidity); - function setUp() external { - _setUp(); - } + function setUp() external { + _setUp(); + } - function testAccrueNoLiquidity() external { - lendgine.accrueInterest(); + function testAccrueNoLiquidity() external { + lendgine.accrueInterest(); - assertEq(1, lendgine.lastUpdate()); - assertEq(0, lendgine.rewardPerPositionStored()); - assertEq(0, lendgine.totalLiquidityBorrowed()); - } + assertEq(1, lendgine.lastUpdate()); + assertEq(0, lendgine.rewardPerPositionStored()); + assertEq(0, lendgine.totalLiquidityBorrowed()); + } - function testAccrueNoTime() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); + function testAccrueNoTime() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); - lendgine.accrueInterest(); + lendgine.accrueInterest(); - assertEq(1, lendgine.lastUpdate()); - assertEq(0, lendgine.rewardPerPositionStored()); - assertEq(0.5 ether, lendgine.totalLiquidityBorrowed()); - } + assertEq(1, lendgine.lastUpdate()); + assertEq(0, lendgine.rewardPerPositionStored()); + assertEq(0.5 ether, lendgine.totalLiquidityBorrowed()); + } - function testAccrueInterest() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); + function testAccrueInterest() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); - vm.warp(365 days + 1); + vm.warp(365 days + 1); - lendgine.accrueInterest(); + lendgine.accrueInterest(); - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 - assertEq(365 days + 1, lendgine.lastUpdate()); - assertEq(0.5 ether - lpDilution, lendgine.totalLiquidityBorrowed()); - assertEq(token1Dilution, lendgine.rewardPerPositionStored()); - } + assertEq(365 days + 1, lendgine.lastUpdate()); + assertEq(0.5 ether - lpDilution, lendgine.totalLiquidityBorrowed()); + assertEq(token1Dilution, lendgine.rewardPerPositionStored()); + } - function testMaxDilution() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); + function testMaxDilution() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); - vm.warp(730 days + 1); + vm.warp(730 days + 1); - lendgine.accrueInterest(); + lendgine.accrueInterest(); - assertEq(730 days + 1, lendgine.lastUpdate()); - assertEq(0, lendgine.totalLiquidityBorrowed()); - assertEq(5 ether, lendgine.rewardPerPositionStored()); - } + assertEq(730 days + 1, lendgine.lastUpdate()); + assertEq(0, lendgine.totalLiquidityBorrowed()); + assertEq(5 ether, lendgine.rewardPerPositionStored()); + } - function testLendgineEmit() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); + function testLendgineEmit() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); - vm.warp(365 days + 1); + vm.warp(365 days + 1); - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 - vm.expectEmit(false, false, false, true, address(lendgine)); - emit AccrueInterest(365 days, token1Dilution, lpDilution); - lendgine.accrueInterest(); - } + vm.expectEmit(false, false, false, true, address(lendgine)); + emit AccrueInterest(365 days, token1Dilution, lpDilution); + lendgine.accrueInterest(); + } + + function testNonStandardDecimals() external { + token1Scale = 9; + + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); + + token0.mint(address(this), 1e18); + token1.mint(address(this), 8 * 1e9); + + lendgine.deposit( + address(this), + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1e18, + amount1: 8 * 1e9, + payer: address(this) + }) + ) + ); + + token1.mint(cuh, 5 * 1e9); + + vm.prank(cuh); + token1.approve(address(this), 5 * 1e9); + lendgine.mint(cuh, 5 * 1e9, abi.encode(MintCallbackData({token: address(token1), payer: cuh}))); + + vm.warp(365 days + 1); + + lendgine.accrueInterest(); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 token1Dilution = lendgine.convertLiquidityToCollateral(lpDilution); // same as rewardPerPosition because + // position size is 1 + + assertEq(0.5 ether - lpDilution, lendgine.totalLiquidityBorrowed()); + assertEq(token1Dilution, lendgine.rewardPerPositionStored()); + } } diff --git a/test/AccruePositionInterestTest.t.sol b/test/AccruePositionInterestTest.t.sol index da2fcd3..48ea83e 100644 --- a/test/AccruePositionInterestTest.t.sol +++ b/test/AccruePositionInterestTest.t.sol @@ -7,63 +7,63 @@ import { Position } from "../src/core/libraries/Position.sol"; import { TestHelper } from "./utils/TestHelper.sol"; contract AccruePositionInterestTest is TestHelper { - event AccruePositionInterest(address indexed owner, uint256 rewardPerPosition); + event AccruePositionInterest(address indexed owner, uint256 rewardPerPosition); - function setUp() external { - _setUp(); - } + function setUp() external { + _setUp(); + } - function testNoPositionError() external { - vm.expectRevert(Position.NoPositionError.selector); - vm.prank(cuh); - lendgine.accruePositionInterest(); - } + function testNoPositionError() external { + vm.expectRevert(Position.NoPositionError.selector); + vm.prank(cuh); + lendgine.accruePositionInterest(); + } - function testNoTime() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); + function testNoTime() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); - vm.prank(cuh); - lendgine.accruePositionInterest(); + vm.prank(cuh); + lendgine.accruePositionInterest(); - (uint256 positionSize, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); - assertEq(1 ether, positionSize); - assertEq(0, rewardPerPositionPaid); - assertEq(0, tokensOwed); - } + (uint256 positionSize, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); + assertEq(1 ether, positionSize); + assertEq(0, rewardPerPositionPaid); + assertEq(0, tokensOwed); + } - function testAccruePositionInterest() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); + function testAccruePositionInterest() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); - vm.warp(365 days + 1); + vm.warp(365 days + 1); - vm.prank(cuh); - lendgine.accruePositionInterest(); + vm.prank(cuh); + lendgine.accruePositionInterest(); - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 - (uint256 positionSize, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); - assertEq(1 ether, positionSize); - assertEq(token1Dilution, rewardPerPositionPaid); - assertEq(token1Dilution, tokensOwed); - } + (uint256 positionSize, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); + assertEq(1 ether, positionSize); + assertEq(token1Dilution, rewardPerPositionPaid); + assertEq(token1Dilution, tokensOwed); + } - function testLendgineEmit() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); + function testLendgineEmit() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); - vm.warp(365 days + 1); + vm.warp(365 days + 1); - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 token1Dilution = 10 * lpDilution; // same as rewardPerPosition because position size is 1 - vm.prank(cuh); - vm.expectEmit(true, false, false, true, address(lendgine)); - emit AccruePositionInterest(cuh, token1Dilution); - lendgine.accruePositionInterest(); - } + vm.prank(cuh); + vm.expectEmit(true, false, false, true, address(lendgine)); + emit AccruePositionInterest(cuh, token1Dilution); + lendgine.accruePositionInterest(); + } } diff --git a/test/BurnTest.t.sol b/test/BurnTest.t.sol index 12b72ae..aaf35db 100644 --- a/test/BurnTest.t.sol +++ b/test/BurnTest.t.sol @@ -7,261 +7,243 @@ import { TestHelper } from "./utils/TestHelper.sol"; import { FullMath } from "../src/libraries/FullMath.sol"; contract BurnTest is TestHelper { - event Burn(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); - - event Mint(uint256 amount0In, uint256 amount1In, uint256 liquidity); - - function setUp() external { - _setUp(); - _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); - _mint(cuh, cuh, 5 ether); - } - - function testBurnPartial() external { - uint256 collateral = _burn(cuh, cuh, 0.25 ether, 0.25 ether, 2 ether); - - // check returned tokens - assertEq(2.5 ether, collateral); - assertEq(0.25 ether, token0.balanceOf(cuh)); - assertEq(2 ether + 2.5 ether, token1.balanceOf(cuh)); - - // check lendgine token - assertEq(0.25 ether, lendgine.totalSupply()); - assertEq(0.25 ether, lendgine.balanceOf(cuh)); - - // check storage slots - assertEq(0.25 ether, lendgine.totalLiquidityBorrowed()); - assertEq(0.75 ether, lendgine.totalLiquidity()); - assertEq(0.75 ether, uint256(lendgine.reserve0())); - assertEq(6 ether, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(0.75 ether, token0.balanceOf(address(lendgine))); - assertEq(2.5 ether + 6 ether, token1.balanceOf(address(lendgine))); - } - - function testBurnFull() external { - uint256 collateral = _burn(cuh, cuh, 0.5 ether, 0.5 ether, 4 ether); - - // check returned tokens - assertEq(5 ether, collateral); - assertEq(0 ether, token0.balanceOf(cuh)); - assertEq(5 ether, token1.balanceOf(cuh)); - - // check lendgine token - assertEq(0 ether, lendgine.totalSupply()); - assertEq(0 ether, lendgine.balanceOf(cuh)); - - // check storage slots - assertEq(0 ether, lendgine.totalLiquidityBorrowed()); - assertEq(1 ether, lendgine.totalLiquidity()); - assertEq(1 ether, uint256(lendgine.reserve0())); - assertEq(8 ether, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(1 ether, token0.balanceOf(address(lendgine))); - assertEq(8 ether, token1.balanceOf(address(lendgine))); - } - - function testZeroBurn() external { - vm.expectRevert(Lendgine.InputError.selector); - lendgine.burn(cuh, bytes("")); - } - - function testUnderPay() external { - vm.prank(cuh); - lendgine.transfer(address(lendgine), 0.5 ether); - - vm.startPrank(cuh); - token0.approve(address(this), 0.5 ether); - token1.approve(address(this), 3 ether); - vm.stopPrank(); - - vm.expectRevert(Pair.InvariantError.selector); - lendgine.burn( - cuh, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 0.5 ether, - amount1: 3 ether, - payer: cuh - }) - ) - ); - } - - function testEmitLendgine() external { - vm.prank(cuh); - lendgine.transfer(address(lendgine), 0.5 ether); - - vm.startPrank(cuh); - token0.approve(address(this), 0.5 ether); - token1.approve(address(this), 4 ether); - vm.stopPrank(); - - vm.expectEmit(true, true, false, true, address(lendgine)); - emit Burn(address(this), 5 ether, 0.5 ether, 0.5 ether, cuh); - lendgine.burn( - cuh, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 0.5 ether, - amount1: 4 ether, - payer: cuh - }) - ) - ); - } - - function testEmitPair() external { - vm.prank(cuh); - lendgine.transfer(address(lendgine), 0.5 ether); - - vm.startPrank(cuh); - token0.approve(address(this), 0.5 ether); - token1.approve(address(this), 4 ether); - vm.stopPrank(); - - vm.expectEmit(false, false, false, true, address(lendgine)); - emit Mint(0.5 ether, 4 ether, 0.5 ether); - lendgine.burn( - cuh, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 0.5 ether, - amount1: 4 ether, - payer: cuh - }) - ) - ); - } - - function testAccrueOnBurn() external { - vm.warp(365 days + 1); - lendgine.accrueInterest(); - - uint256 reserve0 = lendgine.reserve0(); - uint256 reserve1 = lendgine.reserve1(); - - uint256 amount0 = FullMath.mulDivRoundingUp( - reserve0, - lendgine.convertShareToLiquidity(0.5 ether), - lendgine.totalLiquidity() - ); - uint256 amount1 = FullMath.mulDivRoundingUp( - reserve1, - lendgine.convertShareToLiquidity(0.5 ether), - lendgine.totalLiquidity() - ); - - _burn(cuh, cuh, 0.5 ether, amount0, amount1); - - assertEq(365 days + 1, lendgine.lastUpdate()); - assert(lendgine.rewardPerPositionStored() != 0); - } - - function testProportionalBurn() external { - vm.warp(365 days + 1); - lendgine.accrueInterest(); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - uint256 reserve0 = lendgine.reserve0(); - uint256 reserve1 = lendgine.reserve1(); - uint256 shares = 0.25 ether; - - uint256 amount0 = FullMath.mulDivRoundingUp( - reserve0, - lendgine.convertShareToLiquidity(shares), - lendgine.totalLiquidity() - ); - uint256 amount1 = FullMath.mulDivRoundingUp( - reserve1, - lendgine.convertShareToLiquidity(shares), - lendgine.totalLiquidity() - ); - - uint256 collateral = _burn(cuh, cuh, shares, amount0, amount1); - - // check collateral returned - assertEq(5 * (0.5 ether - lpDilution), collateral); // withdrew half the collateral - - // check lendgine storage slots - assertEq((0.5 ether - lpDilution) / 2, lendgine.totalLiquidityBorrowed()); // withdrew half the liquidity - assertEq(0.5 ether + (0.5 ether - lpDilution) / 2, lendgine.totalLiquidity()); - } - - function testNonStandardDecimals() external { - token1Scale = 9; - - lendgine = Lendgine( - factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound) - ); - - token0.mint(address(this), 1e18); - token1.mint(address(this), 8 * 1e9); - - lendgine.deposit( - address(this), - 1 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 1e18, - amount1: 8 * 1e9, - payer: address(this) - }) - ) - ); - - token1.mint(address(this), 5 * 1e9); - - lendgine.mint( - address(this), - 5 * 1e9, - abi.encode(MintCallbackData({ token: address(token1), payer: address(this) })) - ); - - lendgine.transfer(address(lendgine), 0.25 ether); - - uint256 collateral = lendgine.burn( - address(this), - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 0.25 ether, - amount1: 2 * 1e9, - payer: address(this) - }) - ) - ); - - // check returned tokens - assertEq(2.5 * 1e9, collateral); - assertEq(0.25 ether, token0.balanceOf(address(this))); - assertEq(4.5 * 1e9, token1.balanceOf(address(this))); - - // check lendgine token - assertEq(0.25 ether, lendgine.totalSupply()); - assertEq(0.25 ether, lendgine.balanceOf(address(this))); - - // check storage slots - assertEq(0.25 ether, lendgine.totalLiquidityBorrowed()); - assertEq(0.75 ether, lendgine.totalLiquidity()); - assertEq(0.75 ether, uint256(lendgine.reserve0())); - assertEq(6 * 1e9, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(0.75 ether, token0.balanceOf(address(lendgine))); - assertEq(8.5 * 1e9, token1.balanceOf(address(lendgine))); - } + event Burn(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); + + event Mint(uint256 amount0In, uint256 amount1In, uint256 liquidity); + + function setUp() external { + _setUp(); + _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); + _mint(cuh, cuh, 5 ether); + } + + function testBurnPartial() external { + uint256 collateral = _burn(cuh, cuh, 0.25 ether, 0.25 ether, 2 ether); + + // check returned tokens + assertEq(2.5 ether, collateral); + assertEq(0.25 ether, token0.balanceOf(cuh)); + assertEq(2 ether + 2.5 ether, token1.balanceOf(cuh)); + + // check lendgine token + assertEq(0.25 ether, lendgine.totalSupply()); + assertEq(0.25 ether, lendgine.balanceOf(cuh)); + + // check storage slots + assertEq(0.25 ether, lendgine.totalLiquidityBorrowed()); + assertEq(0.75 ether, lendgine.totalLiquidity()); + assertEq(0.75 ether, uint256(lendgine.reserve0())); + assertEq(6 ether, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(0.75 ether, token0.balanceOf(address(lendgine))); + assertEq(2.5 ether + 6 ether, token1.balanceOf(address(lendgine))); + } + + function testBurnFull() external { + uint256 collateral = _burn(cuh, cuh, 0.5 ether, 0.5 ether, 4 ether); + + // check returned tokens + assertEq(5 ether, collateral); + assertEq(0 ether, token0.balanceOf(cuh)); + assertEq(5 ether, token1.balanceOf(cuh)); + + // check lendgine token + assertEq(0 ether, lendgine.totalSupply()); + assertEq(0 ether, lendgine.balanceOf(cuh)); + + // check storage slots + assertEq(0 ether, lendgine.totalLiquidityBorrowed()); + assertEq(1 ether, lendgine.totalLiquidity()); + assertEq(1 ether, uint256(lendgine.reserve0())); + assertEq(8 ether, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(1 ether, token0.balanceOf(address(lendgine))); + assertEq(8 ether, token1.balanceOf(address(lendgine))); + } + + function testZeroBurn() external { + vm.expectRevert(Lendgine.InputError.selector); + lendgine.burn(cuh, bytes("")); + } + + function testUnderPay() external { + vm.prank(cuh); + lendgine.transfer(address(lendgine), 0.5 ether); + + vm.startPrank(cuh); + token0.approve(address(this), 0.5 ether); + token1.approve(address(this), 3 ether); + vm.stopPrank(); + + vm.expectRevert(Pair.InvariantError.selector); + lendgine.burn( + cuh, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 0.5 ether, + amount1: 3 ether, + payer: cuh + }) + ) + ); + } + + function testEmitLendgine() external { + vm.prank(cuh); + lendgine.transfer(address(lendgine), 0.5 ether); + + vm.startPrank(cuh); + token0.approve(address(this), 0.5 ether); + token1.approve(address(this), 4 ether); + vm.stopPrank(); + + vm.expectEmit(true, true, false, true, address(lendgine)); + emit Burn(address(this), 5 ether, 0.5 ether, 0.5 ether, cuh); + lendgine.burn( + cuh, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 0.5 ether, + amount1: 4 ether, + payer: cuh + }) + ) + ); + } + + function testEmitPair() external { + vm.prank(cuh); + lendgine.transfer(address(lendgine), 0.5 ether); + + vm.startPrank(cuh); + token0.approve(address(this), 0.5 ether); + token1.approve(address(this), 4 ether); + vm.stopPrank(); + + vm.expectEmit(false, false, false, true, address(lendgine)); + emit Mint(0.5 ether, 4 ether, 0.5 ether); + lendgine.burn( + cuh, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 0.5 ether, + amount1: 4 ether, + payer: cuh + }) + ) + ); + } + + function testAccrueOnBurn() external { + vm.warp(365 days + 1); + lendgine.accrueInterest(); + + uint256 reserve0 = lendgine.reserve0(); + uint256 reserve1 = lendgine.reserve1(); + + uint256 amount0 = + FullMath.mulDivRoundingUp(reserve0, lendgine.convertShareToLiquidity(0.5 ether), lendgine.totalLiquidity()); + uint256 amount1 = + FullMath.mulDivRoundingUp(reserve1, lendgine.convertShareToLiquidity(0.5 ether), lendgine.totalLiquidity()); + + _burn(cuh, cuh, 0.5 ether, amount0, amount1); + + assertEq(365 days + 1, lendgine.lastUpdate()); + assert(lendgine.rewardPerPositionStored() != 0); + } + + function testProportionalBurn() external { + vm.warp(365 days + 1); + lendgine.accrueInterest(); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + + uint256 reserve0 = lendgine.reserve0(); + uint256 reserve1 = lendgine.reserve1(); + uint256 shares = 0.25 ether; + + uint256 amount0 = + FullMath.mulDivRoundingUp(reserve0, lendgine.convertShareToLiquidity(shares), lendgine.totalLiquidity()); + uint256 amount1 = + FullMath.mulDivRoundingUp(reserve1, lendgine.convertShareToLiquidity(shares), lendgine.totalLiquidity()); + + uint256 collateral = _burn(cuh, cuh, shares, amount0, amount1); + + // check collateral returned + assertEq(5 * (0.5 ether - lpDilution), collateral); // withdrew half the collateral + + // check lendgine storage slots + assertEq((0.5 ether - lpDilution) / 2, lendgine.totalLiquidityBorrowed()); // withdrew half the liquidity + assertEq(0.5 ether + (0.5 ether - lpDilution) / 2, lendgine.totalLiquidity()); + } + + function testNonStandardDecimals() external { + token1Scale = 9; + + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); + + token0.mint(address(this), 1e18); + token1.mint(address(this), 8 * 1e9); + + lendgine.deposit( + address(this), + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1e18, + amount1: 8 * 1e9, + payer: address(this) + }) + ) + ); + + token1.mint(address(this), 5 * 1e9); + + lendgine.mint(address(this), 5 * 1e9, abi.encode(MintCallbackData({token: address(token1), payer: address(this)}))); + + lendgine.transfer(address(lendgine), 0.25 ether); + + uint256 collateral = lendgine.burn( + address(this), + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 0.25 ether, + amount1: 2 * 1e9, + payer: address(this) + }) + ) + ); + + // check returned tokens + assertEq(2.5 * 1e9, collateral); + assertEq(0.25 ether, token0.balanceOf(address(this))); + assertEq(4.5 * 1e9, token1.balanceOf(address(this))); + + // check lendgine token + assertEq(0.25 ether, lendgine.totalSupply()); + assertEq(0.25 ether, lendgine.balanceOf(address(this))); + + // check storage slots + assertEq(0.25 ether, lendgine.totalLiquidityBorrowed()); + assertEq(0.75 ether, lendgine.totalLiquidity()); + assertEq(0.75 ether, uint256(lendgine.reserve0())); + assertEq(6 * 1e9, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(0.75 ether, token0.balanceOf(address(lendgine))); + assertEq(8.5 * 1e9, token1.balanceOf(address(lendgine))); + } } diff --git a/test/CollectTest.t.sol b/test/CollectTest.t.sol index 23dea4e..75c8c3e 100644 --- a/test/CollectTest.t.sol +++ b/test/CollectTest.t.sol @@ -6,87 +6,87 @@ import { Pair } from "../src/core/Pair.sol"; import { TestHelper } from "./utils/TestHelper.sol"; contract CollectTest is TestHelper { - event Collect(address indexed owner, address indexed to, uint256 amount); + event Collect(address indexed owner, address indexed to, uint256 amount); - function setUp() external { - _setUp(); - } + function setUp() external { + _setUp(); + } - function testZeroCollect() external { - uint256 collateral = lendgine.collect(cuh, 0); - assertEq(0, collateral); + function testZeroCollect() external { + uint256 collateral = lendgine.collect(cuh, 0); + assertEq(0, collateral); - collateral = lendgine.collect(cuh, 1 ether); - assertEq(0, collateral); - } + collateral = lendgine.collect(cuh, 1 ether); + assertEq(0, collateral); + } - function testCollectBasic() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); + function testCollectBasic() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); + vm.warp(365 days + 1); - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - vm.prank(cuh); - lendgine.accruePositionInterest(); + vm.prank(cuh); + lendgine.accruePositionInterest(); - vm.prank(cuh); - uint256 collateral = lendgine.collect(cuh, lpDilution * 5); + vm.prank(cuh); + uint256 collateral = lendgine.collect(cuh, lpDilution * 5); - // check return data - assertEq(lpDilution * 5, collateral); + // check return data + assertEq(lpDilution * 5, collateral); - // check position - (, , uint256 tokensOwed) = lendgine.positions(cuh); - assertEq(lpDilution * 5, tokensOwed); + // check position + (,, uint256 tokensOwed) = lendgine.positions(cuh); + assertEq(lpDilution * 5, tokensOwed); - // check token balances - assertEq(lpDilution * 5, token1.balanceOf(cuh)); - } + // check token balances + assertEq(lpDilution * 5, token1.balanceOf(cuh)); + } - function testOverCollect() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); + function testOverCollect() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); + vm.warp(365 days + 1); - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - vm.prank(cuh); - lendgine.accruePositionInterest(); + vm.prank(cuh); + lendgine.accruePositionInterest(); - vm.prank(cuh); - uint256 collateral = lendgine.collect(cuh, 100 ether); + vm.prank(cuh); + uint256 collateral = lendgine.collect(cuh, 100 ether); - // check return data - assertEq(lpDilution * 10, collateral); + // check return data + assertEq(lpDilution * 10, collateral); - // check position - (, , uint256 tokensOwed) = lendgine.positions(cuh); - assertEq(0, tokensOwed); + // check position + (,, uint256 tokensOwed) = lendgine.positions(cuh); + assertEq(0, tokensOwed); - // check token balances - assertEq(lpDilution * 10, token1.balanceOf(cuh)); - } + // check token balances + assertEq(lpDilution * 10, token1.balanceOf(cuh)); + } - function testEmit() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); + function testEmit() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); + vm.warp(365 days + 1); - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - vm.prank(cuh); - lendgine.accruePositionInterest(); + vm.prank(cuh); + lendgine.accruePositionInterest(); - vm.prank(cuh); - vm.expectEmit(true, true, false, true, address(lendgine)); - emit Collect(cuh, cuh, lpDilution * 10); - lendgine.collect(cuh, lpDilution * 10); - } + vm.prank(cuh); + vm.expectEmit(true, true, false, true, address(lendgine)); + emit Collect(cuh, cuh, lpDilution * 10); + lendgine.collect(cuh, lpDilution * 10); + } } diff --git a/test/DepositTest.t.sol b/test/DepositTest.t.sol index 4023512..153b64a 100644 --- a/test/DepositTest.t.sol +++ b/test/DepositTest.t.sol @@ -6,224 +6,231 @@ import { Pair } from "../src/core/Pair.sol"; import { TestHelper } from "./utils/TestHelper.sol"; contract DepositTest is TestHelper { - event Deposit(address indexed sender, uint256 size, uint256 liquidity, address indexed to); - - event Mint(uint256 amount0In, uint256 amount1In, uint256 liquidity); - - function setUp() external { - _setUp(); - } - - function testBasicDeposit() external { - uint256 size = _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - - // check lendgine storage slots - assertEq(1 ether, lendgine.totalLiquidity()); - assertEq(1 ether, lendgine.totalPositionSize()); - assertEq(1 ether, uint256(lendgine.reserve0())); - assertEq(8 ether, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(1 ether, token0.balanceOf(address(lendgine))); - assertEq(8 ether, token1.balanceOf(address(lendgine))); - - // check position size - assertEq(1 ether, size); - (uint256 positionSize, , ) = lendgine.positions(cuh); - assertEq(1 ether, positionSize); - } - - function testOverDeposit() external { - uint256 size = _deposit(cuh, cuh, 1 ether + 1, 8 ether + 1, 1 ether); - - // check lendgine storage slots - assertEq(1 ether, lendgine.totalLiquidity()); - assertEq(1 ether, lendgine.totalPositionSize()); - assertEq(1 ether + 1, uint256(lendgine.reserve0())); - assertEq(8 ether + 1, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(1 ether + 1, token0.balanceOf(address(lendgine))); - assertEq(8 ether + 1, token1.balanceOf(address(lendgine))); - - // check position size - assertEq(1 ether, size); - (uint256 positionSize, , ) = lendgine.positions(cuh); - assertEq(1 ether, positionSize); - } - - function testZeroMint() external { - vm.expectRevert(Lendgine.InputError.selector); - lendgine.deposit(cuh, 0, bytes("")); - } - - function testUnderPayment() external { - token0.mint(cuh, 1 ether); - token1.mint(cuh, 7 ether); - - vm.startPrank(cuh); - token0.approve(address(this), 1 ether); - token1.approve(address(this), 7 ether); - vm.stopPrank(); - - vm.expectRevert(Pair.InvariantError.selector); - lendgine.deposit( - cuh, - 1 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 1 ether, - amount1: 7 ether, - payer: cuh - }) - ) - ); - } - - function testEmitLendgine() external { - token0.mint(cuh, 1 ether); - token1.mint(cuh, 8 ether); - - vm.startPrank(cuh); - token0.approve(address(this), 1 ether); - token1.approve(address(this), 8 ether); - vm.stopPrank(); - - vm.expectEmit(true, true, false, true, address(lendgine)); - emit Deposit(address(this), 1 ether, 1 ether, cuh); - lendgine.deposit( - cuh, - 1 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 1 ether, - amount1: 8 ether, - payer: cuh - }) - ) - ); - } - - function testEmitPair() external { - token0.mint(cuh, 1 ether); - token1.mint(cuh, 8 ether); - - vm.startPrank(cuh); - token0.approve(address(this), 1 ether); - token1.approve(address(this), 8 ether); - vm.stopPrank(); - - vm.expectEmit(false, false, false, true, address(lendgine)); - emit Mint(1 ether, 8 ether, 1 ether); - lendgine.deposit( - cuh, - 1 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 1 ether, - amount1: 8 ether, - payer: cuh - }) - ) - ); - } - - function testAccrueOnDepositEmpty() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - - assertEq(1, lendgine.lastUpdate()); - } - - function testAccrueOnDeposit() external { - _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - - vm.warp(365 days + 1); - - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - - assertEq(365 days + 1, lendgine.lastUpdate()); - assert(lendgine.rewardPerPositionStored() != 0); - } - - function testAccrueOnPositionDeposit() external { - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - - vm.warp(365 days + 1); - - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - - (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); - assert(rewardPerPositionPaid != 0); - assert(tokensOwed != 0); - } - - function testProportionPositionSize() external { - _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - - vm.warp(365 days + 1); - - uint256 size = _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - // check position size - assertEq((1 ether * 1 ether) / (1 ether - lpDilution), size); - assertApproxEqAbs(1 ether, (size * (2 ether - lpDilution)) / (1 ether + size), 1); - (uint256 positionSize, , ) = lendgine.positions(cuh); - assertEq((1 ether * 1 ether) / (1 ether - lpDilution), positionSize); - - // check lendgine storage slots - assertEq(1 ether + size, lendgine.totalPositionSize()); - assertEq(1.5 ether, lendgine.totalLiquidity()); - } - - function testNonStandardDecimals() external { - token1Scale = 9; - - lendgine = Lendgine( - factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound) - ); - - token0.mint(address(this), 1e18); - token1.mint(address(this), 8 * 1e9); - - uint256 size = lendgine.deposit( - address(this), - 1 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 1e18, - amount1: 8 * 1e9, - payer: address(this) - }) - ) - ); - - // check lendgine storage slots - assertEq(1 ether, lendgine.totalLiquidity()); - assertEq(1 ether, lendgine.totalPositionSize()); - assertEq(1 ether, uint256(lendgine.reserve0())); - assertEq(8 * 1e9, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(1 ether, token0.balanceOf(address(lendgine))); - assertEq(8 * 1e9, token1.balanceOf(address(lendgine))); - - // check position size - assertEq(1 ether, size); - (uint256 positionSize, , ) = lendgine.positions(address(this)); - assertEq(1 ether, positionSize); - } + event Deposit(address indexed sender, uint256 size, uint256 liquidity, address indexed to); + + event Mint(uint256 amount0In, uint256 amount1In, uint256 liquidity); + + function setUp() external { + _setUp(); + } + + function testBasicDeposit() external { + uint256 size = _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + + // check lendgine storage slots + assertEq(1 ether, lendgine.totalLiquidity()); + assertEq(1 ether, lendgine.totalPositionSize()); + assertEq(1 ether, uint256(lendgine.reserve0())); + assertEq(8 ether, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(1 ether, token0.balanceOf(address(lendgine))); + assertEq(8 ether, token1.balanceOf(address(lendgine))); + + // check position size + assertEq(1 ether, size); + (uint256 positionSize,,) = lendgine.positions(cuh); + assertEq(1 ether, positionSize); + } + + function testOverDeposit() external { + uint256 size = _deposit(cuh, cuh, 1 ether + 1, 8 ether + 1, 1 ether); + + // check lendgine storage slots + assertEq(1 ether, lendgine.totalLiquidity()); + assertEq(1 ether, lendgine.totalPositionSize()); + assertEq(1 ether + 1, uint256(lendgine.reserve0())); + assertEq(8 ether + 1, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(1 ether + 1, token0.balanceOf(address(lendgine))); + assertEq(8 ether + 1, token1.balanceOf(address(lendgine))); + + // check position size + assertEq(1 ether, size); + (uint256 positionSize,,) = lendgine.positions(cuh); + assertEq(1 ether, positionSize); + } + + function testZeroMint() external { + vm.expectRevert(Lendgine.InputError.selector); + lendgine.deposit(cuh, 0, bytes("")); + } + + function testUnderPayment() external { + token0.mint(cuh, 1 ether); + token1.mint(cuh, 7 ether); + + vm.startPrank(cuh); + token0.approve(address(this), 1 ether); + token1.approve(address(this), 7 ether); + vm.stopPrank(); + + vm.expectRevert(Pair.InvariantError.selector); + lendgine.deposit( + cuh, + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1 ether, + amount1: 7 ether, + payer: cuh + }) + ) + ); + } + + function testEmitLendgine() external { + token0.mint(cuh, 1 ether); + token1.mint(cuh, 8 ether); + + vm.startPrank(cuh); + token0.approve(address(this), 1 ether); + token1.approve(address(this), 8 ether); + vm.stopPrank(); + + vm.expectEmit(true, true, false, true, address(lendgine)); + emit Deposit(address(this), 1 ether, 1 ether, cuh); + lendgine.deposit( + cuh, + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1 ether, + amount1: 8 ether, + payer: cuh + }) + ) + ); + } + + function testEmitPair() external { + token0.mint(cuh, 1 ether); + token1.mint(cuh, 8 ether); + + vm.startPrank(cuh); + token0.approve(address(this), 1 ether); + token1.approve(address(this), 8 ether); + vm.stopPrank(); + + vm.expectEmit(false, false, false, true, address(lendgine)); + emit Mint(1 ether, 8 ether, 1 ether); + lendgine.deposit( + cuh, + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1 ether, + amount1: 8 ether, + payer: cuh + }) + ) + ); + } + + function testAccrueOnDepositEmpty() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + + assertEq(1, lendgine.lastUpdate()); + } + + function testAccrueOnDeposit() external { + _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + + vm.warp(365 days + 1); + + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + + assertEq(365 days + 1, lendgine.lastUpdate()); + assert(lendgine.rewardPerPositionStored() != 0); + } + + function testAccrueOnPositionDeposit() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + + vm.warp(365 days + 1); + + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + + (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); + assert(rewardPerPositionPaid != 0); + assert(tokensOwed != 0); + } + + function testProportionPositionSize() external { + _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + + vm.warp(365 days + 1); + + uint256 size = _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + + // check position size + assertEq((1 ether * 1 ether) / (1 ether - lpDilution), size); + assertApproxEqAbs(1 ether, (size * (2 ether - lpDilution)) / (1 ether + size), 1); + (uint256 positionSize,,) = lendgine.positions(cuh); + assertEq((1 ether * 1 ether) / (1 ether - lpDilution), positionSize); + + // check lendgine storage slots + assertEq(1 ether + size, lendgine.totalPositionSize()); + assertEq(1.5 ether, lendgine.totalLiquidity()); + } + + function testNonStandardDecimals() external { + token1Scale = 9; + + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); + + token0.mint(address(this), 1e18); + token1.mint(address(this), 8 * 1e9); + + uint256 size = lendgine.deposit( + address(this), + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1e18, + amount1: 8 * 1e9, + payer: address(this) + }) + ) + ); + + // check lendgine storage slots + assertEq(1 ether, lendgine.totalLiquidity()); + assertEq(1 ether, lendgine.totalPositionSize()); + assertEq(1 ether, uint256(lendgine.reserve0())); + assertEq(8 * 1e9, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(1 ether, token0.balanceOf(address(lendgine))); + assertEq(8 * 1e9, token1.balanceOf(address(lendgine))); + + // check position size + assertEq(1 ether, size); + (uint256 positionSize,,) = lendgine.positions(address(this)); + assertEq(1 ether, positionSize); + } + + function testDepositAfterFullAccrue() external { + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 10 ether); + vm.warp(730 days + 1); + + vm.expectRevert(Lendgine.CompleteUtilizationError.selector); + lendgine.deposit(cuh, 1 ether, bytes("")); + } } diff --git a/test/FactoryTest.t.sol b/test/FactoryTest.t.sol index 5830370..843568f 100644 --- a/test/FactoryTest.t.sol +++ b/test/FactoryTest.t.sol @@ -6,104 +6,104 @@ import { Lendgine } from "../src/core/Lendgine.sol"; import { Test } from "forge-std/Test.sol"; contract FactoryTest is Test { - event LendgineCreated( - address indexed token0, - address indexed token1, - uint256 token0Scale, - uint256 token1Scale, - uint256 indexed upperBound, - address lendgine + event LendgineCreated( + address indexed token0, + address indexed token1, + uint256 token0Scale, + uint256 token1Scale, + uint256 indexed upperBound, + address lendgine + ); + + Factory public factory; + + function setUp() external { + factory = new Factory(); + } + + function testGetLendgine() external { + address lendgine = factory.createLendgine(address(1), address(2), 18, 18, 1e18); + + assertEq(lendgine, factory.getLendgine(address(1), address(2), 18, 18, 1e18)); + } + + function testDeployAddress() external { + address lendgineEstimate = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + address(factory), + keccak256(abi.encode(address(1), address(2), 18, 18, 1e18)), + keccak256(type(Lendgine).creationCode) + ) + ) + ) + ) ); - Factory public factory; - - function setUp() external { - factory = new Factory(); - } - - function testGetLendgine() external { - address lendgine = factory.createLendgine(address(1), address(2), 18, 18, 1e18); - - assertEq(lendgine, factory.getLendgine(address(1), address(2), 18, 18, 1e18)); - } - - function testDeployAddress() external { - address lendgineEstimate = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"ff", - address(factory), - keccak256(abi.encode(address(1), address(2), 18, 18, 1e18)), - keccak256(type(Lendgine).creationCode) - ) - ) - ) - ) - ); - - address lendgine = factory.createLendgine(address(1), address(2), 18, 18, 1e18); - - assertEq(lendgine, lendgineEstimate); - } - - function testSameTokenError() external { - vm.expectRevert(Factory.SameTokenError.selector); - factory.createLendgine(address(1), address(1), 18, 18, 1e18); - } - - function testZeroAddressError() external { - vm.expectRevert(Factory.ZeroAddressError.selector); - factory.createLendgine(address(0), address(1), 18, 18, 1e18); - - vm.expectRevert(Factory.ZeroAddressError.selector); - factory.createLendgine(address(1), address(0), 18, 18, 1e18); - } - - function testDeployedError() external { - factory.createLendgine(address(1), address(2), 18, 18, 1e18); - - vm.expectRevert(Factory.DeployedError.selector); - factory.createLendgine(address(1), address(2), 18, 18, 1e18); - } - - function helpParametersZero() private { - (address token0, address token1, uint256 token0Scale, uint256 token1Scale, uint256 upperBound) = factory - .parameters(); - - assertEq(address(0), token0); - assertEq(address(0), token1); - assertEq(0, token0Scale); - assertEq(0, token1Scale); - assertEq(0, upperBound); - } - - function testParameters() external { - helpParametersZero(); - - factory.createLendgine(address(1), address(2), 18, 18, 1e18); - - helpParametersZero(); - } - - function testEmit() external { - address lendgineEstimate = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"ff", - address(factory), - keccak256(abi.encode(address(1), address(2), 18, 18, 1e18)), - keccak256(type(Lendgine).creationCode) - ) - ) - ) + address lendgine = factory.createLendgine(address(1), address(2), 18, 18, 1e18); + + assertEq(lendgine, lendgineEstimate); + } + + function testSameTokenError() external { + vm.expectRevert(Factory.SameTokenError.selector); + factory.createLendgine(address(1), address(1), 18, 18, 1e18); + } + + function testZeroAddressError() external { + vm.expectRevert(Factory.ZeroAddressError.selector); + factory.createLendgine(address(0), address(1), 18, 18, 1e18); + + vm.expectRevert(Factory.ZeroAddressError.selector); + factory.createLendgine(address(1), address(0), 18, 18, 1e18); + } + + function testDeployedError() external { + factory.createLendgine(address(1), address(2), 18, 18, 1e18); + + vm.expectRevert(Factory.DeployedError.selector); + factory.createLendgine(address(1), address(2), 18, 18, 1e18); + } + + function helpParametersZero() private { + (address token0, address token1, uint256 token0Scale, uint256 token1Scale, uint256 upperBound) = + factory.parameters(); + + assertEq(address(0), token0); + assertEq(address(0), token1); + assertEq(0, token0Scale); + assertEq(0, token1Scale); + assertEq(0, upperBound); + } + + function testParameters() external { + helpParametersZero(); + + factory.createLendgine(address(1), address(2), 18, 18, 1e18); + + helpParametersZero(); + } + + function testEmit() external { + address lendgineEstimate = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + address(factory), + keccak256(abi.encode(address(1), address(2), 18, 18, 1e18)), + keccak256(type(Lendgine).creationCode) ) - ); - vm.expectEmit(true, true, true, true, address(factory)); - emit LendgineCreated(address(1), address(2), 18, 18, 1e18, lendgineEstimate); - factory.createLendgine(address(1), address(2), 18, 18, 1e18); - } + ) + ) + ) + ); + vm.expectEmit(true, true, true, true, address(factory)); + emit LendgineCreated(address(1), address(2), 18, 18, 1e18, lendgineEstimate); + factory.createLendgine(address(1), address(2), 18, 18, 1e18); + } } diff --git a/test/ImmutableStateTest.t.sol b/test/ImmutableStateTest.t.sol index 02d8755..5ae04c7 100644 --- a/test/ImmutableStateTest.t.sol +++ b/test/ImmutableStateTest.t.sol @@ -6,19 +6,19 @@ import { Lendgine } from "../src/core/Lendgine.sol"; import { Test } from "forge-std/Test.sol"; contract ImmutableStateTest is Test { - Factory public factory; - Lendgine public lendgine; + Factory public factory; + Lendgine public lendgine; - function setUp() external { - factory = new Factory(); - lendgine = Lendgine(factory.createLendgine(address(1), address(2), 18, 18, 1e18)); - } + function setUp() external { + factory = new Factory(); + lendgine = Lendgine(factory.createLendgine(address(1), address(2), 18, 18, 1e18)); + } - function testImmutableState() external { - assertEq(address(1), lendgine.token0()); - assertEq(address(2), lendgine.token1()); - assertEq(1, lendgine.token0Scale()); - assertEq(1, lendgine.token1Scale()); - assertEq(1e18, lendgine.upperBound()); - } + function testImmutableState() external { + assertEq(address(1), lendgine.token0()); + assertEq(address(2), lendgine.token1()); + assertEq(1, lendgine.token0Scale()); + assertEq(1, lendgine.token1Scale()); + assertEq(1e18, lendgine.upperBound()); + } } diff --git a/test/LendgineRouterTest.t.sol b/test/LendgineRouterTest.t.sol index 0db76c6..2782b45 100644 --- a/test/LendgineRouterTest.t.sol +++ b/test/LendgineRouterTest.t.sol @@ -13,703 +13,705 @@ import { IUniswapV3Factory } from "../src/periphery/UniswapV3/interfaces/IUniswa import { IUniswapV3Pool } from "../src/periphery/UniswapV3/interfaces/IUniswapV3Pool.sol"; contract LendgineRouterTest is TestHelper { - event Mint(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); - - event Burn(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); - - LendgineRouter public lendgineRouter; - - IUniswapV2Factory public uniswapV2Factory = IUniswapV2Factory(0xc35DADB65012eC5796536bD9864eD8773aBc74C4); - IUniswapV2Pair public uniswapV2Pair; - IUniswapV3Factory public uniswapV3Factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - IUniswapV3Pool public uniswapV3Pool = IUniswapV3Pool(0x07A4f63f643fE39261140DF5E613b9469eccEC86); // uni / eth 5 bps pool - - function setUp() external { - vm.createSelectFork("goerli"); - - _setUp(); - lendgineRouter = new LendgineRouter( - address(factory), - address(uniswapV2Factory), - address(uniswapV3Factory), - address(0) - ); - - // set up the uniswap v2 pair - uniswapV2Pair = IUniswapV2Pair(uniswapV2Factory.createPair(address(token0), address(token1))); - token0.mint(address(uniswapV2Pair), 100 ether); - token1.mint(address(uniswapV2Pair), 100 ether); - uniswapV2Pair.mint(address(this)); - - _deposit(address(this), address(this), 100 ether, 800 ether, 100 ether); - } - - function setUpUniswapV3() internal { - // change tokens - token0 = MockERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); // UNI - token1 = MockERC20(0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6); // WETH - - // get tokens - vm.prank(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); - token0.transfer(cuh, 100 ether); - - vm.prank(0xb3A16C2B68BBB0111EbD27871a5934b949837D95); - token1.transfer(cuh, 100 ether); - - // deploy lendgine - lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), 18, 18, 3 ether)); - - // deposit tokens - vm.startPrank(cuh); - token0.approve(address(this), 5.40225 ether); - token1.approve(address(this), 45.3 ether); - vm.stopPrank(); - - // price is .735 weth / uni - lendgine.deposit( - address(this), - 10 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 5.40225 ether, - amount1: 45.3 ether, - payer: cuh - }) - ) - ); - } - - function testMintNoBorrow() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 0, - sharesMin: 0.1 ether, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - // check option amounts - assertEq(0.1 ether, lendgine.totalSupply()); - assertEq(0.1 ether, lendgine.balanceOf(cuh)); - - // check uniswap - // swap 0.1 ether of token 0 to token 1 - assertEq(100.1 ether, token0.balanceOf(address(uniswapV2Pair))); - assertApproxEqRel(99.9 ether, token1.balanceOf(address(uniswapV2Pair)), .001 ether); - - // check lendgine storage - assertEq(0.1 ether, lendgine.totalLiquidityBorrowed()); - - // check user balances - assertApproxEqRel(0.9 ether, token1.balanceOf(cuh), 1 ether); - - // check router token balances - assertEq(0, token0.balanceOf(address(lendgineRouter))); - assertEq(0, token1.balanceOf(address(lendgineRouter))); - assertEq(0, lendgine.balanceOf(address(lendgineRouter))); - } - - function testMintBorrow() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 8.8 ether, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - // check option amounts - assertEq(0.98 ether, lendgine.totalSupply()); - assertEq(0.98 ether, lendgine.balanceOf(cuh)); - - // check uniswap - // swap 0.98 ether of token 0 to token 1 - assertEq(100.98 ether, token0.balanceOf(address(uniswapV2Pair))); - assertApproxEqRel(99.02 ether, token1.balanceOf(address(uniswapV2Pair)), .001 ether); - - // check lendgine storage - assertEq(0.98 ether, lendgine.totalLiquidityBorrowed()); - - // check user balances - assertApproxEqRel(0, token1.balanceOf(cuh), 1 ether); - - // check router token balances - assertEq(0, token0.balanceOf(address(lendgineRouter))); - assertEq(0, token1.balanceOf(address(lendgineRouter))); - assertEq(0, lendgine.balanceOf(address(lendgineRouter))); - } - - function testMintV3() external { - setUpUniswapV3(); - - uint256 balance0Before = token0.balanceOf(address(uniswapV3Pool)); - uint256 balance1Before = token1.balanceOf(address(uniswapV3Pool)); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: 18, - token1Exp: 18, - upperBound: 3 ether, - amountIn: 1.2 ether, - amountBorrow: 0, - sharesMin: 0.2 ether, - swapType: SwapHelper.SwapType.UniswapV3, - swapExtraData: abi.encode(uint24(500)), - recipient: cuh, - deadline: block.timestamp - }) - ); - - // check option amounts - assertEq(0.2 ether, lendgine.totalSupply()); - assertEq(0.2 ether, lendgine.balanceOf(cuh)); - - // check uniswap - // swap (1.2 / 6) * .540 ether of token 0 to token 1 - assertApproxEqRel(balance0Before + .108 ether, token0.balanceOf(address(uniswapV3Pool)), .001 ether); - assertApproxEqRel(balance1Before - .072 ether, token1.balanceOf(address(uniswapV3Pool)), .001 ether); - - // check lendgine storage - assertEq(0.2 ether, lendgine.totalLiquidityBorrowed()); - - // check router token balances - assertEq(0, token0.balanceOf(address(lendgineRouter))); - assertEq(0, token1.balanceOf(address(lendgineRouter))); - assertEq(0, lendgine.balanceOf(address(lendgineRouter))); - } - - function testAmountError() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - vm.expectRevert(LendgineRouter.AmountError.selector); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 0, - sharesMin: 0.2 ether, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - } - - function testUserAmountError() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - vm.expectRevert(LendgineRouter.AmountError.selector); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 10 ether, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - } - - function testMintEmit() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - vm.expectEmit(true, true, true, true, address(lendgineRouter)); - emit Mint(cuh, address(lendgine), 1 ether, 0.1 ether, cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 0, - sharesMin: 0.1 ether, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - } - - function testBurn() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 8.8 ether, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - vm.prank(cuh); - lendgine.approve(address(lendgineRouter), 0.98 ether); - - vm.prank(cuh); - lendgineRouter.burn( - LendgineRouter.BurnParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - shares: .98 ether, - collateralMin: .96 ether, - amount0Min: .98 ether, - amount1Min: 8 * .98 ether, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - // check lendgine token - assertEq(0, lendgine.balanceOf(cuh)); - assertEq(0, lendgine.totalSupply()); - - // check lendgine storage slots - assertEq(0, lendgine.totalLiquidityBorrowed()); - - // check uniswap - assertApproxEqRel(100 ether, token0.balanceOf(address(uniswapV2Pair)), 0.001 ether); - assertApproxEqRel(100 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); - - // check user balances - assertApproxEqRel(0.1 ether, token1.balanceOf(cuh), 1 ether); - - // check router token balances - assertEq(0, token0.balanceOf(address(lendgineRouter))); - assertEq(0, token1.balanceOf(address(lendgineRouter))); - assertEq(0, lendgine.balanceOf(address(lendgineRouter))); - } - - function testBurnNoLiquidity() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 0, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - _withdraw(address(this), address(this), 99.9 ether); - - vm.prank(cuh); - lendgine.approve(address(lendgineRouter), 0.1 ether); - - vm.prank(cuh); - lendgineRouter.burn( - LendgineRouter.BurnParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - shares: .1 ether, - collateralMin: 0, - amount0Min: .1 ether, - amount1Min: .8 ether, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - // check lendgine token - assertEq(0, lendgine.balanceOf(cuh)); - assertEq(0, lendgine.totalSupply()); - - // check lendgine storage slots - assertEq(0, lendgine.totalLiquidityBorrowed()); - - // check uniswap - assertApproxEqRel(100 ether, token0.balanceOf(address(uniswapV2Pair)), 0.001 ether); - assertApproxEqRel(100 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); - - // check user balances - assertApproxEqRel(0.1 ether, token1.balanceOf(cuh), 1 ether); - - // check router token balances - assertEq(0, token0.balanceOf(address(lendgineRouter))); - assertEq(0, token1.balanceOf(address(lendgineRouter))); - assertEq(0, lendgine.balanceOf(address(lendgineRouter))); - } - - function testBurnV3() external { - setUpUniswapV3(); - - uint256 balance0Before = token0.balanceOf(address(uniswapV3Pool)); - uint256 balance1Before = token1.balanceOf(address(uniswapV3Pool)); - - uint256 userBalanceBefore = token1.balanceOf(cuh); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: 18, - token1Exp: 18, - upperBound: 3 ether, - amountIn: 1.2 ether, - amountBorrow: 0, - sharesMin: 0.2 ether, - swapType: SwapHelper.SwapType.UniswapV3, - swapExtraData: abi.encode(uint24(500)), - recipient: cuh, - deadline: block.timestamp - }) - ); - - vm.prank(cuh); - lendgine.approve(address(lendgineRouter), 0.2 ether); - - vm.prank(cuh); - lendgineRouter.burn( - LendgineRouter.BurnParams({ - token0: address(token0), - token1: address(token1), - token0Exp: 18, - token1Exp: 18, - upperBound: 3 ether, - shares: .2 ether, - collateralMin: 0, - amount0Min: 0, - amount1Min: 0, - swapType: SwapHelper.SwapType.UniswapV3, - swapExtraData: abi.encode(uint24(500)), - recipient: cuh, - deadline: block.timestamp - }) - ); - - // check lendgine token - assertEq(0, lendgine.balanceOf(cuh)); - assertEq(0, lendgine.totalSupply()); - - // check lendgine storage slots - assertEq(0, lendgine.totalLiquidityBorrowed()); - - // check uniswap - assertApproxEqRel(balance0Before, token0.balanceOf(address(uniswapV3Pool)), 0.001 ether); - assertApproxEqRel(balance1Before, token1.balanceOf(address(uniswapV3Pool)), 0.001 ether); - - // check user balance - assertApproxEqRel(userBalanceBefore, token1.balanceOf(cuh), .001 ether); - - // check router token balances - assertEq(0, token0.balanceOf(address(lendgineRouter))); - assertEq(0, token1.balanceOf(address(lendgineRouter))); - assertEq(0, lendgine.balanceOf(address(lendgineRouter))); - } - - function testBurnEmit() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 8.8 ether, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - vm.prank(cuh); - lendgine.approve(address(lendgineRouter), 0.98 ether); - - vm.prank(cuh); - vm.expectEmit(true, true, true, true, address(lendgineRouter)); - emit Burn(cuh, address(lendgine), 9.8 ether, .98 ether, cuh); - lendgineRouter.burn( - LendgineRouter.BurnParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - shares: .98 ether, - collateralMin: .96 ether, - amount0Min: .98 ether, - amount1Min: 8 * .98 ether, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - } - - function testBurnAmountError() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 8.8 ether, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - vm.prank(cuh); - lendgine.approve(address(lendgineRouter), 0.98 ether); - - vm.prank(cuh); - vm.expectRevert(LendgineRouter.AmountError.selector); - lendgineRouter.burn( - LendgineRouter.BurnParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - shares: .98 ether, - collateralMin: 0, - amount0Min: .98 ether, - amount1Min: 8 * .98 ether + 1, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - } - - function testBurnUserAmountError() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 8.8 ether, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - vm.prank(cuh); - lendgine.approve(address(lendgineRouter), 0.98 ether); - - vm.prank(cuh); - vm.expectRevert(LendgineRouter.AmountError.selector); - lendgineRouter.burn( - LendgineRouter.BurnParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - shares: .98 ether, - collateralMin: 1 ether, - amount0Min: .98 ether, - amount1Min: 8 * .98 ether, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - } - - function testBurnNoRecipient() external { - token1.mint(cuh, 1 ether); - - vm.prank(cuh); - token1.approve(address(lendgineRouter), 1 ether); - - vm.prank(cuh); - lendgineRouter.mint( - LendgineRouter.MintParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amountIn: 1 ether, - amountBorrow: 8.8 ether, - sharesMin: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: cuh, - deadline: block.timestamp - }) - ); - - uint256 balanceBefore = token1.balanceOf(address(cuh)); - - vm.prank(cuh); - lendgine.approve(address(lendgineRouter), 0.98 ether); - - vm.prank(cuh); - lendgineRouter.burn( - LendgineRouter.BurnParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - shares: .98 ether, - collateralMin: 0, - amount0Min: 0, - amount1Min: 0, - swapType: SwapHelper.SwapType.UniswapV2, - swapExtraData: bytes(""), - recipient: address(0), - deadline: block.timestamp - }) - ); - - // check lendgine token - assertEq(0, lendgine.balanceOf(cuh)); - assertEq(0, lendgine.totalSupply()); - - // check lendgine storage slots - assertEq(0, lendgine.totalLiquidityBorrowed()); - - // check uniswap - assertApproxEqRel(100 ether, token0.balanceOf(address(uniswapV2Pair)), 0.001 ether); - assertApproxEqRel(100 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); - - // check user balances - assertEq(balanceBefore, token1.balanceOf(address(cuh))); - - // check router token balances - assertEq(0, token0.balanceOf(address(lendgineRouter))); - assertApproxEqRel(0.1 ether, token1.balanceOf(address(lendgineRouter)), 1 ether); - assertEq(0, lendgine.balanceOf(address(lendgineRouter))); - } + event Mint(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); + + event Burn(address indexed from, address indexed lendgine, uint256 collateral, uint256 shares, address indexed to); + + LendgineRouter public lendgineRouter; + + IUniswapV2Factory public uniswapV2Factory = IUniswapV2Factory(0xc35DADB65012eC5796536bD9864eD8773aBc74C4); + IUniswapV2Pair public uniswapV2Pair; + IUniswapV3Factory public uniswapV3Factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); + IUniswapV3Pool public uniswapV3Pool = IUniswapV3Pool(0x07A4f63f643fE39261140DF5E613b9469eccEC86); // uni / eth 5 bps + + // pool + + function setUp() external { + vm.createSelectFork("goerli"); + + _setUp(); + lendgineRouter = new LendgineRouter( + address(factory), + address(uniswapV2Factory), + address(uniswapV3Factory), + address(0) + ); + + // set up the uniswap v2 pair + uniswapV2Pair = IUniswapV2Pair(uniswapV2Factory.createPair(address(token0), address(token1))); + token0.mint(address(uniswapV2Pair), 100 ether); + token1.mint(address(uniswapV2Pair), 100 ether); + uniswapV2Pair.mint(address(this)); + + _deposit(address(this), address(this), 100 ether, 800 ether, 100 ether); + } + + function setUpUniswapV3() internal { + // change tokens + token0 = MockERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); // UNI + token1 = MockERC20(0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6); // WETH + + // get tokens + vm.prank(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984); + token0.transfer(cuh, 100 ether); + + vm.prank(0xb3A16C2B68BBB0111EbD27871a5934b949837D95); + token1.transfer(cuh, 100 ether); + + // deploy lendgine + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), 18, 18, 3 ether)); + + // deposit tokens + vm.startPrank(cuh); + token0.approve(address(this), 5.40225 ether); + token1.approve(address(this), 45.3 ether); + vm.stopPrank(); + + // price is .735 weth / uni + lendgine.deposit( + address(this), + 10 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 5.40225 ether, + amount1: 45.3 ether, + payer: cuh + }) + ) + ); + } + + function testMintNoBorrow() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 0, + sharesMin: 0.1 ether, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + // check option amounts + assertEq(0.1 ether, lendgine.totalSupply()); + assertEq(0.1 ether, lendgine.balanceOf(cuh)); + + // check uniswap + // swap 0.1 ether of token 0 to token 1 + assertEq(100.1 ether, token0.balanceOf(address(uniswapV2Pair))); + assertApproxEqRel(99.9 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); + + // check lendgine storage + assertEq(0.1 ether, lendgine.totalLiquidityBorrowed()); + + // check user balances + assertApproxEqRel(0.9 ether, token1.balanceOf(cuh), 1 ether); + + // check router token balances + assertEq(0, token0.balanceOf(address(lendgineRouter))); + assertEq(0, token1.balanceOf(address(lendgineRouter))); + assertEq(0, lendgine.balanceOf(address(lendgineRouter))); + } + + function testMintBorrow() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 8.8 ether, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + // check option amounts + assertEq(0.98 ether, lendgine.totalSupply()); + assertEq(0.98 ether, lendgine.balanceOf(cuh)); + + // check uniswap + // swap 0.98 ether of token 0 to token 1 + assertEq(100.98 ether, token0.balanceOf(address(uniswapV2Pair))); + assertApproxEqRel(99.02 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); + + // check lendgine storage + assertEq(0.98 ether, lendgine.totalLiquidityBorrowed()); + + // check user balances + assertApproxEqRel(0, token1.balanceOf(cuh), 1 ether); + + // check router token balances + assertEq(0, token0.balanceOf(address(lendgineRouter))); + assertEq(0, token1.balanceOf(address(lendgineRouter))); + assertEq(0, lendgine.balanceOf(address(lendgineRouter))); + } + + function testMintV3() external { + setUpUniswapV3(); + + uint256 balance0Before = token0.balanceOf(address(uniswapV3Pool)); + uint256 balance1Before = token1.balanceOf(address(uniswapV3Pool)); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: 18, + token1Exp: 18, + upperBound: 3 ether, + amountIn: 1.2 ether, + amountBorrow: 0, + sharesMin: 0.2 ether, + swapType: SwapHelper.SwapType.UniswapV3, + swapExtraData: abi.encode(uint24(500)), + recipient: cuh, + deadline: block.timestamp + }) + ); + + // check option amounts + assertEq(0.2 ether, lendgine.totalSupply()); + assertEq(0.2 ether, lendgine.balanceOf(cuh)); + + // check uniswap + // swap (1.2 / 6) * .540 ether of token 0 to token 1 + assertApproxEqRel(balance0Before + 0.108 ether, token0.balanceOf(address(uniswapV3Pool)), 0.001 ether); + assertApproxEqRel(balance1Before - 0.072 ether, token1.balanceOf(address(uniswapV3Pool)), 0.001 ether); + + // check lendgine storage + assertEq(0.2 ether, lendgine.totalLiquidityBorrowed()); + + // check router token balances + assertEq(0, token0.balanceOf(address(lendgineRouter))); + assertEq(0, token1.balanceOf(address(lendgineRouter))); + assertEq(0, lendgine.balanceOf(address(lendgineRouter))); + } + + function testAmountError() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + vm.expectRevert(LendgineRouter.AmountError.selector); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 0, + sharesMin: 0.2 ether, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testUserAmountError() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + vm.expectRevert(LendgineRouter.AmountError.selector); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 10 ether, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testMintEmit() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + vm.expectEmit(true, true, true, true, address(lendgineRouter)); + emit Mint(cuh, address(lendgine), 1 ether, 0.1 ether, cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 0, + sharesMin: 0.1 ether, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testBurn() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 8.8 ether, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + vm.prank(cuh); + lendgine.approve(address(lendgineRouter), 0.98 ether); + + vm.prank(cuh); + lendgineRouter.burn( + LendgineRouter.BurnParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + shares: 0.98 ether, + collateralMin: 0.96 ether, + amount0Min: 0.98 ether, + amount1Min: 8 * 0.98 ether, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + // check lendgine token + assertEq(0, lendgine.balanceOf(cuh)); + assertEq(0, lendgine.totalSupply()); + + // check lendgine storage slots + assertEq(0, lendgine.totalLiquidityBorrowed()); + + // check uniswap + assertApproxEqRel(100 ether, token0.balanceOf(address(uniswapV2Pair)), 0.001 ether); + assertApproxEqRel(100 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); + + // check user balances + assertApproxEqRel(0.1 ether, token1.balanceOf(cuh), 1 ether); + + // check router token balances + assertEq(0, token0.balanceOf(address(lendgineRouter))); + assertEq(0, token1.balanceOf(address(lendgineRouter))); + assertEq(0, lendgine.balanceOf(address(lendgineRouter))); + } + + function testBurnNoLiquidity() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 0, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + _withdraw(address(this), address(this), 99.9 ether); + + vm.prank(cuh); + lendgine.approve(address(lendgineRouter), 0.1 ether); + + vm.prank(cuh); + lendgineRouter.burn( + LendgineRouter.BurnParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + shares: 0.1 ether, + collateralMin: 0, + amount0Min: 0.1 ether, + amount1Min: 0.8 ether, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + // check lendgine token + assertEq(0, lendgine.balanceOf(cuh)); + assertEq(0, lendgine.totalSupply()); + + // check lendgine storage slots + assertEq(0, lendgine.totalLiquidityBorrowed()); + + // check uniswap + assertApproxEqRel(100 ether, token0.balanceOf(address(uniswapV2Pair)), 0.001 ether); + assertApproxEqRel(100 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); + + // check user balances + assertApproxEqRel(0.1 ether, token1.balanceOf(cuh), 1 ether); + + // check router token balances + assertEq(0, token0.balanceOf(address(lendgineRouter))); + assertEq(0, token1.balanceOf(address(lendgineRouter))); + assertEq(0, lendgine.balanceOf(address(lendgineRouter))); + } + + function testBurnV3() external { + setUpUniswapV3(); + + uint256 balance0Before = token0.balanceOf(address(uniswapV3Pool)); + uint256 balance1Before = token1.balanceOf(address(uniswapV3Pool)); + + uint256 userBalanceBefore = token1.balanceOf(cuh); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: 18, + token1Exp: 18, + upperBound: 3 ether, + amountIn: 1.2 ether, + amountBorrow: 0, + sharesMin: 0.2 ether, + swapType: SwapHelper.SwapType.UniswapV3, + swapExtraData: abi.encode(uint24(500)), + recipient: cuh, + deadline: block.timestamp + }) + ); + + vm.prank(cuh); + lendgine.approve(address(lendgineRouter), 0.2 ether); + + vm.prank(cuh); + lendgineRouter.burn( + LendgineRouter.BurnParams({ + token0: address(token0), + token1: address(token1), + token0Exp: 18, + token1Exp: 18, + upperBound: 3 ether, + shares: 0.2 ether, + collateralMin: 0, + amount0Min: 0, + amount1Min: 0, + swapType: SwapHelper.SwapType.UniswapV3, + swapExtraData: abi.encode(uint24(500)), + recipient: cuh, + deadline: block.timestamp + }) + ); + + // check lendgine token + assertEq(0, lendgine.balanceOf(cuh)); + assertEq(0, lendgine.totalSupply()); + + // check lendgine storage slots + assertEq(0, lendgine.totalLiquidityBorrowed()); + + // check uniswap + assertApproxEqRel(balance0Before, token0.balanceOf(address(uniswapV3Pool)), 0.001 ether); + assertApproxEqRel(balance1Before, token1.balanceOf(address(uniswapV3Pool)), 0.001 ether); + + // check user balance + assertApproxEqRel(userBalanceBefore, token1.balanceOf(cuh), 0.001 ether); + + // check router token balances + assertEq(0, token0.balanceOf(address(lendgineRouter))); + assertEq(0, token1.balanceOf(address(lendgineRouter))); + assertEq(0, lendgine.balanceOf(address(lendgineRouter))); + } + + function testBurnEmit() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 8.8 ether, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + vm.prank(cuh); + lendgine.approve(address(lendgineRouter), 0.98 ether); + + vm.prank(cuh); + vm.expectEmit(true, true, true, true, address(lendgineRouter)); + emit Burn(cuh, address(lendgine), 9.8 ether, 0.98 ether, cuh); + lendgineRouter.burn( + LendgineRouter.BurnParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + shares: 0.98 ether, + collateralMin: 0.96 ether, + amount0Min: 0.98 ether, + amount1Min: 8 * 0.98 ether, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testBurnAmountError() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 8.8 ether, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + vm.prank(cuh); + lendgine.approve(address(lendgineRouter), 0.98 ether); + + vm.prank(cuh); + vm.expectRevert(LendgineRouter.AmountError.selector); + lendgineRouter.burn( + LendgineRouter.BurnParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + shares: 0.98 ether, + collateralMin: 0, + amount0Min: 0.98 ether, + amount1Min: 8 * 0.98 ether + 1, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testBurnUserAmountError() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 8.8 ether, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + vm.prank(cuh); + lendgine.approve(address(lendgineRouter), 0.98 ether); + + vm.prank(cuh); + vm.expectRevert(LendgineRouter.AmountError.selector); + lendgineRouter.burn( + LendgineRouter.BurnParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + shares: 0.98 ether, + collateralMin: 1 ether, + amount0Min: 0.98 ether, + amount1Min: 8 * 0.98 ether, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testBurnNoRecipient() external { + token1.mint(cuh, 1 ether); + + vm.prank(cuh); + token1.approve(address(lendgineRouter), 1 ether); + + vm.prank(cuh); + lendgineRouter.mint( + LendgineRouter.MintParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amountIn: 1 ether, + amountBorrow: 8.8 ether, + sharesMin: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: cuh, + deadline: block.timestamp + }) + ); + + uint256 balanceBefore = token1.balanceOf(address(cuh)); + + vm.prank(cuh); + lendgine.approve(address(lendgineRouter), 0.98 ether); + + vm.prank(cuh); + lendgineRouter.burn( + LendgineRouter.BurnParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + shares: 0.98 ether, + collateralMin: 0, + amount0Min: 0, + amount1Min: 0, + swapType: SwapHelper.SwapType.UniswapV2, + swapExtraData: bytes(""), + recipient: address(0), + deadline: block.timestamp + }) + ); + + // check lendgine token + assertEq(0, lendgine.balanceOf(cuh)); + assertEq(0, lendgine.totalSupply()); + + // check lendgine storage slots + assertEq(0, lendgine.totalLiquidityBorrowed()); + + // check uniswap + assertApproxEqRel(100 ether, token0.balanceOf(address(uniswapV2Pair)), 0.001 ether); + assertApproxEqRel(100 ether, token1.balanceOf(address(uniswapV2Pair)), 0.001 ether); + + // check user balances + assertEq(balanceBefore, token1.balanceOf(address(cuh))); + + // check router token balances + assertEq(0, token0.balanceOf(address(lendgineRouter))); + assertApproxEqRel(0.1 ether, token1.balanceOf(address(lendgineRouter)), 1 ether); + assertEq(0, lendgine.balanceOf(address(lendgineRouter))); + } } diff --git a/test/LiquidityManagerTest.t.sol b/test/LiquidityManagerTest.t.sol index cf2c955..eeb4489 100644 --- a/test/LiquidityManagerTest.t.sol +++ b/test/LiquidityManagerTest.t.sol @@ -6,574 +6,559 @@ import { LiquidityManager } from "../src/periphery/LiquidityManager.sol"; import { TestHelper } from "./utils/TestHelper.sol"; contract LiquidityManagerTest is TestHelper { - event AddLiquidity( - address indexed from, - address indexed lendgine, - uint256 liquidity, - uint256 size, - uint256 amount0, - uint256 amount1, - address indexed to - ); - - event RemoveLiquidity( - address indexed from, - address indexed lendgine, - uint256 liquidity, - uint256 size, - uint256 amount0, - uint256 amount1, - address indexed to - ); - - event Collect(address indexed from, address indexed lendgine, uint256 amount, address indexed to); - - LiquidityManager public liquidityManager; - - function setUp() external { - _setUp(); - liquidityManager = new LiquidityManager(address(factory), address(0)); - } - - function _addLiquidity( - address to, - address from, - uint256 amount0, - uint256 amount1, - uint256 liquidity - ) internal { - token0.mint(from, amount0); - token1.mint(from, amount1); - - if (from != address(this)) { - vm.startPrank(from); - token0.approve(address(liquidityManager), amount0); - token1.approve(address(liquidityManager), amount1); - vm.stopPrank(); - } else { - token0.approve(address(liquidityManager), amount0); - token1.approve(address(liquidityManager), amount1); - } - - if (from != address(this)) vm.prank(from); - liquidityManager.addLiquidity( - LiquidityManager.AddLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - liquidity: liquidity, - amount0Min: amount0, - amount1Min: amount1, - sizeMin: 0, - recipient: to, - deadline: block.timestamp - }) - ); - } - - function testAddPositionEmpty() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - // test lendgine storage - assertEq(lendgine.totalLiquidity(), 1 ether); - assertEq(lendgine.totalPositionSize(), 1 ether); - assertEq(lendgine.reserve0(), 1 ether); - assertEq(lendgine.reserve1(), 8 ether); - - // test lendgine position - (uint256 positionSize, , ) = lendgine.positions(address(liquidityManager)); - assertEq(1 ether, positionSize); - - // test balances - assertEq(0, token0.balanceOf(address(liquidityManager))); - assertEq(0, token1.balanceOf(address(liquidityManager))); - - // test liquidity manager position - (positionSize, , ) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(1 ether, positionSize); - } - - function testAddPosition() external { - _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); - - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - // test lendgine storage - assertEq(lendgine.totalLiquidity(), 2 ether); - assertEq(lendgine.totalPositionSize(), 2 ether); - assertEq(lendgine.reserve0(), 2 ether); - assertEq(lendgine.reserve1(), 16 ether); - - // test lendgine position - (uint256 positionSize, , ) = lendgine.positions(address(liquidityManager)); - assertEq(1 ether, positionSize); - - // test balances - assertEq(0, token0.balanceOf(address(liquidityManager))); - assertEq(0, token1.balanceOf(address(liquidityManager))); - - // test liquidity manager position - (positionSize, , ) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(1 ether, positionSize); - } - - function testDeadline() external { - vm.warp(2); - - vm.expectRevert(LiquidityManager.LivelinessError.selector); - liquidityManager.addLiquidity( - LiquidityManager.AddLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - liquidity: 1 ether, - amount0Min: 1 ether, - amount1Min: 8 ether, - sizeMin: 1 ether, - recipient: cuh, - deadline: 1 - }) - ); - } - - function testAmountErrorAdd() external { - token0.mint(cuh, 1 ether); - token1.mint(cuh, 8 ether); - - vm.startPrank(cuh); - token0.approve(address(liquidityManager), 1 ether); - token1.approve(address(liquidityManager), 8 ether); - - vm.expectRevert(LiquidityManager.AmountError.selector); - liquidityManager.addLiquidity( - LiquidityManager.AddLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - liquidity: 1 ether, - amount0Min: 1 ether, - amount1Min: 8 ether, - sizeMin: 1 ether + 1, - recipient: cuh, - deadline: block.timestamp - }) - ); - vm.stopPrank(); - } - - function testAccruePosition() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - _mint(address(this), address(this), 5 ether); - - vm.warp(365 days + 1); - - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - uint256 size = (1 ether * 1 ether) / (1 ether - lpDilution); - - // test lendgine storage - assertEq(lendgine.totalLiquidity(), 1.5 ether); - assertEq(lendgine.totalPositionSize(), 1 ether + size); - assertEq(lendgine.reserve0(), 1.5 ether); - assertEq(lendgine.reserve1(), 12 ether); - - // test lendgine position - (uint256 positionSize, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions( - address(liquidityManager) - ); - assertEq(1 ether + size, positionSize); - assertEq(10 * lpDilution, rewardPerPositionPaid); - assertEq(10 * lpDilution, tokensOwed); - - // test balances - assertEq(0, token0.balanceOf(address(liquidityManager))); - assertEq(0, token1.balanceOf(address(liquidityManager))); - - // test liquidity manager position - (positionSize, rewardPerPositionPaid, tokensOwed) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(size + 1 ether, positionSize); - assertEq(10 * lpDilution, rewardPerPositionPaid); - assertEq(10 * lpDilution, tokensOwed); - } - - function testNewPositionAccrued() external { - _addLiquidity(address(this), address(this), 1 ether, 8 ether, 1 ether); - - _mint(address(this), address(this), 5 ether); - - vm.warp(365 days + 1); - - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - uint256 size = (1 ether * 1 ether) / (1 ether - lpDilution); - - // test lendgine storage - assertEq(lendgine.totalLiquidity(), 1.5 ether); - assertEq(lendgine.totalPositionSize(), 1 ether + size); - assertEq(lendgine.reserve0(), 1.5 ether); - assertEq(lendgine.reserve1(), 12 ether); - - // test lendgine position - (uint256 positionSize, uint256 rewardPerPositionPaid, ) = lendgine.positions(address(liquidityManager)); - assertEq(1 ether + size, positionSize); - assertEq(10 * lpDilution, rewardPerPositionPaid); - - // test balances - assertEq(0, token0.balanceOf(address(liquidityManager))); - assertEq(0, token1.balanceOf(address(liquidityManager))); - - // test liquidity manager position - (positionSize, rewardPerPositionPaid, ) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(size, positionSize); - assertEq(10 * lpDilution, rewardPerPositionPaid); - } - - function testCallbackValidation() external { - vm.expectRevert(LiquidityManager.ValidationError.selector); - liquidityManager.pairMintCallback( - 0, - abi.encode( - LiquidityManager.PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - amount0: 0, - amount1: 0, - payer: address(this) - }) - ) - ); + event AddLiquidity( + address indexed from, + address indexed lendgine, + uint256 liquidity, + uint256 size, + uint256 amount0, + uint256 amount1, + address indexed to + ); + + event RemoveLiquidity( + address indexed from, + address indexed lendgine, + uint256 liquidity, + uint256 size, + uint256 amount0, + uint256 amount1, + address indexed to + ); + + event Collect(address indexed from, address indexed lendgine, uint256 amount, address indexed to); + + LiquidityManager public liquidityManager; + + function setUp() external { + _setUp(); + liquidityManager = new LiquidityManager(address(factory), address(0)); + } + + function _addLiquidity(address to, address from, uint256 amount0, uint256 amount1, uint256 liquidity) internal { + token0.mint(from, amount0); + token1.mint(from, amount1); + + if (from != address(this)) { + vm.startPrank(from); + token0.approve(address(liquidityManager), amount0); + token1.approve(address(liquidityManager), amount1); + vm.stopPrank(); + } else { + token0.approve(address(liquidityManager), amount0); + token1.approve(address(liquidityManager), amount1); } - function testEmitAdd() external { - token0.mint(cuh, 1 ether); - token1.mint(cuh, 8 ether); - - vm.startPrank(cuh); - token0.approve(address(liquidityManager), 1 ether); - token1.approve(address(liquidityManager), 8 ether); - - vm.expectEmit(true, true, true, true, address(liquidityManager)); - emit AddLiquidity(cuh, address(lendgine), 1 ether, 1 ether, 1 ether, 8 ether, cuh); - liquidityManager.addLiquidity( - LiquidityManager.AddLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - liquidity: 1 ether, - amount0Min: 1 ether, - amount1Min: 8 ether, - sizeMin: 1 ether, - recipient: cuh, - deadline: block.timestamp - }) - ); - vm.stopPrank(); - } - - function testRemovePosition() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - vm.prank(cuh); - liquidityManager.removeLiquidity( - LiquidityManager.RemoveLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - size: 1 ether, - amount0Min: 1 ether, - amount1Min: 8 ether, - recipient: cuh, - deadline: block.timestamp - }) - ); - - // test lendgine storage - assertEq(lendgine.totalLiquidity(), 0); - assertEq(lendgine.totalPositionSize(), 0); - assertEq(lendgine.reserve0(), 0); - assertEq(lendgine.reserve1(), 0); - - // test lendgine position - (uint256 positionSize, , ) = lendgine.positions(address(liquidityManager)); - assertEq(0, positionSize); - - // test balances - assertEq(0, token0.balanceOf(address(liquidityManager))); - assertEq(0, token1.balanceOf(address(liquidityManager))); - - // test liquidity manager position - (positionSize, , ) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(0, positionSize); - } + if (from != address(this)) vm.prank(from); + liquidityManager.addLiquidity( + LiquidityManager.AddLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + liquidity: liquidity, + amount0Min: amount0, + amount1Min: amount1, + sizeMin: 0, + recipient: to, + deadline: block.timestamp + }) + ); + } + + function testAddPositionEmpty() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + // test lendgine storage + assertEq(lendgine.totalLiquidity(), 1 ether); + assertEq(lendgine.totalPositionSize(), 1 ether); + assertEq(lendgine.reserve0(), 1 ether); + assertEq(lendgine.reserve1(), 8 ether); + + // test lendgine position + (uint256 positionSize,,) = lendgine.positions(address(liquidityManager)); + assertEq(1 ether, positionSize); + + // test balances + assertEq(0, token0.balanceOf(address(liquidityManager))); + assertEq(0, token1.balanceOf(address(liquidityManager))); + + // test liquidity manager position + (positionSize,,) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(1 ether, positionSize); + } + + function testAddPosition() external { + _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); + + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + // test lendgine storage + assertEq(lendgine.totalLiquidity(), 2 ether); + assertEq(lendgine.totalPositionSize(), 2 ether); + assertEq(lendgine.reserve0(), 2 ether); + assertEq(lendgine.reserve1(), 16 ether); + + // test lendgine position + (uint256 positionSize,,) = lendgine.positions(address(liquidityManager)); + assertEq(1 ether, positionSize); + + // test balances + assertEq(0, token0.balanceOf(address(liquidityManager))); + assertEq(0, token1.balanceOf(address(liquidityManager))); + + // test liquidity manager position + (positionSize,,) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(1 ether, positionSize); + } + + function testDeadline() external { + vm.warp(2); + + vm.expectRevert(LiquidityManager.LivelinessError.selector); + liquidityManager.addLiquidity( + LiquidityManager.AddLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + liquidity: 1 ether, + amount0Min: 1 ether, + amount1Min: 8 ether, + sizeMin: 1 ether, + recipient: cuh, + deadline: 1 + }) + ); + } + + function testAmountErrorAdd() external { + token0.mint(cuh, 1 ether); + token1.mint(cuh, 8 ether); + + vm.startPrank(cuh); + token0.approve(address(liquidityManager), 1 ether); + token1.approve(address(liquidityManager), 8 ether); + + vm.expectRevert(LiquidityManager.AmountError.selector); + liquidityManager.addLiquidity( + LiquidityManager.AddLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + liquidity: 1 ether, + amount0Min: 1 ether, + amount1Min: 8 ether, + sizeMin: 1 ether + 1, + recipient: cuh, + deadline: block.timestamp + }) + ); + vm.stopPrank(); + } + + function testAccruePosition() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + _mint(address(this), address(this), 5 ether); + + vm.warp(365 days + 1); + + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 size = (1 ether * 1 ether) / (1 ether - lpDilution); + + // test lendgine storage + assertEq(lendgine.totalLiquidity(), 1.5 ether); + assertEq(lendgine.totalPositionSize(), 1 ether + size); + assertEq(lendgine.reserve0(), 1.5 ether); + assertEq(lendgine.reserve1(), 12 ether); + + // test lendgine position + (uint256 positionSize, uint256 rewardPerPositionPaid, uint256 tokensOwed) = + lendgine.positions(address(liquidityManager)); + assertEq(1 ether + size, positionSize); + assertEq(10 * lpDilution, rewardPerPositionPaid); + assertEq(10 * lpDilution, tokensOwed); + + // test balances + assertEq(0, token0.balanceOf(address(liquidityManager))); + assertEq(0, token1.balanceOf(address(liquidityManager))); + + // test liquidity manager position + (positionSize, rewardPerPositionPaid, tokensOwed) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(size + 1 ether, positionSize); + assertEq(10 * lpDilution, rewardPerPositionPaid); + assertEq(10 * lpDilution, tokensOwed); + } + + function testNewPositionAccrued() external { + _addLiquidity(address(this), address(this), 1 ether, 8 ether, 1 ether); + + _mint(address(this), address(this), 5 ether); + + vm.warp(365 days + 1); + + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + uint256 size = (1 ether * 1 ether) / (1 ether - lpDilution); + + // test lendgine storage + assertEq(lendgine.totalLiquidity(), 1.5 ether); + assertEq(lendgine.totalPositionSize(), 1 ether + size); + assertEq(lendgine.reserve0(), 1.5 ether); + assertEq(lendgine.reserve1(), 12 ether); + + // test lendgine position + (uint256 positionSize, uint256 rewardPerPositionPaid,) = lendgine.positions(address(liquidityManager)); + assertEq(1 ether + size, positionSize); + assertEq(10 * lpDilution, rewardPerPositionPaid); + + // test balances + assertEq(0, token0.balanceOf(address(liquidityManager))); + assertEq(0, token1.balanceOf(address(liquidityManager))); + + // test liquidity manager position + (positionSize, rewardPerPositionPaid,) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(size, positionSize); + assertEq(10 * lpDilution, rewardPerPositionPaid); + } + + function testCallbackValidation() external { + vm.expectRevert(LiquidityManager.ValidationError.selector); + liquidityManager.pairMintCallback( + 0, + abi.encode( + LiquidityManager.PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + amount0: 0, + amount1: 0, + payer: address(this) + }) + ) + ); + } + + function testEmitAdd() external { + token0.mint(cuh, 1 ether); + token1.mint(cuh, 8 ether); + + vm.startPrank(cuh); + token0.approve(address(liquidityManager), 1 ether); + token1.approve(address(liquidityManager), 8 ether); + + vm.expectEmit(true, true, true, true, address(liquidityManager)); + emit AddLiquidity(cuh, address(lendgine), 1 ether, 1 ether, 1 ether, 8 ether, cuh); + liquidityManager.addLiquidity( + LiquidityManager.AddLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + liquidity: 1 ether, + amount0Min: 1 ether, + amount1Min: 8 ether, + sizeMin: 1 ether, + recipient: cuh, + deadline: block.timestamp + }) + ); + vm.stopPrank(); + } + + function testRemovePosition() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + vm.prank(cuh); + liquidityManager.removeLiquidity( + LiquidityManager.RemoveLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + size: 1 ether, + amount0Min: 1 ether, + amount1Min: 8 ether, + recipient: cuh, + deadline: block.timestamp + }) + ); - function testRemoveAmountError() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - vm.prank(cuh); - vm.expectRevert(LiquidityManager.AmountError.selector); - liquidityManager.removeLiquidity( - LiquidityManager.RemoveLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - size: 1 ether, - amount0Min: 1 ether + 1, - amount1Min: 8 ether + 1, - recipient: cuh, - deadline: block.timestamp - }) - ); - } + // test lendgine storage + assertEq(lendgine.totalLiquidity(), 0); + assertEq(lendgine.totalPositionSize(), 0); + assertEq(lendgine.reserve0(), 0); + assertEq(lendgine.reserve1(), 0); + + // test lendgine position + (uint256 positionSize,,) = lendgine.positions(address(liquidityManager)); + assertEq(0, positionSize); + + // test balances + assertEq(0, token0.balanceOf(address(liquidityManager))); + assertEq(0, token1.balanceOf(address(liquidityManager))); + + // test liquidity manager position + (positionSize,,) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(0, positionSize); + } + + function testRemoveAmountError() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + vm.prank(cuh); + vm.expectRevert(LiquidityManager.AmountError.selector); + liquidityManager.removeLiquidity( + LiquidityManager.RemoveLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + size: 1 ether, + amount0Min: 1 ether + 1, + amount1Min: 8 ether + 1, + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testRemoveNoRecipient() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + vm.prank(cuh); + liquidityManager.removeLiquidity( + LiquidityManager.RemoveLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + size: 1 ether, + amount0Min: 1 ether, + amount1Min: 8 ether, + recipient: address(0), + deadline: block.timestamp + }) + ); - function testRemoveNoRecipient() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - vm.prank(cuh); - liquidityManager.removeLiquidity( - LiquidityManager.RemoveLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - size: 1 ether, - amount0Min: 1 ether, - amount1Min: 8 ether, - recipient: address(0), - deadline: block.timestamp - }) - ); - - // test lendgine storage - assertEq(lendgine.totalLiquidity(), 0); - assertEq(lendgine.totalPositionSize(), 0); - assertEq(lendgine.reserve0(), 0); - assertEq(lendgine.reserve1(), 0); - - // test lendgine position - (uint256 positionSize, , ) = lendgine.positions(address(liquidityManager)); - assertEq(0, positionSize); - - // test balances - assertEq(1 ether, token0.balanceOf(address(liquidityManager))); - assertEq(8 ether, token1.balanceOf(address(liquidityManager))); - - // test liquidity manager position - (positionSize, , ) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(0, positionSize); - } + // test lendgine storage + assertEq(lendgine.totalLiquidity(), 0); + assertEq(lendgine.totalPositionSize(), 0); + assertEq(lendgine.reserve0(), 0); + assertEq(lendgine.reserve1(), 0); + + // test lendgine position + (uint256 positionSize,,) = lendgine.positions(address(liquidityManager)); + assertEq(0, positionSize); + + // test balances + assertEq(1 ether, token0.balanceOf(address(liquidityManager))); + assertEq(8 ether, token1.balanceOf(address(liquidityManager))); + + // test liquidity manager position + (positionSize,,) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(0, positionSize); + } + + function testOverRemove() external { + _addLiquidity(address(this), address(this), 1 ether, 8 ether, 1 ether); + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + vm.prank(cuh); + vm.expectRevert(); + liquidityManager.removeLiquidity( + LiquidityManager.RemoveLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + size: 1.5 ether, + amount0Min: 1 ether, + amount1Min: 8 ether, + recipient: address(0), + deadline: block.timestamp + }) + ); + } + + function testEmitRemove() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + + vm.prank(cuh); + vm.expectEmit(true, true, true, true, address(liquidityManager)); + emit RemoveLiquidity(cuh, address(lendgine), 1 ether, 1 ether, 1 ether, 8 ether, cuh); + liquidityManager.removeLiquidity( + LiquidityManager.RemoveLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + size: 1 ether, + amount0Min: 1 ether, + amount1Min: 8 ether, + recipient: cuh, + deadline: block.timestamp + }) + ); + } + + function testRemoveAccrue() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + vm.warp(365 days + 1); + + vm.prank(cuh); + liquidityManager.removeLiquidity( + LiquidityManager.RemoveLiquidityParams({ + token0: address(token0), + token1: address(token1), + token0Exp: token0Scale, + token1Exp: token1Scale, + upperBound: upperBound, + size: 0.5 ether, + amount0Min: 0, + amount1Min: 0, + recipient: cuh, + deadline: block.timestamp + }) + ); - function testOverRemove() external { - _addLiquidity(address(this), address(this), 1 ether, 8 ether, 1 ether); - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - vm.prank(cuh); - vm.expectRevert(); - liquidityManager.removeLiquidity( - LiquidityManager.RemoveLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - size: 1.5 ether, - amount0Min: 1 ether, - amount1Min: 8 ether, - recipient: address(0), - deadline: block.timestamp - }) - ); - } + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + + // test lendgine storage + assertEq(lendgine.totalLiquidity(), lpDilution / 2); + assertEq(lendgine.totalPositionSize(), 0.5 ether); + assertEq(lendgine.reserve0(), lpDilution / 2); + assertEq(lendgine.reserve1(), lpDilution * 4); + + // test lendgine position + (uint256 positionSize, uint256 rewardPerPositionPaid,) = lendgine.positions(address(liquidityManager)); + assertEq(0.5 ether, positionSize); + assertEq(lpDilution * 10, rewardPerPositionPaid); + + // test balances + assertEq(0, token0.balanceOf(address(liquidityManager))); + assertEq(0, token1.balanceOf(address(liquidityManager))); + + // test liquidity manager position + (positionSize, rewardPerPositionPaid,) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(0.5 ether, positionSize); + assertEq(lpDilution * 10, rewardPerPositionPaid); + } + + function testCollect() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + vm.warp(365 days + 1); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + + vm.prank(cuh); + liquidityManager.collect( + LiquidityManager.CollectParams({lendgine: address(lendgine), recipient: cuh, amountRequested: lpDilution * 10}) + ); - function testEmitRemove() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - - vm.prank(cuh); - vm.expectEmit(true, true, true, true, address(liquidityManager)); - emit RemoveLiquidity(cuh, address(lendgine), 1 ether, 1 ether, 1 ether, 8 ether, cuh); - liquidityManager.removeLiquidity( - LiquidityManager.RemoveLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - size: 1 ether, - amount0Min: 1 ether, - amount1Min: 8 ether, - recipient: cuh, - deadline: block.timestamp - }) - ); - } + // test lendgine storage slots + assertEq(lpDilution * 10, lendgine.rewardPerPositionStored()); - function testRemoveAccrue() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); - - vm.prank(cuh); - liquidityManager.removeLiquidity( - LiquidityManager.RemoveLiquidityParams({ - token0: address(token0), - token1: address(token1), - token0Exp: token0Scale, - token1Exp: token1Scale, - upperBound: upperBound, - size: 0.5 ether, - amount0Min: 0, - amount1Min: 0, - recipient: cuh, - deadline: block.timestamp - }) - ); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - // test lendgine storage - assertEq(lendgine.totalLiquidity(), lpDilution / 2); - assertEq(lendgine.totalPositionSize(), 0.5 ether); - assertEq(lendgine.reserve0(), lpDilution / 2); - assertEq(lendgine.reserve1(), lpDilution * 4); - - // test lendgine position - (uint256 positionSize, uint256 rewardPerPositionPaid, ) = lendgine.positions(address(liquidityManager)); - assertEq(0.5 ether, positionSize); - assertEq(lpDilution * 10, rewardPerPositionPaid); - - // test balances - assertEq(0, token0.balanceOf(address(liquidityManager))); - assertEq(0, token1.balanceOf(address(liquidityManager))); - - // test liquidity manager position - (positionSize, rewardPerPositionPaid, ) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(0.5 ether, positionSize); - assertEq(lpDilution * 10, rewardPerPositionPaid); - } + // test lendgine position + (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(address(liquidityManager)); + assertEq(lpDilution * 10, rewardPerPositionPaid); + assertEq(0, tokensOwed); - function testCollect() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - vm.prank(cuh); - liquidityManager.collect( - LiquidityManager.CollectParams({ - lendgine: address(lendgine), - recipient: cuh, - amountRequested: lpDilution * 10 - }) - ); - - // test lendgine storage slots - assertEq(lpDilution * 10, lendgine.rewardPerPositionStored()); - - // test lendgine position - (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(address(liquidityManager)); - assertEq(lpDilution * 10, rewardPerPositionPaid); - assertEq(0, tokensOwed); - - // test liquidity manager position - (, rewardPerPositionPaid, tokensOwed) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(lpDilution * 10, rewardPerPositionPaid); - assertEq(0, tokensOwed); - - // test user balances - assertEq(token1.balanceOf(cuh), lpDilution * 10); - } + // test liquidity manager position + (, rewardPerPositionPaid, tokensOwed) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(lpDilution * 10, rewardPerPositionPaid); + assertEq(0, tokensOwed); - function testOverCollect() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); + // test user balances + assertEq(token1.balanceOf(cuh), lpDilution * 10); + } - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + function testOverCollect() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + vm.warp(365 days + 1); - vm.prank(cuh); - liquidityManager.collect( - LiquidityManager.CollectParams({ lendgine: address(lendgine), recipient: cuh, amountRequested: 100 ether }) - ); + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - // test lendgine storage slots - assertEq(lpDilution * 10, lendgine.rewardPerPositionStored()); + vm.prank(cuh); + liquidityManager.collect( + LiquidityManager.CollectParams({lendgine: address(lendgine), recipient: cuh, amountRequested: 100 ether}) + ); - // test lendgine position - (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(address(liquidityManager)); - assertEq(lpDilution * 10, rewardPerPositionPaid); - assertEq(0, tokensOwed); + // test lendgine storage slots + assertEq(lpDilution * 10, lendgine.rewardPerPositionStored()); + + // test lendgine position + (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(address(liquidityManager)); + assertEq(lpDilution * 10, rewardPerPositionPaid); + assertEq(0, tokensOwed); + + // test liquidity manager position + (, rewardPerPositionPaid, tokensOwed) = liquidityManager.positions(cuh, address(lendgine)); + assertEq(lpDilution * 10, rewardPerPositionPaid); + assertEq(0, tokensOwed); + + // test user balances + assertEq(token1.balanceOf(cuh), lpDilution * 10); + } + + function testCollectNoRecipient() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + vm.warp(365 days + 1); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + + vm.prank(cuh); + liquidityManager.collect( + LiquidityManager.CollectParams({ + lendgine: address(lendgine), + recipient: address(0), + amountRequested: lpDilution * 10 + }) + ); - // test liquidity manager position - (, rewardPerPositionPaid, tokensOwed) = liquidityManager.positions(cuh, address(lendgine)); - assertEq(lpDilution * 10, rewardPerPositionPaid); - assertEq(0, tokensOwed); + // test user balances + assertEq(token1.balanceOf(address(liquidityManager)), lpDilution * 10); + } - // test user balances - assertEq(token1.balanceOf(cuh), lpDilution * 10); - } + function testEmitCollect() external { + _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); + _mint(address(this), address(this), 5 ether); + vm.warp(365 days + 1); - function testCollectNoRecipient() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - vm.prank(cuh); - liquidityManager.collect( - LiquidityManager.CollectParams({ - lendgine: address(lendgine), - recipient: address(0), - amountRequested: lpDilution * 10 - }) - ); - - // test user balances - assertEq(token1.balanceOf(address(liquidityManager)), lpDilution * 10); - } + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - function testEmitCollect() external { - _addLiquidity(cuh, cuh, 1 ether, 8 ether, 1 ether); - _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - vm.prank(cuh); - vm.expectEmit(true, true, true, true, address(liquidityManager)); - emit Collect(cuh, address(lendgine), lpDilution * 10, cuh); - liquidityManager.collect( - LiquidityManager.CollectParams({ - lendgine: address(lendgine), - recipient: cuh, - amountRequested: lpDilution * 10 - }) - ); - } + vm.prank(cuh); + vm.expectEmit(true, true, true, true, address(liquidityManager)); + emit Collect(cuh, address(lendgine), lpDilution * 10, cuh); + liquidityManager.collect( + LiquidityManager.CollectParams({lendgine: address(lendgine), recipient: cuh, amountRequested: lpDilution * 10}) + ); + } } diff --git a/test/MintTest.t.sol b/test/MintTest.t.sol index 65b0b79..750ddf6 100644 --- a/test/MintTest.t.sol +++ b/test/MintTest.t.sol @@ -6,177 +6,179 @@ import { Pair } from "../src/core/Pair.sol"; import { TestHelper } from "./utils/TestHelper.sol"; contract MintTest is TestHelper { - event Mint(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); - - event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to); - - function setUp() external { - _setUp(); - _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); - } - - function testMintPartial() external { - uint256 shares = _mint(cuh, cuh, 5 ether); - - // check lendgine token - assertEq(0.5 ether, shares); - assertEq(0.5 ether, lendgine.totalSupply()); - assertEq(0.5 ether, lendgine.balanceOf(cuh)); - - // check lendgine storage slots - assertEq(0.5 ether, lendgine.totalLiquidityBorrowed()); - assertEq(0.5 ether, lendgine.totalLiquidity()); - assertEq(0.5 ether, uint256(lendgine.reserve0())); - assertEq(4 ether, uint256(lendgine.reserve1())); + event Mint(address indexed sender, uint256 collateral, uint256 shares, uint256 liquidity, address indexed to); + + event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to); + + function setUp() external { + _setUp(); + _deposit(address(this), address(this), 1 ether, 8 ether, 1 ether); + } + + function testMintPartial() external { + uint256 shares = _mint(cuh, cuh, 5 ether); + + // check lendgine token + assertEq(0.5 ether, shares); + assertEq(0.5 ether, lendgine.totalSupply()); + assertEq(0.5 ether, lendgine.balanceOf(cuh)); + + // check lendgine storage slots + assertEq(0.5 ether, lendgine.totalLiquidityBorrowed()); + assertEq(0.5 ether, lendgine.totalLiquidity()); + assertEq(0.5 ether, uint256(lendgine.reserve0())); + assertEq(4 ether, uint256(lendgine.reserve1())); - // check lendgine balances - assertEq(0.5 ether, token0.balanceOf(address(lendgine))); - assertEq(4 ether + 5 ether, token1.balanceOf(address(lendgine))); + // check lendgine balances + assertEq(0.5 ether, token0.balanceOf(address(lendgine))); + assertEq(4 ether + 5 ether, token1.balanceOf(address(lendgine))); - // check user balances - assertEq(0.5 ether, token0.balanceOf(cuh)); - assertEq(4 ether, token1.balanceOf(cuh)); - } - - function testMintFull() external { - uint256 shares = _mint(cuh, cuh, 10 ether); - - // check lendgine token - assertEq(1 ether, shares); - assertEq(1 ether, lendgine.totalSupply()); - assertEq(1 ether, lendgine.balanceOf(cuh)); - - // check lendgine storage slots - assertEq(1 ether, lendgine.totalLiquidityBorrowed()); - assertEq(0, lendgine.totalLiquidity()); - assertEq(0, uint256(lendgine.reserve0())); - assertEq(0, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(0, token0.balanceOf(address(lendgine))); - assertEq(10 ether, token1.balanceOf(address(lendgine))); - - // check user balances - assertEq(1 ether, token0.balanceOf(cuh)); - assertEq(8 ether, token1.balanceOf(cuh)); - } - - function testMintFullDouble() external { - _mint(cuh, cuh, 5 ether); - _mint(cuh, cuh, 5 ether); - } - - function testZeroMint() external { - vm.expectRevert(Lendgine.InputError.selector); - lendgine.mint(cuh, 0, bytes("")); - } - - function testOverMint() external { - _mint(address(this), address(this), 5 ether); - - vm.expectRevert(Lendgine.CompleteUtilizationError.selector); - lendgine.mint(cuh, 5 ether + 10, bytes("")); - } - - function testEmitLendgine() external { - token1.mint(cuh, 5 ether); - - vm.prank(cuh); - token1.approve(address(this), 5 ether); - - vm.expectEmit(true, true, false, true, address(lendgine)); - emit Mint(address(this), 5 ether, 0.5 ether, 0.5 ether, cuh); - lendgine.mint(cuh, 5 ether, abi.encode(MintCallbackData({ token: address(token1), payer: cuh }))); - } - - function testEmitPair() external { - token1.mint(cuh, 5 ether); - - vm.prank(cuh); - token1.approve(address(this), 5 ether); - - vm.expectEmit(true, false, false, true, address(lendgine)); - emit Burn(0.5 ether, 4 ether, 0.5 ether, cuh); - lendgine.mint(cuh, 5 ether, abi.encode(MintCallbackData({ token: address(token1), payer: cuh }))); - } - - function testAccrueOnMint() external { - _mint(cuh, cuh, 1 ether); - vm.warp(365 days + 1); - _mint(cuh, cuh, 1 ether); - - assertEq(365 days + 1, lendgine.lastUpdate()); - assert(lendgine.rewardPerPositionStored() != 0); - } - - function testProportionalMint() external { - _mint(cuh, cuh, 5 ether); - vm.warp(365 days + 1); - uint256 shares = _mint(cuh, cuh, 1 ether); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - // check mint amount - assertEq((0.1 ether * 0.5 ether) / (0.5 ether - lpDilution), shares); - assertEq(shares + 0.5 ether, lendgine.balanceOf(cuh)); - - // check lendgine storage slots - assertEq(0.6 ether - lpDilution, lendgine.totalLiquidityBorrowed()); - assertEq(shares + 0.5 ether, lendgine.totalSupply()); - } - - function testNonStandardDecimals() external { - token1Scale = 9; - - lendgine = Lendgine( - factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound) - ); - - token0.mint(address(this), 1e18); - token1.mint(address(this), 8 * 1e9); - - lendgine.deposit( - address(this), - 1 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 1e18, - amount1: 8 * 1e9, - payer: address(this) - }) - ) - ); - - token1.mint(cuh, 5 * 1e9); - - vm.prank(cuh); - token1.approve(address(this), 5 * 1e9); - uint256 shares = lendgine.mint( - cuh, - 5 * 1e9, - abi.encode(MintCallbackData({ token: address(token1), payer: cuh })) - ); - - // check lendgine token - assertEq(0.5 ether, shares); - assertEq(0.5 ether, lendgine.totalSupply()); - assertEq(0.5 ether, lendgine.balanceOf(cuh)); - - // check lendgine storage slots - assertEq(0.5 ether, lendgine.totalLiquidityBorrowed()); - assertEq(0.5 ether, lendgine.totalLiquidity()); - assertEq(0.5 ether, uint256(lendgine.reserve0())); - assertEq(4 * 1e9, uint256(lendgine.reserve1())); - - // check lendgine balances - assertEq(0.5 ether, token0.balanceOf(address(lendgine))); - assertEq(9 * 1e9, token1.balanceOf(address(lendgine))); - - // check user balances - assertEq(0.5 ether, token0.balanceOf(cuh)); - assertEq(4 * 1e9, token1.balanceOf(cuh)); - } + // check user balances + assertEq(0.5 ether, token0.balanceOf(cuh)); + assertEq(4 ether, token1.balanceOf(cuh)); + } + + function testMintFull() external { + uint256 shares = _mint(cuh, cuh, 10 ether); + + // check lendgine token + assertEq(1 ether, shares); + assertEq(1 ether, lendgine.totalSupply()); + assertEq(1 ether, lendgine.balanceOf(cuh)); + + // check lendgine storage slots + assertEq(1 ether, lendgine.totalLiquidityBorrowed()); + assertEq(0, lendgine.totalLiquidity()); + assertEq(0, uint256(lendgine.reserve0())); + assertEq(0, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(0, token0.balanceOf(address(lendgine))); + assertEq(10 ether, token1.balanceOf(address(lendgine))); + + // check user balances + assertEq(1 ether, token0.balanceOf(cuh)); + assertEq(8 ether, token1.balanceOf(cuh)); + } + + function testMintFullDouble() external { + _mint(cuh, cuh, 5 ether); + _mint(cuh, cuh, 5 ether); + } + + function testZeroMint() external { + vm.expectRevert(Lendgine.InputError.selector); + lendgine.mint(cuh, 0, bytes("")); + } + + function testOverMint() external { + _mint(address(this), address(this), 5 ether); + + vm.expectRevert(Lendgine.CompleteUtilizationError.selector); + lendgine.mint(cuh, 5 ether + 10, bytes("")); + } + + function testEmitLendgine() external { + token1.mint(cuh, 5 ether); + + vm.prank(cuh); + token1.approve(address(this), 5 ether); + + vm.expectEmit(true, true, false, true, address(lendgine)); + emit Mint(address(this), 5 ether, 0.5 ether, 0.5 ether, cuh); + lendgine.mint(cuh, 5 ether, abi.encode(MintCallbackData({token: address(token1), payer: cuh}))); + } + + function testEmitPair() external { + token1.mint(cuh, 5 ether); + + vm.prank(cuh); + token1.approve(address(this), 5 ether); + + vm.expectEmit(true, false, false, true, address(lendgine)); + emit Burn(0.5 ether, 4 ether, 0.5 ether, cuh); + lendgine.mint(cuh, 5 ether, abi.encode(MintCallbackData({token: address(token1), payer: cuh}))); + } + + function testAccrueOnMint() external { + _mint(cuh, cuh, 1 ether); + vm.warp(365 days + 1); + _mint(cuh, cuh, 1 ether); + + assertEq(365 days + 1, lendgine.lastUpdate()); + assert(lendgine.rewardPerPositionStored() != 0); + } + + function testProportionalMint() external { + _mint(cuh, cuh, 5 ether); + vm.warp(365 days + 1); + uint256 shares = _mint(cuh, cuh, 1 ether); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + + // check mint amount + assertEq((0.1 ether * 0.5 ether) / (0.5 ether - lpDilution), shares); + assertEq(shares + 0.5 ether, lendgine.balanceOf(cuh)); + + // check lendgine storage slots + assertEq(0.6 ether - lpDilution, lendgine.totalLiquidityBorrowed()); + assertEq(shares + 0.5 ether, lendgine.totalSupply()); + } + + function testNonStandardDecimals() external { + token1Scale = 9; + + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); + + token0.mint(address(this), 1e18); + token1.mint(address(this), 8 * 1e9); + + lendgine.deposit( + address(this), + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1e18, + amount1: 8 * 1e9, + payer: address(this) + }) + ) + ); + + token1.mint(cuh, 5 * 1e9); + + vm.prank(cuh); + token1.approve(address(this), 5 * 1e9); + uint256 shares = lendgine.mint(cuh, 5 * 1e9, abi.encode(MintCallbackData({token: address(token1), payer: cuh}))); + + // check lendgine token + assertEq(0.5 ether, shares); + assertEq(0.5 ether, lendgine.totalSupply()); + assertEq(0.5 ether, lendgine.balanceOf(cuh)); + + // check lendgine storage slots + assertEq(0.5 ether, lendgine.totalLiquidityBorrowed()); + assertEq(0.5 ether, lendgine.totalLiquidity()); + assertEq(0.5 ether, uint256(lendgine.reserve0())); + assertEq(4 * 1e9, uint256(lendgine.reserve1())); + + // check lendgine balances + assertEq(0.5 ether, token0.balanceOf(address(lendgine))); + assertEq(9 * 1e9, token1.balanceOf(address(lendgine))); + + // check user balances + assertEq(0.5 ether, token0.balanceOf(cuh)); + assertEq(4 * 1e9, token1.balanceOf(cuh)); + } + + function testMintAfterFullAccrue() external { + _mint(address(this), address(this), 5 ether); + vm.warp(730 days + 1); + + vm.expectRevert(Lendgine.CompleteUtilizationError.selector); + lendgine.mint(cuh, 1 ether, bytes("")); + } } diff --git a/test/PrecisionTest.t.sol b/test/PrecisionTest.t.sol index 9a8dfb9..87bdcf4 100644 --- a/test/PrecisionTest.t.sol +++ b/test/PrecisionTest.t.sol @@ -20,53 +20,49 @@ import { TestHelper } from "./utils/TestHelper.sol"; // max tvl: $100,000,000 contract PrecisionTest is TestHelper { - function setUp() external { - _setUp(); - } + function setUp() external { + _setUp(); + } - function testBaseline() external { - lendgine.invariant((upperBound * upperBound) / 1e18, 0, 1 ether); + function testBaseline() external { + lendgine.invariant((upperBound * upperBound) / 1e18, 0, 1 ether); - uint256 value = (upperBound * upperBound) / 1e18; - uint256 basePerDollar = 1e18; + uint256 value = (upperBound * upperBound) / 1e18; + uint256 basePerDollar = 1e18; - uint256 minLP = value / basePerDollar; + uint256 minLP = value / basePerDollar; - assert(type(uint120).max / basePerDollar >= 100_000_000); - assert(minLP < 1 ether); - } + assert(type(uint120).max / basePerDollar >= 100_000_000); + assert(minLP < 1 ether); + } - function testHighUpperBound() external { - upperBound = 1e27; - lendgine = Lendgine( - factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound) - ); + function testHighUpperBound() external { + upperBound = 1e27; + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); - lendgine.invariant((upperBound * upperBound) / 1e18, 0, 1 ether); + lendgine.invariant((upperBound * upperBound) / 1e18, 0, 1 ether); - uint256 value = (upperBound * upperBound) / 1e18; - uint256 basePerDollar = 1e21; + uint256 value = (upperBound * upperBound) / 1e18; + uint256 basePerDollar = 1e21; - uint256 minLP = value / basePerDollar; + uint256 minLP = value / basePerDollar; - assert(type(uint120).max / basePerDollar >= 100_000_000); - assert(minLP < 1 ether); - } + assert(type(uint120).max / basePerDollar >= 100_000_000); + assert(minLP < 1 ether); + } - function testLowUpperBound() external { - upperBound = 1e9; - lendgine = Lendgine( - factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound) - ); + function testLowUpperBound() external { + upperBound = 1e9; + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); - lendgine.invariant((upperBound * upperBound) / 1e18, 0, 1 ether); + lendgine.invariant((upperBound * upperBound) / 1e18, 0, 1 ether); - uint256 value = (upperBound * upperBound) / 1e18; - uint256 basePerDollar = 1e27; + uint256 value = (upperBound * upperBound) / 1e18; + uint256 basePerDollar = 1e27; - uint256 minLP = value / basePerDollar; + uint256 minLP = value / basePerDollar; - assert(type(uint120).max / basePerDollar >= 100_000_000); - assert(minLP < 1 ether); - } + assert(type(uint120).max / basePerDollar >= 100_000_000); + assert(minLP < 1 ether); + } } diff --git a/test/SwapTest.t.sol b/test/SwapTest.t.sol index b14d22f..bd85e80 100644 --- a/test/SwapTest.t.sol +++ b/test/SwapTest.t.sol @@ -6,87 +6,69 @@ import { Pair } from "../src/core/Pair.sol"; import { TestHelper } from "./utils/TestHelper.sol"; contract SwapTest is TestHelper { - event Swap(uint256 amount0Out, uint256 amount1Out, uint256 amount0In, uint256 amount1In, address indexed to); + event Swap(uint256 amount0Out, uint256 amount1Out, uint256 amount0In, uint256 amount1In, address indexed to); - function setUp() external { - _setUp(); - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - } + function setUp() external { + _setUp(); + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + } - function testSwapFull() external { - token0.mint(cuh, 24 ether); + function testSwapFull() external { + token0.mint(cuh, 24 ether); - vm.prank(cuh); - token0.approve(address(this), 24 ether); + vm.prank(cuh); + token0.approve(address(this), 24 ether); - lendgine.swap( - cuh, - 0, - 8 ether, - abi.encode( - SwapCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 24 ether, - amount1: 0, - payer: cuh - }) - ) - ); + lendgine.swap( + cuh, + 0, + 8 ether, + abi.encode( + SwapCallbackData({token0: address(token0), token1: address(token1), amount0: 24 ether, amount1: 0, payer: cuh}) + ) + ); - // check user balances - assertEq(0, token0.balanceOf(cuh)); - assertEq(8 ether, token1.balanceOf(cuh)); + // check user balances + assertEq(0, token0.balanceOf(cuh)); + assertEq(8 ether, token1.balanceOf(cuh)); - // check lendgine storage slots - assertEq(25 ether, lendgine.reserve0()); - assertEq(0, lendgine.reserve1()); - } + // check lendgine storage slots + assertEq(25 ether, lendgine.reserve0()); + assertEq(0, lendgine.reserve1()); + } - function testUnderPay() external { - token0.mint(cuh, 23 ether); + function testUnderPay() external { + token0.mint(cuh, 23 ether); - vm.prank(cuh); - token0.approve(address(this), 23 ether); + vm.prank(cuh); + token0.approve(address(this), 23 ether); - vm.expectRevert(Pair.InvariantError.selector); - lendgine.swap( - cuh, - 0, - 8 ether, - abi.encode( - SwapCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 23 ether, - amount1: 0, - payer: cuh - }) - ) - ); - } + vm.expectRevert(Pair.InvariantError.selector); + lendgine.swap( + cuh, + 0, + 8 ether, + abi.encode( + SwapCallbackData({token0: address(token0), token1: address(token1), amount0: 23 ether, amount1: 0, payer: cuh}) + ) + ); + } - function testEmit() external { - token0.mint(cuh, 24 ether); + function testEmit() external { + token0.mint(cuh, 24 ether); - vm.prank(cuh); - token0.approve(address(this), 24 ether); + vm.prank(cuh); + token0.approve(address(this), 24 ether); - vm.expectEmit(true, false, false, true, address(lendgine)); - emit Swap(0, 8 ether, 24 ether, 0, cuh); - lendgine.swap( - cuh, - 0, - 8 ether, - abi.encode( - SwapCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 24 ether, - amount1: 0, - payer: cuh - }) - ) - ); - } + vm.expectEmit(true, false, false, true, address(lendgine)); + emit Swap(0, 8 ether, 24 ether, 0, cuh); + lendgine.swap( + cuh, + 0, + 8 ether, + abi.encode( + SwapCallbackData({token0: address(token0), token1: address(token1), amount0: 24 ether, amount1: 0, payer: cuh}) + ) + ); + } } diff --git a/test/WithdrawTest.t.sol b/test/WithdrawTest.t.sol index fe6cbea..abc7eed 100644 --- a/test/WithdrawTest.t.sol +++ b/test/WithdrawTest.t.sol @@ -7,192 +7,184 @@ import { TestHelper } from "./utils/TestHelper.sol"; import { FullMath } from "../src/libraries/FullMath.sol"; contract WithdrawTest is TestHelper { - event Withdraw(address indexed sender, uint256 size, uint256 liquidity, address indexed to); + event Withdraw(address indexed sender, uint256 size, uint256 liquidity, address indexed to); - event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to); + event Burn(uint256 amount0Out, uint256 amount1Out, uint256 liquidity, address indexed to); - function setUp() external { - _setUp(); + function setUp() external { + _setUp(); - _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); - } + _deposit(cuh, cuh, 1 ether, 8 ether, 1 ether); + } - function testWithdrawPartial() external { - (uint256 amount0, uint256 amount1, uint256 liquidity) = _withdraw(cuh, cuh, 0.5 ether); + function testWithdrawPartial() external { + (uint256 amount0, uint256 amount1, uint256 liquidity) = _withdraw(cuh, cuh, 0.5 ether); - assertEq(liquidity, 0.5 ether); - assertEq(0.5 ether, amount0); - assertEq(4 ether, amount1); + assertEq(liquidity, 0.5 ether); + assertEq(0.5 ether, amount0); + assertEq(4 ether, amount1); - assertEq(0.5 ether, lendgine.totalLiquidity()); - assertEq(0.5 ether, lendgine.totalPositionSize()); + assertEq(0.5 ether, lendgine.totalLiquidity()); + assertEq(0.5 ether, lendgine.totalPositionSize()); - assertEq(0.5 ether, uint256(lendgine.reserve0())); - assertEq(4 ether, uint256(lendgine.reserve1())); - assertEq(0.5 ether, token0.balanceOf(address(lendgine))); - assertEq(4 ether, token1.balanceOf(address(lendgine))); - - assertEq(0.5 ether, token0.balanceOf(address(cuh))); - assertEq(4 ether, token1.balanceOf(address(cuh))); - - (uint256 positionSize, , ) = lendgine.positions(cuh); - assertEq(0.5 ether, positionSize); - } - - function testWithdrawFull() external { - (uint256 amount0, uint256 amount1, uint256 liquidity) = _withdraw(cuh, cuh, 1 ether); + assertEq(0.5 ether, uint256(lendgine.reserve0())); + assertEq(4 ether, uint256(lendgine.reserve1())); + assertEq(0.5 ether, token0.balanceOf(address(lendgine))); + assertEq(4 ether, token1.balanceOf(address(lendgine))); - assertEq(liquidity, 1 ether); - assertEq(1 ether, amount0); - assertEq(8 ether, amount1); + assertEq(0.5 ether, token0.balanceOf(address(cuh))); + assertEq(4 ether, token1.balanceOf(address(cuh))); - assertEq(0, lendgine.totalLiquidity()); - assertEq(0, lendgine.totalPositionSize()); - - assertEq(0, uint256(lendgine.reserve0())); - assertEq(0, uint256(lendgine.reserve1())); - assertEq(0, token0.balanceOf(address(lendgine))); - assertEq(0, token1.balanceOf(address(lendgine))); + (uint256 positionSize,,) = lendgine.positions(cuh); + assertEq(0.5 ether, positionSize); + } - assertEq(1 ether, token0.balanceOf(address(cuh))); - assertEq(8 ether, token1.balanceOf(address(cuh))); - - (uint256 positionSize, , ) = lendgine.positions(cuh); - assertEq(0, positionSize); - } + function testWithdrawFull() external { + (uint256 amount0, uint256 amount1, uint256 liquidity) = _withdraw(cuh, cuh, 1 ether); - function testEmitLendgine() external { - vm.expectEmit(true, true, false, true, address(lendgine)); - emit Withdraw(cuh, 1 ether, 1 ether, cuh); - _withdraw(cuh, cuh, 1 ether); - } - - function testEmitPair() external { - vm.expectEmit(true, false, false, true, address(lendgine)); - emit Burn(1 ether, 8 ether, 1 ether, cuh); - _withdraw(cuh, cuh, 1 ether); - } - - function testZeroWithdraw() external { - vm.expectRevert(Lendgine.InputError.selector); - _withdraw(cuh, cuh, 0); - } - - function testOverWithdraw() external { - vm.expectRevert(Lendgine.InsufficientPositionError.selector); - _withdraw(cuh, cuh, 2 ether); - } - - function testMaxUtilizedWithdraw() external { - _mint(address(this), address(this), 5 ether); - _withdraw(cuh, cuh, 0.5 ether); - } - - function testCompleteUtilization() external { - _mint(address(this), address(this), 5 ether); - - vm.expectRevert(Lendgine.CompleteUtilizationError.selector); - _withdraw(cuh, cuh, 0.5 ether + 1); - } - - function testAccrueOnWithdraw() external { - _mint(address(this), address(this), 1 ether); - vm.warp(365 days + 1); - _withdraw(cuh, cuh, .5 ether); - - assertEq(365 days + 1, lendgine.lastUpdate()); - assert(lendgine.rewardPerPositionStored() != 0); - } - - function testAccrueOnPositionWithdraw() external { - _mint(address(this), address(this), 1 ether); - vm.warp(365 days + 1); - _withdraw(cuh, cuh, .5 ether); - - (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); - assert(rewardPerPositionPaid != 0); - assert(tokensOwed != 0); - } - - function testProportionalPositionSize() external { - uint256 shares = _mint(address(this), address(this), 5 ether); - vm.warp(365 days + 1); - lendgine.accrueInterest(); - - uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); - uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year - - uint256 reserve0 = lendgine.reserve0(); - uint256 reserve1 = lendgine.reserve1(); - - uint256 _amount0 = FullMath.mulDivRoundingUp( - reserve0, - lendgine.convertShareToLiquidity(0.5 ether), - lendgine.totalLiquidity() - ); - uint256 _amount1 = FullMath.mulDivRoundingUp( - reserve1, - lendgine.convertShareToLiquidity(0.5 ether), - lendgine.totalLiquidity() - ); - - _burn(address(this), address(this), 0.5 ether, _amount0, _amount1); - - (, , uint256 liquidity) = _withdraw(cuh, cuh, 1 ether); - - // check liquidity - assertEq(liquidity, 1 ether - lpDilution); - - // check lendgine storage slots - assertEq(lendgine.totalLiquidity(), 0); - assertEq(lendgine.totalPositionSize(), 0); - assertEq(lendgine.totalLiquidityBorrowed(), 0); - assertEq(0, lendgine.reserve0()); - assertEq(0, lendgine.reserve1()); - } - - function testNonStandardDecimals() external { - token1Scale = 9; - - lendgine = Lendgine( - factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound) - ); - - token0.mint(address(this), 1e18); - token1.mint(address(this), 8 * 1e9); - - lendgine.deposit( - address(this), - 1 ether, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: 1e18, - amount1: 8 * 1e9, - payer: address(this) - }) - ) - ); - - (uint256 amount0, uint256 amount1, uint256 liquidity) = lendgine.withdraw(address(this), 0.5 ether); - - assertEq(liquidity, 0.5 ether); - assertEq(0.5 ether, amount0); - assertEq(4 * 1e9, amount1); - - assertEq(0.5 ether, lendgine.totalLiquidity()); - assertEq(0.5 ether, lendgine.totalPositionSize()); - - assertEq(0.5 ether, uint256(lendgine.reserve0())); - assertEq(4 * 1e9, uint256(lendgine.reserve1())); - assertEq(0.5 ether, token0.balanceOf(address(lendgine))); - assertEq(4 * 1e9, token1.balanceOf(address(lendgine))); - - assertEq(0.5 ether, token0.balanceOf(address(this))); - assertEq(4 * 1e9, token1.balanceOf(address(this))); - - (uint256 positionSize, , ) = lendgine.positions(address(this)); - assertEq(0.5 ether, positionSize); - } + assertEq(liquidity, 1 ether); + assertEq(1 ether, amount0); + assertEq(8 ether, amount1); + + assertEq(0, lendgine.totalLiquidity()); + assertEq(0, lendgine.totalPositionSize()); + + assertEq(0, uint256(lendgine.reserve0())); + assertEq(0, uint256(lendgine.reserve1())); + assertEq(0, token0.balanceOf(address(lendgine))); + assertEq(0, token1.balanceOf(address(lendgine))); + + assertEq(1 ether, token0.balanceOf(address(cuh))); + assertEq(8 ether, token1.balanceOf(address(cuh))); + + (uint256 positionSize,,) = lendgine.positions(cuh); + assertEq(0, positionSize); + } + + function testEmitLendgine() external { + vm.expectEmit(true, true, false, true, address(lendgine)); + emit Withdraw(cuh, 1 ether, 1 ether, cuh); + _withdraw(cuh, cuh, 1 ether); + } + + function testEmitPair() external { + vm.expectEmit(true, false, false, true, address(lendgine)); + emit Burn(1 ether, 8 ether, 1 ether, cuh); + _withdraw(cuh, cuh, 1 ether); + } + + function testZeroWithdraw() external { + vm.expectRevert(Lendgine.InputError.selector); + _withdraw(cuh, cuh, 0); + } + + function testOverWithdraw() external { + vm.expectRevert(Lendgine.InsufficientPositionError.selector); + _withdraw(cuh, cuh, 2 ether); + } + + function testMaxUtilizedWithdraw() external { + _mint(address(this), address(this), 5 ether); + _withdraw(cuh, cuh, 0.5 ether); + } + + function testCompleteUtilization() external { + _mint(address(this), address(this), 5 ether); + + vm.expectRevert(Lendgine.CompleteUtilizationError.selector); + _withdraw(cuh, cuh, 0.5 ether + 1); + } + + function testAccrueOnWithdraw() external { + _mint(address(this), address(this), 1 ether); + vm.warp(365 days + 1); + _withdraw(cuh, cuh, 0.5 ether); + + assertEq(365 days + 1, lendgine.lastUpdate()); + assert(lendgine.rewardPerPositionStored() != 0); + } + + function testAccrueOnPositionWithdraw() external { + _mint(address(this), address(this), 1 ether); + vm.warp(365 days + 1); + _withdraw(cuh, cuh, 0.5 ether); + + (, uint256 rewardPerPositionPaid, uint256 tokensOwed) = lendgine.positions(cuh); + assert(rewardPerPositionPaid != 0); + assert(tokensOwed != 0); + } + + function testProportionalPositionSize() external { + uint256 shares = _mint(address(this), address(this), 5 ether); + vm.warp(365 days + 1); + lendgine.accrueInterest(); + + uint256 borrowRate = lendgine.getBorrowRate(0.5 ether, 1 ether); + uint256 lpDilution = borrowRate / 2; // 0.5 lp for one year + + uint256 reserve0 = lendgine.reserve0(); + uint256 reserve1 = lendgine.reserve1(); + + uint256 _amount0 = + FullMath.mulDivRoundingUp(reserve0, lendgine.convertShareToLiquidity(0.5 ether), lendgine.totalLiquidity()); + uint256 _amount1 = + FullMath.mulDivRoundingUp(reserve1, lendgine.convertShareToLiquidity(0.5 ether), lendgine.totalLiquidity()); + + _burn(address(this), address(this), 0.5 ether, _amount0, _amount1); + + (,, uint256 liquidity) = _withdraw(cuh, cuh, 1 ether); + + // check liquidity + assertEq(liquidity, 1 ether - lpDilution); + + // check lendgine storage slots + assertEq(lendgine.totalLiquidity(), 0); + assertEq(lendgine.totalPositionSize(), 0); + assertEq(lendgine.totalLiquidityBorrowed(), 0); + assertEq(0, lendgine.reserve0()); + assertEq(0, lendgine.reserve1()); + } + + function testNonStandardDecimals() external { + token1Scale = 9; + + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); + + token0.mint(address(this), 1e18); + token1.mint(address(this), 8 * 1e9); + + lendgine.deposit( + address(this), + 1 ether, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: 1e18, + amount1: 8 * 1e9, + payer: address(this) + }) + ) + ); + + (uint256 amount0, uint256 amount1, uint256 liquidity) = lendgine.withdraw(address(this), 0.5 ether); + + assertEq(liquidity, 0.5 ether); + assertEq(0.5 ether, amount0); + assertEq(4 * 1e9, amount1); + + assertEq(0.5 ether, lendgine.totalLiquidity()); + assertEq(0.5 ether, lendgine.totalPositionSize()); + + assertEq(0.5 ether, uint256(lendgine.reserve0())); + assertEq(4 * 1e9, uint256(lendgine.reserve1())); + assertEq(0.5 ether, token0.balanceOf(address(lendgine))); + assertEq(4 * 1e9, token1.balanceOf(address(lendgine))); + + assertEq(0.5 ether, token0.balanceOf(address(this))); + assertEq(4 * 1e9, token1.balanceOf(address(this))); + + (uint256 positionSize,,) = lendgine.positions(address(this)); + assertEq(0.5 ether, positionSize); + } } diff --git a/test/utils/CallbackHelper.sol b/test/utils/CallbackHelper.sol index 7d12200..30bd9b5 100644 --- a/test/utils/CallbackHelper.sol +++ b/test/utils/CallbackHelper.sol @@ -8,72 +8,66 @@ import { ISwapCallback } from "../../src/core/interfaces/callback/ISwapCallback. import { SafeTransferLib } from "../../src/libraries/SafeTransferLib.sol"; contract CallbackHelper is IPairMintCallback, IMintCallback, ISwapCallback { - struct PairMintCallbackData { - address token0; - address token1; - uint256 amount0; - uint256 amount1; - address payer; - } + struct PairMintCallbackData { + address token0; + address token1; + uint256 amount0; + uint256 amount1; + address payer; + } - function pairMintCallback(uint256, bytes calldata data) external override { - PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData)); + function pairMintCallback(uint256, bytes calldata data) external override { + PairMintCallbackData memory decoded = abi.decode(data, (PairMintCallbackData)); - if (decoded.payer == address(this)) { - if (decoded.amount0 > 0) SafeTransferLib.safeTransfer(decoded.token0, msg.sender, decoded.amount0); - if (decoded.amount1 > 0) SafeTransferLib.safeTransfer(decoded.token1, msg.sender, decoded.amount1); - } else { - if (decoded.amount0 > 0) - SafeTransferLib.safeTransferFrom(decoded.token0, decoded.payer, msg.sender, decoded.amount0); - if (decoded.amount1 > 0) - SafeTransferLib.safeTransferFrom(decoded.token1, decoded.payer, msg.sender, decoded.amount1); - } + if (decoded.payer == address(this)) { + if (decoded.amount0 > 0) SafeTransferLib.safeTransfer(decoded.token0, msg.sender, decoded.amount0); + if (decoded.amount1 > 0) SafeTransferLib.safeTransfer(decoded.token1, msg.sender, decoded.amount1); + } else { + if (decoded.amount0 > 0) { + SafeTransferLib.safeTransferFrom(decoded.token0, decoded.payer, msg.sender, decoded.amount0); + } + if (decoded.amount1 > 0) { + SafeTransferLib.safeTransferFrom(decoded.token1, decoded.payer, msg.sender, decoded.amount1); + } } + } - struct MintCallbackData { - address token; - address payer; - } + struct MintCallbackData { + address token; + address payer; + } - function mintCallback( - uint256 collateral, - uint256, - uint256, - uint256, - bytes calldata data - ) external override { - MintCallbackData memory decoded = abi.decode(data, (MintCallbackData)); + function mintCallback(uint256 collateral, uint256, uint256, uint256, bytes calldata data) external override { + MintCallbackData memory decoded = abi.decode(data, (MintCallbackData)); - if (decoded.payer == address(this)) { - SafeTransferLib.safeTransfer(decoded.token, msg.sender, collateral); - } else { - SafeTransferLib.safeTransferFrom(decoded.token, decoded.payer, msg.sender, collateral); - } + if (decoded.payer == address(this)) { + SafeTransferLib.safeTransfer(decoded.token, msg.sender, collateral); + } else { + SafeTransferLib.safeTransferFrom(decoded.token, decoded.payer, msg.sender, collateral); } + } - struct SwapCallbackData { - address token0; - address token1; - uint256 amount0; - uint256 amount1; - address payer; - } + struct SwapCallbackData { + address token0; + address token1; + uint256 amount0; + uint256 amount1; + address payer; + } - function swapCallback( - uint256, - uint256, - bytes calldata data - ) external override { - SwapCallbackData memory decoded = abi.decode(data, (SwapCallbackData)); + function swapCallback(uint256, uint256, bytes calldata data) external override { + SwapCallbackData memory decoded = abi.decode(data, (SwapCallbackData)); - if (decoded.payer == address(this)) { - if (decoded.amount0 > 0) SafeTransferLib.safeTransfer(decoded.token0, msg.sender, decoded.amount0); - if (decoded.amount1 > 0) SafeTransferLib.safeTransfer(decoded.token1, msg.sender, decoded.amount1); - } else { - if (decoded.amount0 > 0) - SafeTransferLib.safeTransferFrom(decoded.token0, decoded.payer, msg.sender, decoded.amount0); - if (decoded.amount1 > 0) - SafeTransferLib.safeTransferFrom(decoded.token1, decoded.payer, msg.sender, decoded.amount1); - } + if (decoded.payer == address(this)) { + if (decoded.amount0 > 0) SafeTransferLib.safeTransfer(decoded.token0, msg.sender, decoded.amount0); + if (decoded.amount1 > 0) SafeTransferLib.safeTransfer(decoded.token1, msg.sender, decoded.amount1); + } else { + if (decoded.amount0 > 0) { + SafeTransferLib.safeTransferFrom(decoded.token0, decoded.payer, msg.sender, decoded.amount0); + } + if (decoded.amount1 > 0) { + SafeTransferLib.safeTransferFrom(decoded.token1, decoded.payer, msg.sender, decoded.amount1); + } } + } } diff --git a/test/utils/TestHelper.sol b/test/utils/TestHelper.sol index 080353d..1c2eebb 100644 --- a/test/utils/TestHelper.sol +++ b/test/utils/TestHelper.sol @@ -9,137 +9,133 @@ import { CallbackHelper } from "./CallbackHelper.sol"; import { MockERC20 } from "./mocks/MockERC20.sol"; abstract contract TestHelper is Test, CallbackHelper { - MockERC20 public token0; - MockERC20 public token1; + MockERC20 public token0; + MockERC20 public token1; - uint8 public token0Scale; - uint8 public token1Scale; + uint8 public token0Scale; + uint8 public token1Scale; - uint256 public upperBound; + uint256 public upperBound; - Factory public factory; - Lendgine public lendgine; + Factory public factory; + Lendgine public lendgine; - address public cuh; - address public dennis; + address public cuh; + address public dennis; - function mkaddr(string memory name) public returns (address) { - address addr = address(uint160(uint256(keccak256(abi.encodePacked(name))))); - vm.label(addr, name); - return addr; - } + function mkaddr(string memory name) public returns (address) { + address addr = address(uint160(uint256(keccak256(abi.encodePacked(name))))); + vm.label(addr, name); + return addr; + } - constructor() { - cuh = mkaddr("cuh"); - dennis = mkaddr("dennis"); + constructor() { + cuh = mkaddr("cuh"); + dennis = mkaddr("dennis"); - token0Scale = 18; - token1Scale = 18; + token0Scale = 18; + token1Scale = 18; - upperBound = 5 * 1e18; - } + upperBound = 5 * 1e18; + } - function _setUp() internal { - token0 = new MockERC20(); - token1 = new MockERC20(); + function _setUp() internal { + token0 = new MockERC20(); + token1 = new MockERC20(); - factory = new Factory(); - lendgine = Lendgine( - factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound) - ); - } + factory = new Factory(); + lendgine = Lendgine(factory.createLendgine(address(token0), address(token1), token0Scale, token1Scale, upperBound)); + } - function _mint( - address from, - address to, - uint256 collateral - ) internal returns (uint256 shares) { - token1.mint(from, collateral); + function _mint(address from, address to, uint256 collateral) internal returns (uint256 shares) { + token1.mint(from, collateral); - if (from != address(this)) { - vm.prank(from); - token1.approve(address(this), collateral); - } - - shares = lendgine.mint(to, collateral, abi.encode(MintCallbackData({ token: address(token1), payer: from }))); + if (from != address(this)) { + vm.prank(from); + token1.approve(address(this), collateral); } - function _burn( - address to, - address from, - uint256 shares, - uint256 amount0, - uint256 amount1 - ) internal returns (uint256 collateral) { - if (from != address(this)) { - vm.startPrank(from); - lendgine.transfer(address(lendgine), shares); - token0.approve(address(this), amount0); - token1.approve(address(this), amount1); - vm.stopPrank(); - } else { - lendgine.transfer(address(lendgine), shares); - } - - collateral = lendgine.burn( - to, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: amount0, - amount1: amount1, - payer: from - }) - ) - ); + shares = lendgine.mint(to, collateral, abi.encode(MintCallbackData({token: address(token1), payer: from}))); + } + + function _burn( + address to, + address from, + uint256 shares, + uint256 amount0, + uint256 amount1 + ) + internal + returns (uint256 collateral) + { + if (from != address(this)) { + vm.startPrank(from); + lendgine.transfer(address(lendgine), shares); + token0.approve(address(this), amount0); + token1.approve(address(this), amount1); + vm.stopPrank(); + } else { + lendgine.transfer(address(lendgine), shares); } - function _deposit( - address to, - address from, - uint256 amount0, - uint256 amount1, - uint256 liquidity - ) internal returns (uint256 size) { - token0.mint(from, amount0); - token1.mint(from, amount1); - - if (from != address(this)) { - vm.startPrank(from); - token0.approve(address(this), amount0); - token1.approve(address(this), amount1); - vm.stopPrank(); - } - - size = lendgine.deposit( - to, - liquidity, - abi.encode( - PairMintCallbackData({ - token0: address(token0), - token1: address(token1), - amount0: amount0, - amount1: amount1, - payer: from - }) - ) - ); + collateral = lendgine.burn( + to, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: amount0, + amount1: amount1, + payer: from + }) + ) + ); + } + + function _deposit( + address to, + address from, + uint256 amount0, + uint256 amount1, + uint256 liquidity + ) + internal + returns (uint256 size) + { + token0.mint(from, amount0); + token1.mint(from, amount1); + + if (from != address(this)) { + vm.startPrank(from); + token0.approve(address(this), amount0); + token1.approve(address(this), amount1); + vm.stopPrank(); } - function _withdraw( - address from, - address to, - uint256 size - ) - internal - returns ( - uint256 amount0, - uint256 amount1, - uint256 liquidity - ) - { - vm.prank(from); - (amount0, amount1, liquidity) = lendgine.withdraw(to, size); - } + size = lendgine.deposit( + to, + liquidity, + abi.encode( + PairMintCallbackData({ + token0: address(token0), + token1: address(token1), + amount0: amount0, + amount1: amount1, + payer: from + }) + ) + ); + } + + function _withdraw( + address from, + address to, + uint256 size + ) + internal + returns (uint256 amount0, uint256 amount1, uint256 liquidity) + { + vm.prank(from); + (amount0, amount1, liquidity) = lendgine.withdraw(to, size); + } } diff --git a/test/utils/mocks/MockERC20.sol b/test/utils/mocks/MockERC20.sol index 08e0764..4c4178b 100644 --- a/test/utils/mocks/MockERC20.sol +++ b/test/utils/mocks/MockERC20.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.0; import { ERC20 } from "../../../src/core/ERC20.sol"; contract MockERC20 is ERC20 { - constructor() ERC20() {} + constructor() ERC20() { } - function mint(address to, uint256 value) public virtual { - _mint(to, value); - } + function mint(address to, uint256 value) public virtual { + _mint(to, value); + } - function burn(address from, uint256 value) public virtual { - _burn(from, value); - } + function burn(address from, uint256 value) public virtual { + _burn(from, value); + } }