├── remappings.txt ├── foundry.toml ├── .gitignore ├── .gitmodules ├── .github └── workflows │ └── test.yml ├── test ├── VotingEscrowImplementation.sol └── VotingEscrow.sol ├── README.md └── src └── VotingEscrow.sol /remappings.txt: -------------------------------------------------------------------------------- 1 | @uniswap/v4-core/=lib/v4-core/ 2 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 3 | @uniswap/v4-periphery/=lib/v4-periphery/ -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/v4-core"] 5 | path = lib/v4-core 6 | url = https://github.com/Uniswap/v4-core 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | [submodule "lib/v4-periphery"] 11 | path = lib/v4-periphery 12 | url = https://github.com/Uniswap/v4-periphery 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /test/VotingEscrowImplementation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseHook} from "@uniswap/v4-periphery/contracts/BaseHook.sol"; 5 | import {VotingEscrow} from "../src/VotingEscrow.sol"; 6 | import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; 7 | import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; 8 | 9 | contract VotingEscrowImplementation is VotingEscrow { 10 | constructor( 11 | IPoolManager _poolManager, 12 | address _token, 13 | string memory _name, 14 | string memory _symbol, 15 | VotingEscrow addressToEtch 16 | ) VotingEscrow(_poolManager, _token, _name, _symbol) { 17 | Hooks.validateHookAddress(addressToEtch, getHooksCalls()); 18 | } 19 | 20 | // make this a no-op in testing 21 | function validateHookAddress(BaseHook _this) internal pure override {} 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Foundry 2 | 3 | **Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** 4 | 5 | Foundry consists of: 6 | 7 | - **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). 8 | - **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. 9 | - **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. 10 | - **Chisel**: Fast, utilitarian, and verbose solidity REPL. 11 | 12 | ## Documentation 13 | 14 | https://book.getfoundry.sh/ 15 | 16 | ## Usage 17 | 18 | ### Build 19 | 20 | ```shell 21 | $ forge build 22 | ``` 23 | 24 | ### Test 25 | 26 | ```shell 27 | $ forge test 28 | ``` 29 | 30 | ### Format 31 | 32 | ```shell 33 | $ forge fmt 34 | ``` 35 | 36 | ### Gas Snapshots 37 | 38 | ```shell 39 | $ forge snapshot 40 | ``` 41 | 42 | ### Anvil 43 | 44 | ```shell 45 | $ anvil 46 | ``` 47 | 48 | ### Deploy 49 | 50 | ```shell 51 | $ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key 52 | ``` 53 | 54 | ### Cast 55 | 56 | ```shell 57 | $ cast 58 | ``` 59 | 60 | ### Help 61 | 62 | ```shell 63 | $ forge --help 64 | $ anvil --help 65 | $ cast --help 66 | ``` 67 | -------------------------------------------------------------------------------- /test/VotingEscrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol"; 6 | import {TestERC20} from "@uniswap/v4-core/contracts/test/TestERC20.sol"; 7 | import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; 8 | import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; 9 | import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; 10 | import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; 11 | import {VotingEscrowImplementation} from "./VotingEscrowImplementation.sol"; 12 | import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; 13 | import {PoolModifyPositionTest} from "@uniswap/v4-core/contracts/test/PoolModifyPositionTest.sol"; 14 | import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; 15 | import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; 16 | 17 | contract VotingEscrowTest is Test, Deployers { 18 | using PoolIdLibrary for PoolKey; 19 | 20 | int24 constant MAX_TICK_SPACING = 32767; 21 | uint160 constant SQRT_RATIO_2_1 = 112045541949572279837463876454; 22 | 23 | TestERC20Decimals token0; 24 | TestERC20Decimals token1; 25 | PoolManager manager; 26 | VotingEscrowImplementation votingEscrow = VotingEscrowImplementation( 27 | address( 28 | uint160(Hooks.BEFORE_MODIFY_POSITION_FLAG | Hooks.AFTER_MODIFY_POSITION_FLAG | Hooks.AFTER_INITIALIZE_FLAG) 29 | ) 30 | ); 31 | PoolKey key; 32 | PoolId id; 33 | 34 | PoolModifyPositionTest modifyPositionRouter; 35 | 36 | function setUp() public { 37 | token0 = new TestERC20Decimals(2**128); 38 | token1 = new TestERC20Decimals(2**128); 39 | 40 | if (uint256(uint160(address(token0))) > uint256(uint160(address(token1)))) { 41 | TestERC20Decimals token0_ = token1; 42 | token1 = token0; 43 | token0 = token0_; 44 | } 45 | 46 | manager = new PoolManager(500000); 47 | 48 | vm.record(); 49 | VotingEscrowImplementation impl = 50 | new VotingEscrowImplementation(manager, address(token0), "veToken", "veTKN", votingEscrow); 51 | (, bytes32[] memory writes) = vm.accesses(address(impl)); 52 | vm.etch(address(votingEscrow), address(impl).code); 53 | // for each storage key that was written during the hook implementation, copy the value over 54 | unchecked { 55 | for (uint256 i = 0; i < writes.length; i++) { 56 | bytes32 slot = writes[i]; 57 | vm.store(address(votingEscrow), slot, vm.load(address(impl), slot)); 58 | } 59 | } 60 | key = PoolKey( 61 | Currency.wrap(address(token0)), Currency.wrap(address(token1)), 0, MAX_TICK_SPACING, votingEscrow 62 | ); 63 | id = key.toId(); 64 | 65 | modifyPositionRouter = new PoolModifyPositionTest(manager); 66 | 67 | token0.approve(address(votingEscrow), type(uint256).max); 68 | token1.approve(address(votingEscrow), type(uint256).max); 69 | token0.approve(address(modifyPositionRouter), type(uint256).max); 70 | token1.approve(address(modifyPositionRouter), type(uint256).max); 71 | } 72 | 73 | function testBeforeInitializeAllowsPoolCreation() public { 74 | manager.initialize(key, SQRT_RATIO_1_1); 75 | } 76 | 77 | function testAfterInitializeState() public { 78 | manager.initialize(key, SQRT_RATIO_2_1); 79 | assertEq(PoolId.unwrap(votingEscrow.poolId()), PoolId.unwrap(id)); 80 | } 81 | 82 | function testModifyPosition() public { 83 | manager.initialize(key, SQRT_RATIO_1_1); 84 | 85 | modifyPositionRouter.modifyPosition( 86 | key, 87 | IPoolManager.ModifyPositionParams( 88 | TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000 89 | ) 90 | ); 91 | } 92 | } 93 | 94 | contract TestERC20Decimals is TestERC20 { 95 | constructor(uint256 amountToMint) TestERC20(amountToMint) {} 96 | 97 | function decimals() public pure returns (uint8) { 98 | return 18; 99 | } 100 | } -------------------------------------------------------------------------------- /src/VotingEscrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | pragma solidity ^0.8.19; 3 | 4 | import {BaseHook} from "@uniswap/v4-periphery/contracts/BaseHook.sol"; 5 | import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; 6 | import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; 7 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 9 | import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; 10 | import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; 11 | import {Position} from "@uniswap/v4-core/contracts/libraries/Position.sol"; 12 | import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; 13 | 14 | /// @title VotingEscrow 15 | /// @author Curve Finance (MIT) - original concept and implementation in Vyper 16 | /// (see https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy) 17 | /// mStable (AGPL) - forking Curve's Vyper contract and porting to Solidity 18 | /// (see https://github.com/mstable/mStable-contracts/blob/master/contracts/governance/IncentivisedVotingLockup.sol) 19 | /// FIAT DAO (AGPL) - https://github.com/code-423n4/2022-08-fiatdao/blob/main/contracts/VotingEscrow.sol 20 | /// VotingEscrow (AGPL) - This version, forked and applied to Uniswap v4 hook by https://github.com/kadenzipfel 21 | /// @notice Curve VotingEscrow mechanics applied to Uniswap v4 hook 22 | contract VotingEscrow is BaseHook, ReentrancyGuard { 23 | using PoolIdLibrary for PoolKey; 24 | 25 | // Shared Events 26 | event Deposit(address indexed provider, uint256 value, uint256 locktime, LockAction indexed action, uint256 ts); 27 | event Withdraw(address indexed provider, uint256 value, LockAction indexed action, uint256 ts); 28 | 29 | // Pool ID 30 | PoolId public poolId; 31 | 32 | // Shared global state 33 | ERC20 public token; 34 | bool initialized; 35 | uint256 public constant WEEK = 7 days; 36 | uint256 public constant MAXTIME = 365 days; 37 | uint256 public constant MULTIPLIER = 10 ** 18; 38 | 39 | // Lock state 40 | uint256 public globalEpoch; 41 | Point[1000000000000000000] public pointHistory; // 1e9 * userPointHistory-length, so sufficient for 1e9 users 42 | mapping(address => Point[1000000000]) public userPointHistory; 43 | mapping(address => uint256) public userPointEpoch; 44 | mapping(uint256 => int128) public slopeChanges; 45 | mapping(address => LockedBalance) public locked; 46 | mapping(address => LockTicks) public lockTicks; 47 | 48 | // Voting token 49 | string public name; 50 | string public symbol; 51 | uint256 public decimals = 18; 52 | 53 | // Structs 54 | struct Point { 55 | int128 bias; 56 | int128 slope; 57 | uint256 ts; 58 | uint256 blk; 59 | } 60 | 61 | struct LockedBalance { 62 | uint128 amount; 63 | uint128 end; 64 | } 65 | 66 | struct LockTicks { 67 | int24 lowerTick; 68 | int24 upperTick; 69 | } 70 | 71 | // Miscellaneous 72 | enum LockAction { 73 | CREATE, 74 | INCREASE_TIME 75 | } 76 | 77 | /// @notice Constructor 78 | /// @param _poolManager Uniswap v4 PoolManager contract 79 | /// @param _name Name of non-transferrable ve token 80 | /// @param _symbol Symbol of non-transferrable ve token 81 | constructor(IPoolManager _poolManager, address _token, string memory _name, string memory _symbol) 82 | BaseHook(_poolManager) 83 | { 84 | token = ERC20(_token); 85 | pointHistory[0] = Point({bias: int128(0), slope: int128(0), ts: block.timestamp, blk: block.number}); 86 | 87 | decimals = ERC20(_token).decimals(); 88 | require(decimals <= 18, "Exceeds max decimals"); 89 | 90 | name = _name; 91 | symbol = _symbol; 92 | } 93 | 94 | function getHooksCalls() public pure override returns (Hooks.Calls memory) { 95 | return Hooks.Calls({ 96 | beforeInitialize: false, 97 | afterInitialize: true, 98 | beforeModifyPosition: true, 99 | afterModifyPosition: true, 100 | beforeSwap: false, 101 | afterSwap: false, 102 | beforeDonate: false, 103 | afterDonate: false 104 | }); 105 | } 106 | 107 | /// @notice Hook run after initializing pool 108 | /// @param key Pool key 109 | function afterInitialize(address, PoolKey calldata key, uint160, int24) 110 | external 111 | override 112 | poolManagerOnly 113 | returns (bytes4) 114 | { 115 | poolId = key.toId(); 116 | return VotingEscrow.afterInitialize.selector; 117 | } 118 | 119 | /// @notice Hook run before modifying user liquidity position 120 | /// @param sender msg.sender of PoolManager.modifyPosition call 121 | /// @param modifyPositionParams Params passed to PoolManager.modifyPosition call 122 | function beforeModifyPosition( 123 | address sender, 124 | PoolKey calldata, 125 | IPoolManager.ModifyPositionParams calldata modifyPositionParams 126 | ) external view override poolManagerOnly returns (bytes4) { 127 | LockTicks memory lockTicks_ = lockTicks[sender]; 128 | if ( 129 | lockTicks_.lowerTick != modifyPositionParams.tickLower 130 | || lockTicks_.upperTick != modifyPositionParams.tickUpper 131 | ) { 132 | // Not modifying locked position, continue execution 133 | return VotingEscrow.beforeModifyPosition.selector; 134 | } 135 | 136 | LockedBalance memory locked_ = locked[sender]; 137 | // Can only increase position liquidity while locked 138 | require( 139 | modifyPositionParams.liquidityDelta > 0 || locked_.end <= block.timestamp, "Can't withdraw before lock end" 140 | ); 141 | return VotingEscrow.beforeModifyPosition.selector; 142 | } 143 | 144 | /// @notice Hook run after modifying user liquidity position 145 | /// @param sender msg.sender of PoolManager.modifyPosition call 146 | /// @param modifyPositionParams Params passed to PoolManager.modifyPosition call 147 | function afterModifyPosition( 148 | address sender, 149 | PoolKey calldata, 150 | IPoolManager.ModifyPositionParams calldata modifyPositionParams, 151 | BalanceDelta 152 | ) external override poolManagerOnly returns (bytes4) { 153 | LockTicks memory lockTicks_ = lockTicks[sender]; 154 | if ( 155 | lockTicks_.lowerTick != modifyPositionParams.tickLower 156 | || lockTicks_.upperTick != modifyPositionParams.tickUpper 157 | ) { 158 | // Not modifying locked position, continue execution 159 | return VotingEscrow.afterModifyPosition.selector; 160 | } 161 | 162 | Position.Info memory position = 163 | poolManager.getPosition(poolId, sender, modifyPositionParams.tickLower, modifyPositionParams.tickUpper); 164 | LockedBalance memory locked_ = locked[sender]; 165 | 166 | if (locked_.end > block.timestamp) { 167 | // Lock still active, enforce invariant 168 | assert(locked_.amount < position.liquidity); 169 | } 170 | 171 | LockedBalance memory newLocked = _copyLock(locked_); 172 | newLocked.amount = position.liquidity; 173 | locked[sender] = newLocked; 174 | 175 | _checkpoint(sender, locked_, newLocked); 176 | 177 | return VotingEscrow.afterModifyPosition.selector; 178 | } 179 | 180 | /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// 181 | /// LOCK MANAGEMENT /// 182 | /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// 183 | 184 | /// @notice Returns a user's lock expiration 185 | /// @param _addr The address of the user 186 | /// @return Expiration of the user's lock 187 | function lockEnd(address _addr) external view returns (uint256) { 188 | return locked[_addr].end; 189 | } 190 | 191 | /// @notice Returns the last available user point for a user 192 | /// @param _addr User address 193 | /// @return bias i.e. y 194 | /// @return slope i.e. linear gradient 195 | /// @return ts i.e. time point was logged 196 | function getLastUserPoint(address _addr) external view returns (int128 bias, int128 slope, uint256 ts) { 197 | uint256 uepoch = userPointEpoch[_addr]; 198 | if (uepoch == 0) { 199 | return (0, 0, 0); 200 | } 201 | Point memory point = userPointHistory[_addr][uepoch]; 202 | return (point.bias, point.slope, point.ts); 203 | } 204 | 205 | /// @notice Records a checkpoint of both individual and global slope 206 | /// @param _addr User address, or address(0) for only global 207 | /// @param _oldLocked Old amount that user had locked, or null for global 208 | /// @param _newLocked new amount that user has locked, or null for global 209 | function _checkpoint(address _addr, LockedBalance memory _oldLocked, LockedBalance memory _newLocked) internal { 210 | Point memory userOldPoint; 211 | Point memory userNewPoint; 212 | int128 oldSlopeDelta = 0; 213 | int128 newSlopeDelta = 0; 214 | uint256 epoch = globalEpoch; 215 | 216 | if (_addr != address(0)) { 217 | // Calculate slopes and biases 218 | // Kept at zero when they have to 219 | if (_oldLocked.end > block.timestamp) { 220 | userOldPoint.slope = int128(_oldLocked.amount) / int128(int256(MAXTIME)); 221 | userOldPoint.bias = userOldPoint.slope * int128(int256(_oldLocked.end - block.timestamp)); 222 | } 223 | if (_newLocked.end > block.timestamp) { 224 | userNewPoint.slope = int128(_newLocked.amount) / int128(int256(MAXTIME)); 225 | userNewPoint.bias = userNewPoint.slope * int128(int256(_newLocked.end - block.timestamp)); 226 | } 227 | 228 | // Moved from bottom final if statement to resolve stack too deep err 229 | // start { 230 | // Now handle user history 231 | uint256 uEpoch = userPointEpoch[_addr]; 232 | if (uEpoch == 0) { 233 | userPointHistory[_addr][uEpoch + 1] = userOldPoint; 234 | } 235 | 236 | userPointEpoch[_addr] = uEpoch + 1; 237 | userNewPoint.ts = block.timestamp; 238 | userNewPoint.blk = block.number; 239 | userPointHistory[_addr][uEpoch + 1] = userNewPoint; 240 | 241 | // } end 242 | 243 | // Read values of scheduled changes in the slope 244 | // oldLocked.end can be in the past and in the future 245 | // newLocked.end can ONLY by in the FUTURE unless everything expired: than zeros 246 | oldSlopeDelta = slopeChanges[_oldLocked.end]; 247 | if (_newLocked.end != 0) { 248 | if (_newLocked.end == _oldLocked.end) { 249 | newSlopeDelta = oldSlopeDelta; 250 | } else { 251 | newSlopeDelta = slopeChanges[_newLocked.end]; 252 | } 253 | } 254 | } 255 | 256 | Point memory lastPoint = Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number}); 257 | if (epoch > 0) { 258 | lastPoint = pointHistory[epoch]; 259 | } 260 | uint256 lastCheckpoint = lastPoint.ts; 261 | 262 | // initialLastPoint is used for extrapolation to calculate block number 263 | // (approximately, for *At methods) and save them 264 | // as we cannot figure that out exactly from inside the contract 265 | Point memory initialLastPoint = Point({bias: 0, slope: 0, ts: lastPoint.ts, blk: lastPoint.blk}); 266 | uint256 blockSlope = 0; // dblock/dt 267 | if (block.timestamp > lastPoint.ts) { 268 | blockSlope = (MULTIPLIER * (block.number - lastPoint.blk)) / (block.timestamp - lastPoint.ts); 269 | } 270 | // If last point is already recorded in this block, slope=0 271 | // But that's ok b/c we know the block in such case 272 | 273 | // Go over weeks to fill history and calculate what the current point is 274 | uint256 iterativeTime = _floorToWeek(lastCheckpoint); 275 | for (uint256 i = 0; i < 255; i++) { 276 | // Hopefully it won't happen that this won't get used in 5 years! 277 | // If it does, users will be able to withdraw but vote weight will be broken 278 | iterativeTime = iterativeTime + WEEK; 279 | int128 dSlope = 0; 280 | if (iterativeTime > block.timestamp) { 281 | iterativeTime = block.timestamp; 282 | } else { 283 | dSlope = slopeChanges[iterativeTime]; 284 | } 285 | int128 biasDelta = lastPoint.slope * int128(int256((iterativeTime - lastCheckpoint))); 286 | lastPoint.bias = lastPoint.bias - biasDelta; 287 | lastPoint.slope = lastPoint.slope + dSlope; 288 | // This can happen 289 | if (lastPoint.bias < 0) { 290 | lastPoint.bias = 0; 291 | } 292 | // This cannot happen - just in case 293 | if (lastPoint.slope < 0) { 294 | lastPoint.slope = 0; 295 | } 296 | lastCheckpoint = iterativeTime; 297 | lastPoint.ts = iterativeTime; 298 | lastPoint.blk = initialLastPoint.blk + (blockSlope * (iterativeTime - initialLastPoint.ts)) / MULTIPLIER; 299 | 300 | // when epoch is incremented, we either push here or after slopes updated below 301 | epoch = epoch + 1; 302 | if (iterativeTime == block.timestamp) { 303 | lastPoint.blk = block.number; 304 | break; 305 | } else { 306 | pointHistory[epoch] = lastPoint; 307 | } 308 | } 309 | 310 | globalEpoch = epoch; 311 | // Now pointHistory is filled until t=now 312 | 313 | if (_addr != address(0)) { 314 | // If last point was in this block, the slope change has been applied already 315 | // But in such case we have 0 slope(s) 316 | lastPoint.slope = lastPoint.slope + userNewPoint.slope - userOldPoint.slope; 317 | lastPoint.bias = lastPoint.bias + userNewPoint.bias - userOldPoint.bias; 318 | if (lastPoint.slope < 0) { 319 | lastPoint.slope = 0; 320 | } 321 | if (lastPoint.bias < 0) { 322 | lastPoint.bias = 0; 323 | } 324 | } 325 | 326 | // Record the changed point into history 327 | pointHistory[epoch] = lastPoint; 328 | 329 | if (_addr != address(0)) { 330 | // Schedule the slope changes (slope is going down) 331 | // We subtract new_user_slope from [new_locked.end] 332 | // and add old_user_slope to [old_locked.end] 333 | if (_oldLocked.end > block.timestamp) { 334 | // oldSlopeDelta was - userOldPoint.slope, so we cancel that 335 | oldSlopeDelta = oldSlopeDelta + userOldPoint.slope; 336 | if (_newLocked.end == _oldLocked.end) { 337 | oldSlopeDelta = oldSlopeDelta - userNewPoint.slope; // It was a new deposit, not extension 338 | } 339 | slopeChanges[_oldLocked.end] = oldSlopeDelta; 340 | } 341 | if (_newLocked.end > block.timestamp) { 342 | if (_newLocked.end > _oldLocked.end) { 343 | newSlopeDelta = newSlopeDelta - userNewPoint.slope; // old slope disappeared at this point 344 | slopeChanges[_newLocked.end] = newSlopeDelta; 345 | } 346 | // else: we recorded it already in oldSlopeDelta 347 | } 348 | } 349 | } 350 | 351 | /// @notice Public function to trigger global checkpoint 352 | function checkpoint() external { 353 | LockedBalance memory empty; 354 | _checkpoint(address(0), empty, empty); 355 | } 356 | 357 | /// @notice Creates a new lock 358 | /// @param _unlockTime Time at which the lock expires 359 | /// @param _tickLower Lower tick for liquidity position 360 | /// @param _tickUpper Upper tick for liquidity position 361 | function createLock(uint256 _unlockTime, int24 _tickLower, int24 _tickUpper) external nonReentrant { 362 | uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks 363 | LockedBalance memory locked_ = locked[msg.sender]; 364 | Position.Info memory position = poolManager.getPosition(poolId, msg.sender, _tickLower, _tickUpper); 365 | uint256 value = uint256(position.liquidity); 366 | 367 | // Validate inputs 368 | require(value > 0, "No liquidity position"); 369 | require(locked_.amount == 0, "Lock exists"); 370 | require(unlock_time >= locked_.end, "Only increase lock end"); 371 | require(unlock_time > block.timestamp, "Only future lock end"); 372 | require(unlock_time <= block.timestamp + MAXTIME, "Exceeds maxtime"); 373 | // Update lock and voting power (checkpoint) 374 | locked_.amount = uint128(value); 375 | locked_.end = uint128(unlock_time); 376 | locked[msg.sender] = locked_; 377 | lockTicks[msg.sender] = LockTicks({lowerTick: _tickLower, upperTick: _tickUpper}); 378 | _checkpoint(msg.sender, LockedBalance(0, 0), locked_); 379 | // Deposit locked tokens 380 | emit Deposit(msg.sender, value, unlock_time, LockAction.CREATE, block.timestamp); 381 | } 382 | 383 | /// @notice Extends the expiration of an existing lock 384 | /// @param _unlockTime New lock expiration time 385 | /// @dev Does not update the amount of tokens locked. 386 | /// @dev Does increase the user's voting power. 387 | function increaseUnlockTime(uint256 _unlockTime) external nonReentrant { 388 | LockedBalance memory locked_ = locked[msg.sender]; 389 | uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks 390 | // Validate inputs 391 | require(locked_.amount > 0, "No lock"); 392 | require(unlock_time > locked_.end, "Only increase lock end"); 393 | require(unlock_time <= block.timestamp + MAXTIME, "Exceeds maxtime"); 394 | // Update lock 395 | uint256 oldUnlockTime = locked_.end; 396 | locked_.end = uint128(unlock_time); 397 | locked[msg.sender] = locked_; 398 | require(oldUnlockTime > block.timestamp, "Lock expired"); 399 | LockedBalance memory oldLocked = _copyLock(locked_); 400 | oldLocked.end = uint128(oldUnlockTime); 401 | _checkpoint(msg.sender, oldLocked, locked_); 402 | emit Deposit(msg.sender, 0, unlock_time, LockAction.INCREASE_TIME, block.timestamp); 403 | } 404 | 405 | // Creates a copy of a lock 406 | function _copyLock(LockedBalance memory _locked) internal pure returns (LockedBalance memory) { 407 | return LockedBalance({amount: _locked.amount, end: _locked.end}); 408 | } 409 | 410 | // @dev Floors a timestamp to the nearest weekly increment 411 | // @param _t Timestamp to floor 412 | function _floorToWeek(uint256 _t) internal pure returns (uint256) { 413 | return (_t / WEEK) * WEEK; 414 | } 415 | } 416 | --------------------------------------------------------------------------------