Files
33
Lines
3388
Coverage
65%
677 / 1030
Actions
66%
lib/chimera/src/CryticAsserts.sol
Lines covered: 8 / 12 (66%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | import {Asserts} from "./Asserts.sol"; |
|
| 5 | ||
| 6 | 0 | contract CryticAsserts is Asserts { |
| 7 | event Log(string); |
|
| 8 | ||
| 9 | function gt(uint256 a, uint256 b, string memory reason) internal virtual override { |
|
| 10 | if (!(a > b)) { |
|
| 11 | emit Log(reason); |
|
| 12 | assert(false); |
|
| 13 | } |
|
| 14 | } |
|
| 15 | ||
| 16 | function gte(uint256 a, uint256 b, string memory reason) internal virtual override { |
|
| 17 | if (!(a >= b)) { |
|
| 18 | emit Log(reason); |
|
| 19 | assert(false); |
|
| 20 | } |
|
| 21 | } |
|
| 22 | ||
| 23 | function lt(uint256 a, uint256 b, string memory reason) internal virtual override { |
|
| 24 | if (!(a < b)) { |
|
| 25 | emit Log(reason); |
|
| 26 | assert(false); |
|
| 27 | } |
|
| 28 | } |
|
| 29 | ||
| 30 | 0 | function lte(uint256 a, uint256 b, string memory reason) internal virtual override { |
| 31 | 0 | if (!(a <= b)) { |
| 32 | 0 | emit Log(reason); |
| 33 | assert(false); |
|
| 34 | } |
|
| 35 | } |
|
| 36 | ||
| 37 | 1× | function eq(uint256 a, uint256 b, string memory reason) internal virtual override { |
| 38 | 5× | if (!(a == b)) { |
| 39 | 17× | emit Log(reason); |
| 40 | 3× | assert(false); |
| 41 | } |
|
| 42 | } |
|
| 43 | ||
| 44 | 4× | function t(bool b, string memory reason) internal virtual override { |
| 45 | 3× | if (!b) { |
| 46 | 17× | emit Log(reason); |
| 47 | 4× | assert(false); |
| 48 | } |
|
| 49 | } |
|
| 50 | ||
| 51 | function between(uint256 value, uint256 low, uint256 high) internal virtual override returns (uint256) { |
|
| 52 | if (value < low || value > high) { |
|
| 53 | uint256 ans = low + (value % (high - low + 1)); |
|
| 54 | return ans; |
|
| 55 | } |
|
| 56 | return value; |
|
| 57 | } |
|
| 58 | ||
| 59 | function between(int256 value, int256 low, int256 high) internal virtual override returns (int256) { |
|
| 60 | if (value < low || value > high) { |
|
| 61 | int256 range = high - low + 1; |
|
| 62 | int256 clamped = (value - low) % (range); |
|
| 63 | if (clamped < 0) clamped += range; |
|
| 64 | int256 ans = low + clamped; |
|
| 65 | return ans; |
|
| 66 | } |
|
| 67 | return value; |
|
| 68 | } |
|
| 69 | ||
| 70 | function precondition(bool p) internal virtual override { |
|
| 71 | require(p); |
|
| 72 | } |
|
| 73 | } |
100%
lib/chimera/src/Hevm.sol
Lines covered: 1 / 1 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | // slither-disable-start shadowing-local |
|
| 5 | ||
| 6 | interface IHevm { |
|
| 7 | // Set block.timestamp to newTimestamp |
|
| 8 | function warp(uint256 newTimestamp) external; |
|
| 9 | ||
| 10 | // Set block.number to newNumber |
|
| 11 | function roll(uint256 newNumber) external; |
|
| 12 | ||
| 13 | // Add the condition b to the assumption base for the current branch |
|
| 14 | // This function is almost identical to require |
|
| 15 | function assume(bool b) external; |
|
| 16 | ||
| 17 | // Sets the eth balance of usr to amt |
|
| 18 | function deal(address usr, uint256 amt) external; |
|
| 19 | ||
| 20 | // Loads a storage slot from an address |
|
| 21 | function load(address where, bytes32 slot) external returns (bytes32); |
|
| 22 | ||
| 23 | // Stores a value to an address' storage slot |
|
| 24 | function store(address where, bytes32 slot, bytes32 value) external; |
|
| 25 | ||
| 26 | // Signs data (privateKey, digest) => (v, r, s) |
|
| 27 | function sign(uint256 privateKey, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); |
|
| 28 | ||
| 29 | // Gets address for a given private key |
|
| 30 | function addr(uint256 privateKey) external returns (address addr); |
|
| 31 | ||
| 32 | // Performs a foreign function call via terminal |
|
| 33 | function ffi(string[] calldata inputs) external returns (bytes memory result); |
|
| 34 | ||
| 35 | // Performs the next smart contract call with specified `msg.sender` |
|
| 36 | function prank(address newSender) external; |
|
| 37 | ||
| 38 | // Creates a new fork with the given endpoint and the latest block and returns the identifier of the fork |
|
| 39 | function createFork(string calldata urlOrAlias) external returns (uint256); |
|
| 40 | ||
| 41 | // Takes a fork identifier created by createFork and sets the corresponding forked state as active |
|
| 42 | function selectFork(uint256 forkId) external; |
|
| 43 | ||
| 44 | // Returns the identifier of the current fork |
|
| 45 | function activeFork() external returns (uint256); |
|
| 46 | ||
| 47 | // Labels the address in traces |
|
| 48 | function label(address addr, string calldata label) external; |
|
| 49 | ||
| 50 | /// Sets an address' code. |
|
| 51 | function etch(address target, bytes calldata newRuntimeBytecode) external; |
|
| 52 | } |
|
| 53 | ||
| 54 | 2× | IHevm constant vm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); |
| 55 | ||
| 56 | // slither-disable-end shadowing-local |
100%
lib/forge-std/src/StdChains.sol
Lines covered: 1 / 1 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity >=0.6.2 <0.9.0; |
|
| 3 | ||
| 4 | import {VmSafe} from "./Vm.sol"; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * StdChains provides information about EVM compatible chains that can be used in scripts/tests. |
|
| 8 | * For each chain, the chain's name, chain ID, and a default RPC URL are provided. Chains are |
|
| 9 | * identified by their alias, which is the same as the alias in the `[rpc_endpoints]` section of |
|
| 10 | * the `foundry.toml` file. For best UX, ensure the alias in the `foundry.toml` file match the |
|
| 11 | * alias used in this contract, which can be found as the first argument to the |
|
| 12 | * `setChainWithDefaultRpcUrl` call in the `initializeStdChains` function. |
|
| 13 | * |
|
| 14 | * There are two main ways to use this contract: |
|
| 15 | * 1. Set a chain with `setChain(string memory chainAlias, ChainData memory chain)` or |
|
| 16 | * `setChain(string memory chainAlias, Chain memory chain)` |
|
| 17 | * 2. Get a chain with `getChain(string memory chainAlias)` or `getChain(uint256 chainId)`. |
|
| 18 | * |
|
| 19 | * The first time either of those are used, chains are initialized with the default set of RPC URLs. |
|
| 20 | * This is done in `initializeStdChains`, which uses `setChainWithDefaultRpcUrl`. Defaults are recorded in |
|
| 21 | * `defaultRpcUrls`. |
|
| 22 | * |
|
| 23 | * The `setChain` function is straightforward, and it simply saves off the given chain data. |
|
| 24 | * |
|
| 25 | * The `getChain` methods use `getChainWithUpdatedRpcUrl` to return a chain. For example, let's say |
|
| 26 | * we want to retrieve the RPC URL for `mainnet`: |
|
| 27 | * - If you have specified data with `setChain`, it will return that. |
|
| 28 | * - If you have configured a mainnet RPC URL in `foundry.toml`, it will return the URL, provided it |
|
| 29 | * is valid (e.g. a URL is specified, or an environment variable is given and exists). |
|
| 30 | * - If neither of the above conditions is met, the default data is returned. |
|
| 31 | * |
|
| 32 | * Summarizing the above, the prioritization hierarchy is `setChain` -> `foundry.toml` -> environment variable -> defaults. |
|
| 33 | */ |
|
| 34 | abstract contract StdChains { |
|
| 35 | VmSafe private constant vm = VmSafe(address(uint160(uint256(keccak256("hevm cheat code"))))); |
|
| 36 | ||
| 37 | bool private stdChainsInitialized; |
|
| 38 | ||
| 39 | struct ChainData { |
|
| 40 | string name; |
|
| 41 | uint256 chainId; |
|
| 42 | string rpcUrl; |
|
| 43 | } |
|
| 44 | ||
| 45 | struct Chain { |
|
| 46 | // The chain name. |
|
| 47 | string name; |
|
| 48 | // The chain's Chain ID. |
|
| 49 | uint256 chainId; |
|
| 50 | // The chain's alias. (i.e. what gets specified in `foundry.toml`). |
|
| 51 | string chainAlias; |
|
| 52 | // A default RPC endpoint for this chain. |
|
| 53 | // NOTE: This default RPC URL is included for convenience to facilitate quick tests and |
|
| 54 | // experimentation. Do not use this RPC URL for production test suites, CI, or other heavy |
|
| 55 | // usage as you will be throttled and this is a disservice to others who need this endpoint. |
|
| 56 | string rpcUrl; |
|
| 57 | } |
|
| 58 | ||
| 59 | // Maps from the chain's alias (matching the alias in the `foundry.toml` file) to chain data. |
|
| 60 | mapping(string => Chain) private chains; |
|
| 61 | // Maps from the chain's alias to it's default RPC URL. |
|
| 62 | mapping(string => string) private defaultRpcUrls; |
|
| 63 | // Maps from a chain ID to it's alias. |
|
| 64 | mapping(uint256 => string) private idToAlias; |
|
| 65 | ||
| 66 | 13× | bool private fallbackToDefaultRpcUrls = true; |
| 67 | ||
| 68 | // The RPC URL will be fetched from config or defaultRpcUrls if possible. |
|
| 69 | function getChain(string memory chainAlias) internal virtual returns (Chain memory chain) { |
|
| 70 | require(bytes(chainAlias).length != 0, "StdChains getChain(string): Chain alias cannot be the empty string."); |
|
| 71 | ||
| 72 | initializeStdChains(); |
|
| 73 | chain = chains[chainAlias]; |
|
| 74 | require( |
|
| 75 | chain.chainId != 0, |
|
| 76 | string(abi.encodePacked("StdChains getChain(string): Chain with alias \"", chainAlias, "\" not found.")) |
|
| 77 | ); |
|
| 78 | ||
| 79 | chain = getChainWithUpdatedRpcUrl(chainAlias, chain); |
|
| 80 | } |
|
| 81 | ||
| 82 | function getChain(uint256 chainId) internal virtual returns (Chain memory chain) { |
|
| 83 | require(chainId != 0, "StdChains getChain(uint256): Chain ID cannot be 0."); |
|
| 84 | initializeStdChains(); |
|
| 85 | string memory chainAlias = idToAlias[chainId]; |
|
| 86 | ||
| 87 | chain = chains[chainAlias]; |
|
| 88 | ||
| 89 | require( |
|
| 90 | chain.chainId != 0, |
|
| 91 | string(abi.encodePacked("StdChains getChain(uint256): Chain with ID ", vm.toString(chainId), " not found.")) |
|
| 92 | ); |
|
| 93 | ||
| 94 | chain = getChainWithUpdatedRpcUrl(chainAlias, chain); |
|
| 95 | } |
|
| 96 | ||
| 97 | // set chain info, with priority to argument's rpcUrl field. |
|
| 98 | function setChain(string memory chainAlias, ChainData memory chain) internal virtual { |
|
| 99 | require( |
|
| 100 | bytes(chainAlias).length != 0, |
|
| 101 | "StdChains setChain(string,ChainData): Chain alias cannot be the empty string." |
|
| 102 | ); |
|
| 103 | ||
| 104 | require(chain.chainId != 0, "StdChains setChain(string,ChainData): Chain ID cannot be 0."); |
|
| 105 | ||
| 106 | initializeStdChains(); |
|
| 107 | string memory foundAlias = idToAlias[chain.chainId]; |
|
| 108 | ||
| 109 | require( |
|
| 110 | bytes(foundAlias).length == 0 || keccak256(bytes(foundAlias)) == keccak256(bytes(chainAlias)), |
|
| 111 | string( |
|
| 112 | abi.encodePacked( |
|
| 113 | "StdChains setChain(string,ChainData): Chain ID ", |
|
| 114 | vm.toString(chain.chainId), |
|
| 115 | " already used by \"", |
|
| 116 | foundAlias, |
|
| 117 | "\"." |
|
| 118 | ) |
|
| 119 | ) |
|
| 120 | ); |
|
| 121 | ||
| 122 | uint256 oldChainId = chains[chainAlias].chainId; |
|
| 123 | delete idToAlias[oldChainId]; |
|
| 124 | ||
| 125 | chains[chainAlias] = |
|
| 126 | Chain({name: chain.name, chainId: chain.chainId, chainAlias: chainAlias, rpcUrl: chain.rpcUrl}); |
|
| 127 | idToAlias[chain.chainId] = chainAlias; |
|
| 128 | } |
|
| 129 | ||
| 130 | // set chain info, with priority to argument's rpcUrl field. |
|
| 131 | function setChain(string memory chainAlias, Chain memory chain) internal virtual { |
|
| 132 | setChain(chainAlias, ChainData({name: chain.name, chainId: chain.chainId, rpcUrl: chain.rpcUrl})); |
|
| 133 | } |
|
| 134 | ||
| 135 | function _toUpper(string memory str) private pure returns (string memory) { |
|
| 136 | bytes memory strb = bytes(str); |
|
| 137 | bytes memory copy = new bytes(strb.length); |
|
| 138 | for (uint256 i = 0; i < strb.length; i++) { |
|
| 139 | bytes1 b = strb[i]; |
|
| 140 | if (b >= 0x61 && b <= 0x7A) { |
|
| 141 | copy[i] = bytes1(uint8(b) - 32); |
|
| 142 | } else { |
|
| 143 | copy[i] = b; |
|
| 144 | } |
|
| 145 | } |
|
| 146 | return string(copy); |
|
| 147 | } |
|
| 148 | ||
| 149 | // lookup rpcUrl, in descending order of priority: |
|
| 150 | // current -> config (foundry.toml) -> environment variable -> default |
|
| 151 | function getChainWithUpdatedRpcUrl(string memory chainAlias, Chain memory chain) |
|
| 152 | private |
|
| 153 | view |
|
| 154 | returns (Chain memory) |
|
| 155 | { |
|
| 156 | if (bytes(chain.rpcUrl).length == 0) { |
|
| 157 | try vm.rpcUrl(chainAlias) returns (string memory configRpcUrl) { |
|
| 158 | chain.rpcUrl = configRpcUrl; |
|
| 159 | } catch (bytes memory err) { |
|
| 160 | string memory envName = string(abi.encodePacked(_toUpper(chainAlias), "_RPC_URL")); |
|
| 161 | if (fallbackToDefaultRpcUrls) { |
|
| 162 | chain.rpcUrl = vm.envOr(envName, defaultRpcUrls[chainAlias]); |
|
| 163 | } else { |
|
| 164 | chain.rpcUrl = vm.envString(envName); |
|
| 165 | } |
|
| 166 | // Distinguish 'not found' from 'cannot read' |
|
| 167 | // The upstream error thrown by forge for failing cheats changed so we check both the old and new versions |
|
| 168 | bytes memory oldNotFoundError = |
|
| 169 | abi.encodeWithSignature("CheatCodeError", string(abi.encodePacked("invalid rpc url ", chainAlias))); |
|
| 170 | bytes memory newNotFoundError = abi.encodeWithSignature( |
|
| 171 | "CheatcodeError(string)", string(abi.encodePacked("invalid rpc url: ", chainAlias)) |
|
| 172 | ); |
|
| 173 | bytes32 errHash = keccak256(err); |
|
| 174 | if ( |
|
| 175 | (errHash != keccak256(oldNotFoundError) && errHash != keccak256(newNotFoundError)) |
|
| 176 | || bytes(chain.rpcUrl).length == 0 |
|
| 177 | ) { |
|
| 178 | /// @solidity memory-safe-assembly |
|
| 179 | assembly { |
|
| 180 | revert(add(32, err), mload(err)) |
|
| 181 | } |
|
| 182 | } |
|
| 183 | } |
|
| 184 | } |
|
| 185 | return chain; |
|
| 186 | } |
|
| 187 | ||
| 188 | function setFallbackToDefaultRpcUrls(bool useDefault) internal { |
|
| 189 | fallbackToDefaultRpcUrls = useDefault; |
|
| 190 | } |
|
| 191 | ||
| 192 | function initializeStdChains() private { |
|
| 193 | if (stdChainsInitialized) return; |
|
| 194 | ||
| 195 | stdChainsInitialized = true; |
|
| 196 | ||
| 197 | // If adding an RPC here, make sure to test the default RPC URL in `testRpcs` |
|
| 198 | setChainWithDefaultRpcUrl("anvil", ChainData("Anvil", 31337, "http://127.0.0.1:8545")); |
|
| 199 | setChainWithDefaultRpcUrl( |
|
| 200 | "mainnet", ChainData("Mainnet", 1, "https://eth-mainnet.alchemyapi.io/v2/pwc5rmJhrdoaSEfimoKEmsvOjKSmPDrP") |
|
| 201 | ); |
|
| 202 | setChainWithDefaultRpcUrl( |
|
| 203 | "sepolia", ChainData("Sepolia", 11155111, "https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001") |
|
| 204 | ); |
|
| 205 | setChainWithDefaultRpcUrl("holesky", ChainData("Holesky", 17000, "https://rpc.holesky.ethpandaops.io")); |
|
| 206 | setChainWithDefaultRpcUrl("optimism", ChainData("Optimism", 10, "https://mainnet.optimism.io")); |
|
| 207 | setChainWithDefaultRpcUrl( |
|
| 208 | "optimism_sepolia", ChainData("Optimism Sepolia", 11155420, "https://sepolia.optimism.io") |
|
| 209 | ); |
|
| 210 | setChainWithDefaultRpcUrl("arbitrum_one", ChainData("Arbitrum One", 42161, "https://arb1.arbitrum.io/rpc")); |
|
| 211 | setChainWithDefaultRpcUrl( |
|
| 212 | "arbitrum_one_sepolia", ChainData("Arbitrum One Sepolia", 421614, "https://sepolia-rollup.arbitrum.io/rpc") |
|
| 213 | ); |
|
| 214 | setChainWithDefaultRpcUrl("arbitrum_nova", ChainData("Arbitrum Nova", 42170, "https://nova.arbitrum.io/rpc")); |
|
| 215 | setChainWithDefaultRpcUrl("polygon", ChainData("Polygon", 137, "https://polygon-rpc.com")); |
|
| 216 | setChainWithDefaultRpcUrl( |
|
| 217 | "polygon_amoy", ChainData("Polygon Amoy", 80002, "https://rpc-amoy.polygon.technology") |
|
| 218 | ); |
|
| 219 | setChainWithDefaultRpcUrl("avalanche", ChainData("Avalanche", 43114, "https://api.avax.network/ext/bc/C/rpc")); |
|
| 220 | setChainWithDefaultRpcUrl( |
|
| 221 | "avalanche_fuji", ChainData("Avalanche Fuji", 43113, "https://api.avax-test.network/ext/bc/C/rpc") |
|
| 222 | ); |
|
| 223 | setChainWithDefaultRpcUrl( |
|
| 224 | "bnb_smart_chain", ChainData("BNB Smart Chain", 56, "https://bsc-dataseed1.binance.org") |
|
| 225 | ); |
|
| 226 | setChainWithDefaultRpcUrl( |
|
| 227 | "bnb_smart_chain_testnet", |
|
| 228 | ChainData("BNB Smart Chain Testnet", 97, "https://rpc.ankr.com/bsc_testnet_chapel") |
|
| 229 | ); |
|
| 230 | setChainWithDefaultRpcUrl("gnosis_chain", ChainData("Gnosis Chain", 100, "https://rpc.gnosischain.com")); |
|
| 231 | setChainWithDefaultRpcUrl("moonbeam", ChainData("Moonbeam", 1284, "https://rpc.api.moonbeam.network")); |
|
| 232 | setChainWithDefaultRpcUrl( |
|
| 233 | "moonriver", ChainData("Moonriver", 1285, "https://rpc.api.moonriver.moonbeam.network") |
|
| 234 | ); |
|
| 235 | setChainWithDefaultRpcUrl("moonbase", ChainData("Moonbase", 1287, "https://rpc.testnet.moonbeam.network")); |
|
| 236 | setChainWithDefaultRpcUrl("base_sepolia", ChainData("Base Sepolia", 84532, "https://sepolia.base.org")); |
|
| 237 | setChainWithDefaultRpcUrl("base", ChainData("Base", 8453, "https://mainnet.base.org")); |
|
| 238 | setChainWithDefaultRpcUrl("fraxtal", ChainData("Fraxtal", 252, "https://rpc.frax.com")); |
|
| 239 | setChainWithDefaultRpcUrl("fraxtal_testnet", ChainData("Fraxtal Testnet", 2522, "https://rpc.testnet.frax.com")); |
|
| 240 | } |
|
| 241 | ||
| 242 | // set chain info, with priority to chainAlias' rpc url in foundry.toml |
|
| 243 | function setChainWithDefaultRpcUrl(string memory chainAlias, ChainData memory chain) private { |
|
| 244 | string memory rpcUrl = chain.rpcUrl; |
|
| 245 | defaultRpcUrls[chainAlias] = rpcUrl; |
|
| 246 | chain.rpcUrl = ""; |
|
| 247 | setChain(chainAlias, chain); |
|
| 248 | chain.rpcUrl = rpcUrl; // restore argument |
|
| 249 | } |
|
| 250 | } |
20%
lib/forge-std/src/StdInvariant.sol
Lines covered: 4 / 20 (20%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity >=0.6.2 <0.9.0; |
|
| 3 | ||
| 4 | pragma experimental ABIEncoderV2; |
|
| 5 | ||
| 6 | abstract contract StdInvariant { |
|
| 7 | struct FuzzSelector { |
|
| 8 | address addr; |
|
| 9 | bytes4[] selectors; |
|
| 10 | } |
|
| 11 | ||
| 12 | struct FuzzArtifactSelector { |
|
| 13 | string artifact; |
|
| 14 | bytes4[] selectors; |
|
| 15 | } |
|
| 16 | ||
| 17 | struct FuzzInterface { |
|
| 18 | address addr; |
|
| 19 | string[] artifacts; |
|
| 20 | } |
|
| 21 | ||
| 22 | address[] private _excludedContracts; |
|
| 23 | address[] private _excludedSenders; |
|
| 24 | address[] private _targetedContracts; |
|
| 25 | address[] private _targetedSenders; |
|
| 26 | ||
| 27 | string[] private _excludedArtifacts; |
|
| 28 | string[] private _targetedArtifacts; |
|
| 29 | ||
| 30 | FuzzArtifactSelector[] private _targetedArtifactSelectors; |
|
| 31 | ||
| 32 | FuzzSelector[] private _targetedSelectors; |
|
| 33 | ||
| 34 | FuzzInterface[] private _targetedInterfaces; |
|
| 35 | ||
| 36 | // Functions for users: |
|
| 37 | // These are intended to be called in tests. |
|
| 38 | ||
| 39 | function excludeContract(address newExcludedContract_) internal { |
|
| 40 | _excludedContracts.push(newExcludedContract_); |
|
| 41 | } |
|
| 42 | ||
| 43 | function excludeSender(address newExcludedSender_) internal { |
|
| 44 | _excludedSenders.push(newExcludedSender_); |
|
| 45 | } |
|
| 46 | ||
| 47 | function excludeArtifact(string memory newExcludedArtifact_) internal { |
|
| 48 | _excludedArtifacts.push(newExcludedArtifact_); |
|
| 49 | } |
|
| 50 | ||
| 51 | function targetArtifact(string memory newTargetedArtifact_) internal { |
|
| 52 | _targetedArtifacts.push(newTargetedArtifact_); |
|
| 53 | } |
|
| 54 | ||
| 55 | function targetArtifactSelector(FuzzArtifactSelector memory newTargetedArtifactSelector_) internal { |
|
| 56 | _targetedArtifactSelectors.push(newTargetedArtifactSelector_); |
|
| 57 | } |
|
| 58 | ||
| 59 | function targetContract(address newTargetedContract_) internal { |
|
| 60 | 0 | _targetedContracts.push(newTargetedContract_); |
| 61 | } |
|
| 62 | ||
| 63 | function targetSelector(FuzzSelector memory newTargetedSelector_) internal { |
|
| 64 | _targetedSelectors.push(newTargetedSelector_); |
|
| 65 | } |
|
| 66 | ||
| 67 | function targetSender(address newTargetedSender_) internal { |
|
| 68 | 0 | _targetedSenders.push(newTargetedSender_); |
| 69 | } |
|
| 70 | ||
| 71 | function targetInterface(FuzzInterface memory newTargetedInterface_) internal { |
|
| 72 | _targetedInterfaces.push(newTargetedInterface_); |
|
| 73 | } |
|
| 74 | ||
| 75 | // Functions for forge: |
|
| 76 | // These are called by forge to run invariant tests and don't need to be called in tests. |
|
| 77 | ||
| 78 | 0 | function excludeArtifacts() public view returns (string[] memory excludedArtifacts_) { |
| 79 | 0 | excludedArtifacts_ = _excludedArtifacts; |
| 80 | } |
|
| 81 | ||
| 82 | 0 | function excludeContracts() public view returns (address[] memory excludedContracts_) { |
| 83 | 0 | excludedContracts_ = _excludedContracts; |
| 84 | } |
|
| 85 | ||
| 86 | 0 | function excludeSenders() public view returns (address[] memory excludedSenders_) { |
| 87 | 0 | excludedSenders_ = _excludedSenders; |
| 88 | } |
|
| 89 | ||
| 90 | 0 | function targetArtifacts() public view returns (string[] memory targetedArtifacts_) { |
| 91 | 0 | targetedArtifacts_ = _targetedArtifacts; |
| 92 | } |
|
| 93 | ||
| 94 | 14× | function targetArtifactSelectors() public view returns (FuzzArtifactSelector[] memory targetedArtifactSelectors_) { |
| 95 | 33× | targetedArtifactSelectors_ = _targetedArtifactSelectors; |
| 96 | } |
|
| 97 | ||
| 98 | 0 | function targetContracts() public view returns (address[] memory targetedContracts_) { |
| 99 | 0 | targetedContracts_ = _targetedContracts; |
| 100 | } |
|
| 101 | ||
| 102 | 0 | function targetSelectors() public view returns (FuzzSelector[] memory targetedSelectors_) { |
| 103 | 0 | targetedSelectors_ = _targetedSelectors; |
| 104 | } |
|
| 105 | ||
| 106 | 0 | function targetSenders() public view returns (address[] memory targetedSenders_) { |
| 107 | 0 | targetedSenders_ = _targetedSenders; |
| 108 | } |
|
| 109 | ||
| 110 | 2× | function targetInterfaces() public view returns (FuzzInterface[] memory targetedInterfaces_) { |
| 111 | 7× | targetedInterfaces_ = _targetedInterfaces; |
| 112 | } |
|
| 113 | } |
100%
lib/forge-std/src/Test.sol
Lines covered: 1 / 1 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity >=0.6.2 <0.9.0; |
|
| 3 | ||
| 4 | pragma experimental ABIEncoderV2; |
|
| 5 | ||
| 6 | // 💬 ABOUT |
|
| 7 | // Forge Std's default Test. |
|
| 8 | ||
| 9 | // 🧩 MODULES |
|
| 10 | import {console} from "./console.sol"; |
|
| 11 | import {console2} from "./console2.sol"; |
|
| 12 | import {safeconsole} from "./safeconsole.sol"; |
|
| 13 | import {StdAssertions} from "./StdAssertions.sol"; |
|
| 14 | import {StdChains} from "./StdChains.sol"; |
|
| 15 | import {StdCheats} from "./StdCheats.sol"; |
|
| 16 | import {stdError} from "./StdError.sol"; |
|
| 17 | import {StdInvariant} from "./StdInvariant.sol"; |
|
| 18 | import {stdJson} from "./StdJson.sol"; |
|
| 19 | import {stdMath} from "./StdMath.sol"; |
|
| 20 | import {StdStorage, stdStorage} from "./StdStorage.sol"; |
|
| 21 | import {StdStyle} from "./StdStyle.sol"; |
|
| 22 | import {stdToml} from "./StdToml.sol"; |
|
| 23 | import {StdUtils} from "./StdUtils.sol"; |
|
| 24 | import {Vm} from "./Vm.sol"; |
|
| 25 | ||
| 26 | // 📦 BOILERPLATE |
|
| 27 | import {TestBase} from "./Base.sol"; |
|
| 28 | ||
| 29 | // ⭐️ TEST |
|
| 30 | abstract contract Test is TestBase, StdAssertions, StdChains, StdCheats, StdInvariant, StdUtils { |
|
| 31 | // Note: IS_TEST() must return true. |
|
| 32 | 11× | bool public IS_TEST = true; |
| 33 | } |
42%
lib/forge-std/src/mocks/MockERC20.sol
Lines covered: 34 / 80 (42%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity >=0.6.2 <0.9.0; |
|
| 3 | ||
| 4 | import {IERC20} from "../interfaces/IERC20.sol"; |
|
| 5 | ||
| 6 | /// @notice This is a mock contract of the ERC20 standard for testing purposes only, it SHOULD NOT be used in production. |
|
| 7 | /// @dev Forked from: https://github.com/transmissions11/solmate/blob/0384dbaaa4fcb5715738a9254a7c0a4cb62cf458/src/tokens/ERC20.sol |
|
| 8 | 0 | contract MockERC20 is IERC20 { |
| 9 | /*////////////////////////////////////////////////////////////// |
|
| 10 | METADATA STORAGE |
|
| 11 | //////////////////////////////////////////////////////////////*/ |
|
| 12 | ||
| 13 | string internal _name; |
|
| 14 | ||
| 15 | string internal _symbol; |
|
| 16 | ||
| 17 | uint8 internal _decimals; |
|
| 18 | ||
| 19 | 32× | function name() external view override returns (string memory) { |
| 20 | 0 | return _name; |
| 21 | } |
|
| 22 | ||
| 23 | 0 | function symbol() external view override returns (string memory) { |
| 24 | 0 | return _symbol; |
| 25 | } |
|
| 26 | ||
| 27 | 0 | function decimals() external view override returns (uint8) { |
| 28 | 0 | return _decimals; |
| 29 | } |
|
| 30 | ||
| 31 | /*////////////////////////////////////////////////////////////// |
|
| 32 | ERC20 STORAGE |
|
| 33 | //////////////////////////////////////////////////////////////*/ |
|
| 34 | ||
| 35 | uint256 internal _totalSupply; |
|
| 36 | ||
| 37 | mapping(address => uint256) internal _balanceOf; |
|
| 38 | ||
| 39 | mapping(address => mapping(address => uint256)) internal _allowance; |
|
| 40 | ||
| 41 | 12× | function totalSupply() external view override returns (uint256) { |
| 42 | 0 | return _totalSupply; |
| 43 | } |
|
| 44 | ||
| 45 | 30× | function balanceOf(address owner) external view override returns (uint256) { |
| 46 | 39× | return _balanceOf[owner]; |
| 47 | } |
|
| 48 | ||
| 49 | 0 | function allowance(address owner, address spender) external view override returns (uint256) { |
| 50 | 0 | return _allowance[owner][spender]; |
| 51 | } |
|
| 52 | ||
| 53 | /*////////////////////////////////////////////////////////////// |
|
| 54 | EIP-2612 STORAGE |
|
| 55 | //////////////////////////////////////////////////////////////*/ |
|
| 56 | ||
| 57 | uint256 internal INITIAL_CHAIN_ID; |
|
| 58 | ||
| 59 | bytes32 internal INITIAL_DOMAIN_SEPARATOR; |
|
| 60 | ||
| 61 | 21× | mapping(address => uint256) public nonces; |
| 62 | ||
| 63 | /*////////////////////////////////////////////////////////////// |
|
| 64 | INITIALIZE |
|
| 65 | //////////////////////////////////////////////////////////////*/ |
|
| 66 | ||
| 67 | /// @dev A bool to track whether the contract has been initialized. |
|
| 68 | bool private initialized; |
|
| 69 | ||
| 70 | /// @dev To hide constructor warnings across solc versions due to different constructor visibility requirements and |
|
| 71 | /// syntaxes, we add an initialization function that can be called only once. |
|
| 72 | 0 | function initialize(string memory name_, string memory symbol_, uint8 decimals_) public { |
| 73 | 16× | require(!initialized, "ALREADY_INITIALIZED"); |
| 74 | ||
| 75 | 0 | _name = name_; |
| 76 | 0 | _symbol = symbol_; |
| 77 | 0 | _decimals = decimals_; |
| 78 | ||
| 79 | 0 | INITIAL_CHAIN_ID = _pureChainId(); |
| 80 | 0 | INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); |
| 81 | ||
| 82 | 0 | initialized = true; |
| 83 | } |
|
| 84 | ||
| 85 | /*////////////////////////////////////////////////////////////// |
|
| 86 | ERC20 LOGIC |
|
| 87 | //////////////////////////////////////////////////////////////*/ |
|
| 88 | ||
| 89 | 54× | function approve(address spender, uint256 amount) public virtual override returns (bool) { |
| 90 | 30× | _allowance[msg.sender][spender] = amount; |
| 91 | ||
| 92 | 37× | emit Approval(msg.sender, spender, amount); |
| 93 | ||
| 94 | 4× | return true; |
| 95 | } |
|
| 96 | ||
| 97 | 48× | function transfer(address to, uint256 amount) public virtual override returns (bool) { |
| 98 | 136× | _balanceOf[msg.sender] = _sub(_balanceOf[msg.sender], amount); |
| 99 | 124× | _balanceOf[to] = _add(_balanceOf[to], amount); |
| 100 | ||
| 101 | 36× | emit Transfer(msg.sender, to, amount); |
| 102 | ||
| 103 | return true; |
|
| 104 | } |
|
| 105 | ||
| 106 | 42× | function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { |
| 107 | 72× | uint256 allowed = _allowance[from][msg.sender]; // Saves gas for limited approvals. |
| 108 | ||
| 109 | 111× | if (allowed != ~uint256(0)) _allowance[from][msg.sender] = _sub(allowed, amount); |
| 110 | ||
| 111 | 117× | _balanceOf[from] = _sub(_balanceOf[from], amount); |
| 112 | 99× | _balanceOf[to] = _add(_balanceOf[to], amount); |
| 113 | ||
| 114 | 57× | emit Transfer(from, to, amount); |
| 115 | ||
| 116 | 6× | return true; |
| 117 | } |
|
| 118 | ||
| 119 | /*////////////////////////////////////////////////////////////// |
|
| 120 | EIP-2612 LOGIC |
|
| 121 | //////////////////////////////////////////////////////////////*/ |
|
| 122 | ||
| 123 | 0 | function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) |
| 124 | public |
|
| 125 | virtual |
|
| 126 | 0 | { |
| 127 | 0 | require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); |
| 128 | ||
| 129 | 0 | address recoveredAddress = ecrecover( |
| 130 | 0 | keccak256( |
| 131 | 0 | abi.encodePacked( |
| 132 | "\x19\x01", |
|
| 133 | 0 | DOMAIN_SEPARATOR(), |
| 134 | 0 | keccak256( |
| 135 | 0 | abi.encode( |
| 136 | 0 | keccak256( |
| 137 | "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" |
|
| 138 | ), |
|
| 139 | 0 | owner, |
| 140 | 0 | spender, |
| 141 | 0 | value, |
| 142 | 0 | nonces[owner]++, |
| 143 | deadline |
|
| 144 | ) |
|
| 145 | ) |
|
| 146 | ) |
|
| 147 | ), |
|
| 148 | v, |
|
| 149 | r, |
|
| 150 | s |
|
| 151 | ); |
|
| 152 | ||
| 153 | 0 | require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); |
| 154 | ||
| 155 | 0 | _allowance[recoveredAddress][spender] = value; |
| 156 | ||
| 157 | 0 | emit Approval(owner, spender, value); |
| 158 | } |
|
| 159 | ||
| 160 | 7× | function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { |
| 161 | 13× | return _pureChainId() == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); |
| 162 | } |
|
| 163 | ||
| 164 | 0 | function computeDomainSeparator() internal view virtual returns (bytes32) { |
| 165 | 0 | return keccak256( |
| 166 | 0 | abi.encode( |
| 167 | 0 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), |
| 168 | 0 | keccak256(bytes(_name)), |
| 169 | 0 | keccak256("1"), |
| 170 | 0 | _pureChainId(), |
| 171 | 0 | address(this) |
| 172 | ) |
|
| 173 | ); |
|
| 174 | } |
|
| 175 | ||
| 176 | /*////////////////////////////////////////////////////////////// |
|
| 177 | INTERNAL MINT/BURN LOGIC |
|
| 178 | //////////////////////////////////////////////////////////////*/ |
|
| 179 | ||
| 180 | 0 | function _mint(address to, uint256 amount) internal virtual { |
| 181 | 0 | _totalSupply = _add(_totalSupply, amount); |
| 182 | 0 | _balanceOf[to] = _add(_balanceOf[to], amount); |
| 183 | ||
| 184 | 0 | emit Transfer(address(0), to, amount); |
| 185 | } |
|
| 186 | ||
| 187 | function _burn(address from, uint256 amount) internal virtual { |
|
| 188 | _balanceOf[from] = _sub(_balanceOf[from], amount); |
|
| 189 | _totalSupply = _sub(_totalSupply, amount); |
|
| 190 | ||
| 191 | emit Transfer(from, address(0), amount); |
|
| 192 | } |
|
| 193 | ||
| 194 | /*////////////////////////////////////////////////////////////// |
|
| 195 | INTERNAL SAFE MATH LOGIC |
|
| 196 | //////////////////////////////////////////////////////////////*/ |
|
| 197 | ||
| 198 | 12× | function _add(uint256 a, uint256 b) internal pure returns (uint256) { |
| 199 | 32× | uint256 c = a + b; |
| 200 | 24× | require(c >= a, "ERC20: addition overflow"); |
| 201 | 0 | return c; |
| 202 | } |
|
| 203 | ||
| 204 | 16× | function _sub(uint256 a, uint256 b) internal pure returns (uint256) { |
| 205 | 46× | require(a >= b, "ERC20: subtraction underflow"); |
| 206 | 28× | return a - b; |
| 207 | } |
|
| 208 | ||
| 209 | /*////////////////////////////////////////////////////////////// |
|
| 210 | HELPERS |
|
| 211 | //////////////////////////////////////////////////////////////*/ |
|
| 212 | ||
| 213 | // We use this complex approach of `_viewChainId` and `_pureChainId` to ensure there are no |
|
| 214 | // compiler warnings when accessing chain ID in any solidity version supported by forge-std. We |
|
| 215 | // can't simply access the chain ID in a normal view or pure function because the solc View Pure |
|
| 216 | // Checker changed `chainid` from pure to view in 0.8.0. |
|
| 217 | 2× | function _viewChainId() private view returns (uint256 chainId) { |
| 218 | // Assembly required since `block.chainid` was introduced in 0.8.0. |
|
| 219 | assembly { |
|
| 220 | 2× | chainId := chainid() |
| 221 | } |
|
| 222 | ||
| 223 | address(this); // Silence warnings in older Solc versions. |
|
| 224 | } |
|
| 225 | ||
| 226 | 6× | function _pureChainId() private pure returns (uint256 chainId) { |
| 227 | 2× | function() internal view returns (uint256) fnIn = _viewChainId; |
| 228 | function() internal pure returns (uint256) pureChainId; |
|
| 229 | assembly { |
|
| 230 | pureChainId := fnIn |
|
| 231 | } |
|
| 232 | 8× | chainId = pureChainId(); |
| 233 | } |
|
| 234 | } |
94%
lib/openzeppelin-contracts/contracts/proxy/Clones.sol
Lines covered: 18 / 19 (94%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v5.0.0) (proxy/Clones.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.20; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for |
|
| 8 | * deploying minimal proxy contracts, also known as "clones". |
|
| 9 | * |
|
| 10 | * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies |
|
| 11 | * > a minimal bytecode implementation that delegates all calls to a known, fixed address. |
|
| 12 | * |
|
| 13 | * The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2` |
|
| 14 | * (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the |
|
| 15 | * deterministic method. |
|
| 16 | */ |
|
| 17 | 0 | library Clones { |
| 18 | /** |
|
| 19 | * @dev A clone instance deployment failed. |
|
| 20 | */ |
|
| 21 | error ERC1167FailedCreateClone(); |
|
| 22 | ||
| 23 | /** |
|
| 24 | * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. |
|
| 25 | * |
|
| 26 | * This function uses the create opcode, which should never revert. |
|
| 27 | */ |
|
| 28 | function clone(address implementation) internal returns (address instance) { |
|
| 29 | /// @solidity memory-safe-assembly |
|
| 30 | assembly { |
|
| 31 | // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes |
|
| 32 | // of the `implementation` address with the bytecode before the address. |
|
| 33 | mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000)) |
|
| 34 | // Packs the remaining 17 bytes of `implementation` with the bytecode after the address. |
|
| 35 | mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) |
|
| 36 | instance := create(0, 0x09, 0x37) |
|
| 37 | } |
|
| 38 | if (instance == address(0)) { |
|
| 39 | revert ERC1167FailedCreateClone(); |
|
| 40 | } |
|
| 41 | } |
|
| 42 | ||
| 43 | /** |
|
| 44 | * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. |
|
| 45 | * |
|
| 46 | * This function uses the create2 opcode and a `salt` to deterministically deploy |
|
| 47 | * the clone. Using the same `implementation` and `salt` multiple time will revert, since |
|
| 48 | * the clones cannot be deployed twice at the same address. |
|
| 49 | */ |
|
| 50 | 2× | function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) { |
| 51 | /// @solidity memory-safe-assembly |
|
| 52 | assembly { |
|
| 53 | // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes |
|
| 54 | // of the `implementation` address with the bytecode before the address. |
|
| 55 | 9× | mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000)) |
| 56 | // Packs the remaining 17 bytes of `implementation` with the bytecode after the address. |
|
| 57 | 7× | mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) |
| 58 | 6× | instance := create2(0, 0x09, 0x37, salt) |
| 59 | } |
|
| 60 | 5× | if (instance == address(0)) { |
| 61 | 14× | revert ERC1167FailedCreateClone(); |
| 62 | } |
|
| 63 | } |
|
| 64 | ||
| 65 | /** |
|
| 66 | * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. |
|
| 67 | */ |
|
| 68 | 1× | function predictDeterministicAddress( |
| 69 | address implementation, |
|
| 70 | bytes32 salt, |
|
| 71 | address deployer |
|
| 72 | ) internal pure returns (address predicted) { |
|
| 73 | /// @solidity memory-safe-assembly |
|
| 74 | assembly { |
|
| 75 | 2× | let ptr := mload(0x40) |
| 76 | 4× | mstore(add(ptr, 0x38), deployer) |
| 77 | 5× | mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff) |
| 78 | 5× | mstore(add(ptr, 0x14), implementation) |
| 79 | 3× | mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73) |
| 80 | 6× | mstore(add(ptr, 0x58), salt) |
| 81 | 9× | mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37)) |
| 82 | 6× | predicted := keccak256(add(ptr, 0x43), 0x55) |
| 83 | } |
|
| 84 | } |
|
| 85 | ||
| 86 | /** |
|
| 87 | * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. |
|
| 88 | */ |
|
| 89 | 1× | function predictDeterministicAddress( |
| 90 | address implementation, |
|
| 91 | bytes32 salt |
|
| 92 | 2× | ) internal view returns (address predicted) { |
| 93 | 2× | return predictDeterministicAddress(implementation, salt, address(this)); |
| 94 | } |
|
| 95 | } |
77%
lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
Lines covered: 7 / 9 (77%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.20; |
|
| 5 | ||
| 6 | import {IERC20} from "../IERC20.sol"; |
|
| 7 | import {IERC20Permit} from "../extensions/IERC20Permit.sol"; |
|
| 8 | import {Address} from "../../../utils/Address.sol"; |
|
| 9 | ||
| 10 | /** |
|
| 11 | * @title SafeERC20 |
|
| 12 | * @dev Wrappers around ERC20 operations that throw on failure (when the token |
|
| 13 | * contract returns false). Tokens that return no value (and instead revert or |
|
| 14 | * throw on failure) are also supported, non-reverting calls are assumed to be |
|
| 15 | * successful. |
|
| 16 | * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, |
|
| 17 | * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. |
|
| 18 | */ |
|
| 19 | 0 | library SafeERC20 { |
| 20 | using Address for address; |
|
| 21 | ||
| 22 | /** |
|
| 23 | * @dev An operation with an ERC20 token failed. |
|
| 24 | */ |
|
| 25 | error SafeERC20FailedOperation(address token); |
|
| 26 | ||
| 27 | /** |
|
| 28 | * @dev Indicates a failed `decreaseAllowance` request. |
|
| 29 | */ |
|
| 30 | error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); |
|
| 31 | ||
| 32 | /** |
|
| 33 | * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, |
|
| 34 | * non-reverting calls are assumed to be successful. |
|
| 35 | */ |
|
| 36 | 10× | function safeTransfer(IERC20 token, address to, uint256 value) internal { |
| 37 | 93× | _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); |
| 38 | } |
|
| 39 | ||
| 40 | /** |
|
| 41 | * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the |
|
| 42 | * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. |
|
| 43 | */ |
|
| 44 | 7× | function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { |
| 45 | 68× | _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); |
| 46 | } |
|
| 47 | ||
| 48 | /** |
|
| 49 | * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, |
|
| 50 | * non-reverting calls are assumed to be successful. |
|
| 51 | */ |
|
| 52 | function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { |
|
| 53 | uint256 oldAllowance = token.allowance(address(this), spender); |
|
| 54 | forceApprove(token, spender, oldAllowance + value); |
|
| 55 | } |
|
| 56 | ||
| 57 | /** |
|
| 58 | * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no |
|
| 59 | * value, non-reverting calls are assumed to be successful. |
|
| 60 | */ |
|
| 61 | function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { |
|
| 62 | unchecked { |
|
| 63 | uint256 currentAllowance = token.allowance(address(this), spender); |
|
| 64 | if (currentAllowance < requestedDecrease) { |
|
| 65 | revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); |
|
| 66 | } |
|
| 67 | forceApprove(token, spender, currentAllowance - requestedDecrease); |
|
| 68 | } |
|
| 69 | } |
|
| 70 | ||
| 71 | /** |
|
| 72 | * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, |
|
| 73 | * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval |
|
| 74 | * to be set to zero before setting it to a non-zero value, such as USDT. |
|
| 75 | */ |
|
| 76 | function forceApprove(IERC20 token, address spender, uint256 value) internal { |
|
| 77 | bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); |
|
| 78 | ||
| 79 | if (!_callOptionalReturnBool(token, approvalCall)) { |
|
| 80 | _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); |
|
| 81 | _callOptionalReturn(token, approvalCall); |
|
| 82 | } |
|
| 83 | } |
|
| 84 | ||
| 85 | /** |
|
| 86 | * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement |
|
| 87 | * on the return value: the return value is optional (but if data is returned, it must not be false). |
|
| 88 | * @param token The token targeted by the call. |
|
| 89 | * @param data The call data (encoded using abi.encode or one of its variants). |
|
| 90 | */ |
|
| 91 | 2× | function _callOptionalReturn(IERC20 token, bytes memory data) private { |
| 92 | // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since |
|
| 93 | // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that |
|
| 94 | // the target address contains contract code and also asserts for success in the low-level call. |
|
| 95 | ||
| 96 | 29× | bytes memory returndata = address(token).functionCall(data); |
| 97 | 90× | if (returndata.length != 0 && !abi.decode(returndata, (bool))) { |
| 98 | 0 | revert SafeERC20FailedOperation(address(token)); |
| 99 | } |
|
| 100 | } |
|
| 101 | ||
| 102 | /** |
|
| 103 | * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement |
|
| 104 | * on the return value: the return value is optional (but if data is returned, it must not be false). |
|
| 105 | * @param token The token targeted by the call. |
|
| 106 | * @param data The call data (encoded using abi.encode or one of its variants). |
|
| 107 | * |
|
| 108 | * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. |
|
| 109 | */ |
|
| 110 | function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { |
|
| 111 | // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since |
|
| 112 | // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false |
|
| 113 | // and not revert is the subcall reverts. |
|
| 114 | ||
| 115 | (bool success, bytes memory returndata) = address(token).call(data); |
|
| 116 | return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0; |
|
| 117 | } |
|
| 118 | } |
80%
lib/openzeppelin-contracts/contracts/utils/Address.sol
Lines covered: 16 / 20 (80%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.20; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @dev Collection of functions related to the address type |
|
| 8 | */ |
|
| 9 | 0 | library Address { |
| 10 | /** |
|
| 11 | * @dev The ETH balance of the account is not enough to perform the operation. |
|
| 12 | */ |
|
| 13 | error AddressInsufficientBalance(address account); |
|
| 14 | ||
| 15 | /** |
|
| 16 | * @dev There's no code at `target` (it is not a contract). |
|
| 17 | */ |
|
| 18 | error AddressEmptyCode(address target); |
|
| 19 | ||
| 20 | /** |
|
| 21 | * @dev A call to an address target failed. The target may have reverted. |
|
| 22 | */ |
|
| 23 | error FailedInnerCall(); |
|
| 24 | ||
| 25 | /** |
|
| 26 | * @dev Replacement for Solidity's `transfer`: sends `amount` wei to |
|
| 27 | * `recipient`, forwarding all available gas and reverting on errors. |
|
| 28 | * |
|
| 29 | * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost |
|
| 30 | * of certain opcodes, possibly making contracts go over the 2300 gas limit |
|
| 31 | * imposed by `transfer`, making them unable to receive funds via |
|
| 32 | * `transfer`. {sendValue} removes this limitation. |
|
| 33 | * |
|
| 34 | * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. |
|
| 35 | * |
|
| 36 | * IMPORTANT: because control is transferred to `recipient`, care must be |
|
| 37 | * taken to not create reentrancy vulnerabilities. Consider using |
|
| 38 | * {ReentrancyGuard} or the |
|
| 39 | * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. |
|
| 40 | */ |
|
| 41 | function sendValue(address payable recipient, uint256 amount) internal { |
|
| 42 | if (address(this).balance < amount) { |
|
| 43 | revert AddressInsufficientBalance(address(this)); |
|
| 44 | } |
|
| 45 | ||
| 46 | (bool success, ) = recipient.call{value: amount}(""); |
|
| 47 | if (!success) { |
|
| 48 | revert FailedInnerCall(); |
|
| 49 | } |
|
| 50 | } |
|
| 51 | ||
| 52 | /** |
|
| 53 | * @dev Performs a Solidity function call using a low level `call`. A |
|
| 54 | * plain `call` is an unsafe replacement for a function call: use this |
|
| 55 | * function instead. |
|
| 56 | * |
|
| 57 | * If `target` reverts with a revert reason or custom error, it is bubbled |
|
| 58 | * up by this function (like regular Solidity function calls). However, if |
|
| 59 | * the call reverted with no returned reason, this function reverts with a |
|
| 60 | * {FailedInnerCall} error. |
|
| 61 | * |
|
| 62 | * Returns the raw returned data. To convert to the expected return value, |
|
| 63 | * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. |
|
| 64 | * |
|
| 65 | * Requirements: |
|
| 66 | * |
|
| 67 | * - `target` must be a contract. |
|
| 68 | * - calling `target` with `data` must not revert. |
|
| 69 | */ |
|
| 70 | 9× | function functionCall(address target, bytes memory data) internal returns (bytes memory) { |
| 71 | 18× | return functionCallWithValue(target, data, 0); |
| 72 | } |
|
| 73 | ||
| 74 | /** |
|
| 75 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
|
| 76 | * but also transferring `value` wei to `target`. |
|
| 77 | * |
|
| 78 | * Requirements: |
|
| 79 | * |
|
| 80 | * - the calling contract must have an ETH balance of at least `value`. |
|
| 81 | * - the called Solidity function must be `payable`. |
|
| 82 | */ |
|
| 83 | 10× | function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { |
| 84 | 14× | if (address(this).balance < value) { |
| 85 | 0 | revert AddressInsufficientBalance(address(this)); |
| 86 | } |
|
| 87 | 204× | (bool success, bytes memory returndata) = target.call{value: value}(data); |
| 88 | 24× | return verifyCallResultFromTarget(target, success, returndata); |
| 89 | } |
|
| 90 | ||
| 91 | /** |
|
| 92 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
|
| 93 | * but performing a static call. |
|
| 94 | */ |
|
| 95 | function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { |
|
| 96 | (bool success, bytes memory returndata) = target.staticcall(data); |
|
| 97 | return verifyCallResultFromTarget(target, success, returndata); |
|
| 98 | } |
|
| 99 | ||
| 100 | /** |
|
| 101 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], |
|
| 102 | * but performing a delegate call. |
|
| 103 | */ |
|
| 104 | function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { |
|
| 105 | (bool success, bytes memory returndata) = target.delegatecall(data); |
|
| 106 | return verifyCallResultFromTarget(target, success, returndata); |
|
| 107 | } |
|
| 108 | ||
| 109 | /** |
|
| 110 | * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target |
|
| 111 | * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an |
|
| 112 | * unsuccessful call. |
|
| 113 | */ |
|
| 114 | 3× | function verifyCallResultFromTarget( |
| 115 | address target, |
|
| 116 | bool success, |
|
| 117 | bytes memory returndata |
|
| 118 | 3× | ) internal view returns (bytes memory) { |
| 119 | 12× | if (!success) { |
| 120 | 8× | _revert(returndata); |
| 121 | } else { |
|
| 122 | // only check if target is a contract if the call was successful and the return data is empty |
|
| 123 | // otherwise we already know that it was a contract |
|
| 124 | 36× | if (returndata.length == 0 && target.code.length == 0) { |
| 125 | 0 | revert AddressEmptyCode(target); |
| 126 | } |
|
| 127 | 9× | return returndata; |
| 128 | } |
|
| 129 | } |
|
| 130 | ||
| 131 | /** |
|
| 132 | * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the |
|
| 133 | * revert reason or with a default {FailedInnerCall} error. |
|
| 134 | */ |
|
| 135 | function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { |
|
| 136 | if (!success) { |
|
| 137 | _revert(returndata); |
|
| 138 | } else { |
|
| 139 | return returndata; |
|
| 140 | } |
|
| 141 | } |
|
| 142 | ||
| 143 | /** |
|
| 144 | * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. |
|
| 145 | */ |
|
| 146 | 2× | function _revert(bytes memory returndata) private pure { |
| 147 | // Look for revert reason and bubble it up if present |
|
| 148 | 10× | if (returndata.length > 0) { |
| 149 | // The easiest way to bubble the revert reason is using memory via assembly |
|
| 150 | /// @solidity memory-safe-assembly |
|
| 151 | assembly { |
|
| 152 | 4× | let returndata_size := mload(returndata) |
| 153 | 10× | revert(add(32, returndata), returndata_size) |
| 154 | } |
|
| 155 | } else { |
|
| 156 | 0 | revert FailedInnerCall(); |
| 157 | } |
|
| 158 | } |
|
| 159 | } |
83%
lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol
Lines covered: 10 / 12 (83%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | // OpenZeppelin Contracts (last updated v5.0.0) (utils/ReentrancyGuard.sol) |
|
| 3 | ||
| 4 | pragma solidity ^0.8.20; |
|
| 5 | ||
| 6 | /** |
|
| 7 | * @dev Contract module that helps prevent reentrant calls to a function. |
|
| 8 | * |
|
| 9 | * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier |
|
| 10 | * available, which can be applied to functions to make sure there are no nested |
|
| 11 | * (reentrant) calls to them. |
|
| 12 | * |
|
| 13 | * Note that because there is a single `nonReentrant` guard, functions marked as |
|
| 14 | * `nonReentrant` may not call one another. This can be worked around by making |
|
| 15 | * those functions `private`, and then adding `external` `nonReentrant` entry |
|
| 16 | * points to them. |
|
| 17 | * |
|
| 18 | * TIP: If you would like to learn more about reentrancy and alternative ways |
|
| 19 | * to protect against it, check out our blog post |
|
| 20 | * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. |
|
| 21 | */ |
|
| 22 | abstract contract ReentrancyGuard { |
|
| 23 | // Booleans are more expensive than uint256 or any type that takes up a full |
|
| 24 | // word because each write operation emits an extra SLOAD to first read the |
|
| 25 | // slot's contents, replace the bits taken up by the boolean, and then write |
|
| 26 | // back. This is the compiler's defense against contract upgrades and |
|
| 27 | // pointer aliasing, and it cannot be disabled. |
|
| 28 | ||
| 29 | // The values being non-zero value makes deployment a bit more expensive, |
|
| 30 | // but in exchange the refund on every call to nonReentrant will be lower in |
|
| 31 | // amount. Since refunds are capped to a percentage of the total |
|
| 32 | // transaction's gas, it is best to keep them low in cases like this one, to |
|
| 33 | // increase the likelihood of the full refund coming into effect. |
|
| 34 | 7× | uint256 private constant NOT_ENTERED = 1; |
| 35 | 2× | uint256 private constant ENTERED = 2; |
| 36 | ||
| 37 | uint256 private _status; |
|
| 38 | ||
| 39 | /** |
|
| 40 | * @dev Unauthorized reentrant call. |
|
| 41 | */ |
|
| 42 | error ReentrancyGuardReentrantCall(); |
|
| 43 | ||
| 44 | constructor() { |
|
| 45 | 0 | _status = NOT_ENTERED; |
| 46 | } |
|
| 47 | ||
| 48 | /** |
|
| 49 | * @dev Prevents a contract from calling itself, directly or indirectly. |
|
| 50 | * Calling a `nonReentrant` function from another `nonReentrant` |
|
| 51 | * function is not supported. It is possible to prevent this from happening |
|
| 52 | * by making the `nonReentrant` function external, and making it call a |
|
| 53 | * `private` function that does the actual work. |
|
| 54 | */ |
|
| 55 | modifier nonReentrant() { |
|
| 56 | 28× | _nonReentrantBefore(); |
| 57 | 1× | _; |
| 58 | 12× | _nonReentrantAfter(); |
| 59 | } |
|
| 60 | ||
| 61 | 2× | function _nonReentrantBefore() private { |
| 62 | // On the first call to nonReentrant, _status will be NOT_ENTERED |
|
| 63 | 6× | if (_status == ENTERED) { |
| 64 | 0 | revert ReentrancyGuardReentrantCall(); |
| 65 | } |
|
| 66 | ||
| 67 | // Any calls to nonReentrant after this point will fail |
|
| 68 | 2× | _status = ENTERED; |
| 69 | } |
|
| 70 | ||
| 71 | 7× | function _nonReentrantAfter() private { |
| 72 | // By storing the original value once again, a refund is triggered (see |
|
| 73 | // https://eips.ethereum.org/EIPS/eip-2200) |
|
| 74 | 14× | _status = NOT_ENTERED; |
| 75 | } |
|
| 76 | ||
| 77 | /** |
|
| 78 | * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a |
|
| 79 | * `nonReentrant` function in the call stack. |
|
| 80 | */ |
|
| 81 | function _reentrancyGuardEntered() internal view returns (bool) { |
|
| 82 | return _status == ENTERED; |
|
| 83 | } |
|
| 84 | } |
73%
src/BribeInitiative.sol
Lines covered: 76 / 104 (73%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; |
|
| 5 | import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; |
|
| 6 | ||
| 7 | import {IGovernance} from "./interfaces/IGovernance.sol"; |
|
| 8 | import {IInitiative} from "./interfaces/IInitiative.sol"; |
|
| 9 | import {IBribeInitiative} from "./interfaces/IBribeInitiative.sol"; |
|
| 10 | ||
| 11 | import {DoubleLinkedList} from "./utils/DoubleLinkedList.sol"; |
|
| 12 | ||
| 13 | 234× | contract BribeInitiative is IInitiative, IBribeInitiative { |
| 14 | using SafeERC20 for IERC20; |
|
| 15 | using DoubleLinkedList for DoubleLinkedList.List; |
|
| 16 | ||
| 17 | /// @inheritdoc IBribeInitiative |
|
| 18 | 0 | IGovernance public immutable governance; |
| 19 | /// @inheritdoc IBribeInitiative |
|
| 20 | 0 | IERC20 public immutable bold; |
| 21 | /// @inheritdoc IBribeInitiative |
|
| 22 | 0 | IERC20 public immutable bribeToken; |
| 23 | ||
| 24 | /// @inheritdoc IBribeInitiative |
|
| 25 | 0 | mapping(uint16 => Bribe) public bribeByEpoch; |
| 26 | /// @inheritdoc IBribeInitiative |
|
| 27 | 38× | mapping(address => mapping(uint16 => bool)) public claimedBribeAtEpoch; |
| 28 | ||
| 29 | /// Double linked list of the total LQTY allocated at a given epoch |
|
| 30 | DoubleLinkedList.List internal totalLQTYAllocationByEpoch; |
|
| 31 | /// Double linked list of LQTY allocated by a user at a given epoch |
|
| 32 | mapping(address => DoubleLinkedList.List) internal lqtyAllocationByUserAtEpoch; |
|
| 33 | ||
| 34 | 27× | constructor(address _governance, address _bold, address _bribeToken) { |
| 35 | 5× | governance = IGovernance(_governance); |
| 36 | 5× | bold = IERC20(_bold); |
| 37 | 3× | bribeToken = IERC20(_bribeToken); |
| 38 | } |
|
| 39 | ||
| 40 | modifier onlyGovernance() { |
|
| 41 | 33× | require(msg.sender == address(governance), "BribeInitiative: invalid-sender"); |
| 42 | 2× | _; |
| 43 | } |
|
| 44 | ||
| 45 | /// @inheritdoc IBribeInitiative |
|
| 46 | 25× | function totalLQTYAllocatedByEpoch(uint16 _epoch) external view returns (uint88) { |
| 47 | 3× | return totalLQTYAllocationByEpoch.getValue(_epoch); |
| 48 | } |
|
| 49 | ||
| 50 | /// @inheritdoc IBribeInitiative |
|
| 51 | 16× | function lqtyAllocatedByUserAtEpoch(address _user, uint16 _epoch) external view returns (uint88) { |
| 52 | 19× | return lqtyAllocationByUserAtEpoch[_user].getValue(_epoch); |
| 53 | } |
|
| 54 | ||
| 55 | /// @inheritdoc IBribeInitiative |
|
| 56 | 17× | function depositBribe(uint128 _boldAmount, uint128 _bribeTokenAmount, uint16 _epoch) external { |
| 57 | 12× | bold.safeTransferFrom(msg.sender, address(this), _boldAmount); |
| 58 | 12× | bribeToken.safeTransferFrom(msg.sender, address(this), _bribeTokenAmount); |
| 59 | ||
| 60 | 62× | uint16 epoch = governance.epoch(); |
| 61 | 19× | require(_epoch > epoch, "BribeInitiative: only-future-epochs"); |
| 62 | ||
| 63 | 46× | Bribe memory bribe = bribeByEpoch[_epoch]; |
| 64 | 13× | bribe.boldAmount += _boldAmount; |
| 65 | 22× | bribe.bribeTokenAmount += _bribeTokenAmount; |
| 66 | 32× | bribeByEpoch[_epoch] = bribe; |
| 67 | ||
| 68 | 12× | emit DepositBribe(msg.sender, _boldAmount, _bribeTokenAmount, _epoch); |
| 69 | } |
|
| 70 | ||
| 71 | 1× | function _claimBribe( |
| 72 | address _user, |
|
| 73 | uint16 _epoch, |
|
| 74 | uint16 _prevLQTYAllocationEpoch, |
|
| 75 | uint16 _prevTotalLQTYAllocationEpoch |
|
| 76 | 2× | ) internal returns (uint256 boldAmount, uint256 bribeTokenAmount) { |
| 77 | 68× | require(_epoch != governance.epoch(), "BribeInitiative: cannot-claim-for-current-epoch"); |
| 78 | 33× | require(!claimedBribeAtEpoch[_user][_epoch], "BribeInitiative: already-claimed"); |
| 79 | ||
| 80 | 44× | Bribe memory bribe = bribeByEpoch[_epoch]; |
| 81 | 25× | require(bribe.boldAmount != 0 || bribe.bribeTokenAmount != 0, "BribeInitiative: no-bribe"); |
| 82 | ||
| 83 | 0 | DoubleLinkedList.Item memory lqtyAllocation = |
| 84 | 0 | lqtyAllocationByUserAtEpoch[_user].getItem(_prevLQTYAllocationEpoch); |
| 85 | ||
| 86 | 0 | require( |
| 87 | 0 | lqtyAllocation.value != 0 && _prevLQTYAllocationEpoch <= _epoch |
| 88 | 0 | && (lqtyAllocation.next > _epoch || lqtyAllocation.next == 0), |
| 89 | "BribeInitiative: invalid-prev-lqty-allocation-epoch" |
|
| 90 | ); |
|
| 91 | 0 | DoubleLinkedList.Item memory totalLQTYAllocation = |
| 92 | 0 | totalLQTYAllocationByEpoch.getItem(_prevTotalLQTYAllocationEpoch); |
| 93 | 0 | require( |
| 94 | 0 | totalLQTYAllocation.value != 0 && _prevTotalLQTYAllocationEpoch <= _epoch |
| 95 | 0 | && (totalLQTYAllocation.next > _epoch || totalLQTYAllocation.next == 0), |
| 96 | "BribeInitiative: invalid-prev-total-lqty-allocation-epoch" |
|
| 97 | ); |
|
| 98 | ||
| 99 | 0 | boldAmount = uint256(bribe.boldAmount) * uint256(lqtyAllocation.value) / uint256(totalLQTYAllocation.value); |
| 100 | 0 | bribeTokenAmount = |
| 101 | 0 | uint256(bribe.bribeTokenAmount) * uint256(lqtyAllocation.value) / uint256(totalLQTYAllocation.value); |
| 102 | ||
| 103 | 0 | claimedBribeAtEpoch[_user][_epoch] = true; |
| 104 | ||
| 105 | 0 | emit ClaimBribe(_user, _epoch, boldAmount, bribeTokenAmount); |
| 106 | } |
|
| 107 | ||
| 108 | /// @inheritdoc IBribeInitiative |
|
| 109 | 11× | function claimBribes(ClaimData[] calldata _claimData) |
| 110 | external |
|
| 111 | 2× | returns (uint256 boldAmount, uint256 bribeTokenAmount) |
| 112 | { |
|
| 113 | 8× | for (uint256 i = 0; i < _claimData.length; i++) { |
| 114 | 29× | ClaimData memory claimData = _claimData[i]; |
| 115 | 5× | (uint256 boldAmount_, uint256 bribeTokenAmount_) = _claimBribe( |
| 116 | 13× | msg.sender, claimData.epoch, claimData.prevLQTYAllocationEpoch, claimData.prevTotalLQTYAllocationEpoch |
| 117 | ); |
|
| 118 | 0 | boldAmount += boldAmount_; |
| 119 | 0 | bribeTokenAmount += bribeTokenAmount_; |
| 120 | } |
|
| 121 | ||
| 122 | 0 | if (boldAmount != 0) bold.safeTransfer(msg.sender, boldAmount); |
| 123 | 0 | if (bribeTokenAmount != 0) bribeToken.safeTransfer(msg.sender, bribeTokenAmount); |
| 124 | } |
|
| 125 | ||
| 126 | /// @inheritdoc IInitiative |
|
| 127 | function onRegisterInitiative(uint16) external virtual override onlyGovernance {} |
|
| 128 | ||
| 129 | /// @inheritdoc IInitiative |
|
| 130 | 15× | function onUnregisterInitiative(uint16) external virtual override onlyGovernance {} |
| 131 | ||
| 132 | 5× | function _setTotalLQTYAllocationByEpoch(uint16 _epoch, uint88 _value, bool _insert) private { |
| 133 | 8× | if (_insert) { |
| 134 | 8× | totalLQTYAllocationByEpoch.insert(_epoch, _value, 0); |
| 135 | } else { |
|
| 136 | 23× | totalLQTYAllocationByEpoch.items[_epoch].value = _value; |
| 137 | } |
|
| 138 | 12× | emit ModifyTotalLQTYAllocation(_epoch, _value); |
| 139 | } |
|
| 140 | ||
| 141 | 6× | function _setLQTYAllocationByUserAtEpoch(address _user, uint16 _epoch, uint88 _value, bool _insert) private { |
| 142 | 8× | if (_insert) { |
| 143 | 22× | lqtyAllocationByUserAtEpoch[_user].insert(_epoch, _value, 0); |
| 144 | } else { |
|
| 145 | 36× | lqtyAllocationByUserAtEpoch[_user].items[_epoch].value = _value; |
| 146 | } |
|
| 147 | 13× | emit ModifyLQTYAllocation(_user, _epoch, _value); |
| 148 | } |
|
| 149 | ||
| 150 | /// @inheritdoc IInitiative |
|
| 151 | 21× | function onAfterAllocateLQTY(uint16 _currentEpoch, address _user, uint88 _voteLQTY, uint88 _vetoLQTY) |
| 152 | external |
|
| 153 | virtual |
|
| 154 | onlyGovernance |
|
| 155 | 1× | { |
| 156 | 18× | uint16 mostRecentUserEpoch = lqtyAllocationByUserAtEpoch[_user].getHead(); |
| 157 | ||
| 158 | 8× | if (_currentEpoch == 0) return; |
| 159 | ||
| 160 | // if this is the first user allocation in the epoch, then insert a new item into the user allocation DLL |
|
| 161 | 16× | if (mostRecentUserEpoch != _currentEpoch) { |
| 162 | 33× | uint88 prevVoteLQTY = lqtyAllocationByUserAtEpoch[_user].items[mostRecentUserEpoch].value; |
| 163 | 13× | uint88 newVoteLQTY = (_vetoLQTY == 0) ? _voteLQTY : 0; |
| 164 | 2× | uint16 mostRecentTotalEpoch = totalLQTYAllocationByEpoch.getHead(); |
| 165 | // if this is the first allocation in the epoch, then insert a new item into the total allocation DLL |
|
| 166 | 9× | if (mostRecentTotalEpoch != _currentEpoch) { |
| 167 | 19× | uint88 prevTotalLQTYAllocation = totalLQTYAllocationByEpoch.items[mostRecentTotalEpoch].value; |
| 168 | 10× | if (_vetoLQTY == 0) { |
| 169 | // no veto to no veto |
|
| 170 | 4× | _setTotalLQTYAllocationByEpoch( |
| 171 | 15× | _currentEpoch, prevTotalLQTYAllocation + newVoteLQTY - prevVoteLQTY, true |
| 172 | ); |
|
| 173 | } else { |
|
| 174 | 7× | if (prevVoteLQTY != 0) { |
| 175 | // if the prev user allocation was counted in, then remove the prev user allocation from the |
|
| 176 | // total allocation (no veto to veto) |
|
| 177 | 0 | _setTotalLQTYAllocationByEpoch(_currentEpoch, prevTotalLQTYAllocation - prevVoteLQTY, true); |
| 178 | } else { |
|
| 179 | // veto to veto |
|
| 180 | 7× | _setTotalLQTYAllocationByEpoch(_currentEpoch, prevTotalLQTYAllocation, true); |
| 181 | } |
|
| 182 | } |
|
| 183 | } else { |
|
| 184 | 0 | if (_vetoLQTY == 0) { |
| 185 | // no veto to no veto |
|
| 186 | 2× | _setTotalLQTYAllocationByEpoch( |
| 187 | 0 | _currentEpoch, |
| 188 | 7× | totalLQTYAllocationByEpoch.items[_currentEpoch].value + newVoteLQTY - prevVoteLQTY, |
| 189 | 1× | false |
| 190 | ); |
|
| 191 | 0 | } else if (prevVoteLQTY != 0) { |
| 192 | // no veto to veto |
|
| 193 | 1× | _setTotalLQTYAllocationByEpoch( |
| 194 | 0 | _currentEpoch, totalLQTYAllocationByEpoch.items[_currentEpoch].value - prevVoteLQTY, false |
| 195 | ); |
|
| 196 | } |
|
| 197 | } |
|
| 198 | // insert a new item into the user allocation DLL |
|
| 199 | 8× | _setLQTYAllocationByUserAtEpoch(_user, _currentEpoch, newVoteLQTY, true); |
| 200 | 1× | } else { |
| 201 | 19× | uint88 prevVoteLQTY = lqtyAllocationByUserAtEpoch[_user].getItem(_currentEpoch).value; |
| 202 | 9× | if (_vetoLQTY == 0) { |
| 203 | // update the allocation for the current epoch by adding the new allocation and subtracting |
|
| 204 | // the previous one (no veto to no veto) |
|
| 205 | 3× | _setTotalLQTYAllocationByEpoch( |
| 206 | 2× | _currentEpoch, |
| 207 | 24× | totalLQTYAllocationByEpoch.items[_currentEpoch].value + _voteLQTY - prevVoteLQTY, |
| 208 | false |
|
| 209 | ); |
|
| 210 | 8× | _setLQTYAllocationByUserAtEpoch(_user, _currentEpoch, _voteLQTY, false); |
| 211 | } else { |
|
| 212 | // if the user vetoed the initiative, subtract the allocation from the DLLs (no veto to veto) |
|
| 213 | 3× | _setTotalLQTYAllocationByEpoch( |
| 214 | 24× | _currentEpoch, totalLQTYAllocationByEpoch.items[_currentEpoch].value - prevVoteLQTY, false |
| 215 | ); |
|
| 216 | 8× | _setLQTYAllocationByUserAtEpoch(_user, _currentEpoch, 0, false); |
| 217 | } |
|
| 218 | } |
|
| 219 | } |
|
| 220 | ||
| 221 | /// @inheritdoc IInitiative |
|
| 222 | 18× | function onClaimForInitiative(uint16, uint256) external virtual override onlyGovernance {} |
| 223 | } |
0%
src/CurveV2GaugeRewards.sol
Lines covered: 0 / 12 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {ILiquidityGauge} from "./../src/interfaces/ILiquidityGauge.sol"; |
|
| 5 | ||
| 6 | import {BribeInitiative} from "./BribeInitiative.sol"; |
|
| 7 | ||
| 8 | 0 | contract CurveV2GaugeRewards is BribeInitiative { |
| 9 | 0 | ILiquidityGauge public immutable gauge; |
| 10 | 0 | uint256 public immutable duration; |
| 11 | ||
| 12 | event DepositIntoGauge(uint256 amount); |
|
| 13 | ||
| 14 | 0 | constructor(address _governance, address _bold, address _bribeToken, address _gauge, uint256 _duration) |
| 15 | BribeInitiative(_governance, _bold, _bribeToken) |
|
| 16 | { |
|
| 17 | 0 | gauge = ILiquidityGauge(_gauge); |
| 18 | 0 | duration = _duration; |
| 19 | } |
|
| 20 | ||
| 21 | 0 | function depositIntoGauge() external returns (uint256) { |
| 22 | 0 | uint256 amount = governance.claimForInitiative(address(this)); |
| 23 | ||
| 24 | 0 | bold.approve(address(gauge), amount); |
| 25 | 0 | gauge.deposit_reward_token(address(bold), amount, duration); |
| 26 | ||
| 27 | 0 | emit DepositIntoGauge(amount); |
| 28 | ||
| 29 | 0 | return amount; |
| 30 | } |
|
| 31 | } |
0%
src/ForwardBribe.sol
Lines covered: 0 / 10 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; |
|
| 5 | import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; |
|
| 6 | ||
| 7 | import {BribeInitiative} from "./BribeInitiative.sol"; |
|
| 8 | ||
| 9 | 0 | contract ForwardBribe is BribeInitiative { |
| 10 | using SafeERC20 for IERC20; |
|
| 11 | ||
| 12 | 0 | address public immutable receiver; |
| 13 | ||
| 14 | 0 | constructor(address _governance, address _bold, address _bribeToken, address _receiver) |
| 15 | BribeInitiative(_governance, _bold, _bribeToken) |
|
| 16 | { |
|
| 17 | 0 | receiver = _receiver; |
| 18 | } |
|
| 19 | ||
| 20 | 0 | function forwardBribe() external { |
| 21 | 0 | governance.claimForInitiative(address(this)); |
| 22 | ||
| 23 | 0 | uint boldAmount = bold.balanceOf(address(this)); |
| 24 | 0 | uint bribeTokenAmount = bribeToken.balanceOf(address(this)); |
| 25 | ||
| 26 | 0 | if (boldAmount != 0) bold.transfer(receiver, boldAmount); |
| 27 | 0 | if (bribeTokenAmount != 0) bribeToken.transfer(receiver, bribeTokenAmount); |
| 28 | } |
|
| 29 | } |
84%
src/Governance.sol
Lines covered: 243 / 289 (84%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {console} from "forge-std/console.sol"; |
|
| 5 | ||
| 6 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; |
|
| 7 | import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; |
|
| 8 | import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; |
|
| 9 | ||
| 10 | import {IGovernance} from "./interfaces/IGovernance.sol"; |
|
| 11 | import {IInitiative} from "./interfaces/IInitiative.sol"; |
|
| 12 | import {ILQTYStaking} from "./interfaces/ILQTYStaking.sol"; |
|
| 13 | ||
| 14 | import {UserProxy} from "./UserProxy.sol"; |
|
| 15 | import {UserProxyFactory} from "./UserProxyFactory.sol"; |
|
| 16 | ||
| 17 | import {add, max, abs} from "./utils/Math.sol"; |
|
| 18 | import {Multicall} from "./utils/Multicall.sol"; |
|
| 19 | import {WAD, PermitParams} from "./utils/Types.sol"; |
|
| 20 | ||
| 21 | import {SafeCastLib} from "lib/solmate/src/utils/SafeCastLib.sol"; |
|
| 22 | ||
| 23 | /// @title Governance: Modular Initiative based Governance |
|
| 24 | 224× | contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance { |
| 25 | using SafeERC20 for IERC20; |
|
| 26 | ||
| 27 | /// @inheritdoc IGovernance |
|
| 28 | 0 | ILQTYStaking public immutable stakingV1; |
| 29 | /// @inheritdoc IGovernance |
|
| 30 | 0 | IERC20 public immutable lqty; |
| 31 | /// @inheritdoc IGovernance |
|
| 32 | 0 | IERC20 public immutable bold; |
| 33 | /// @inheritdoc IGovernance |
|
| 34 | 0 | uint256 public immutable EPOCH_START; |
| 35 | /// @inheritdoc IGovernance |
|
| 36 | 0 | uint256 public immutable EPOCH_DURATION; |
| 37 | /// @inheritdoc IGovernance |
|
| 38 | 19× | uint256 public immutable EPOCH_VOTING_CUTOFF; |
| 39 | /// @inheritdoc IGovernance |
|
| 40 | 0 | uint256 public immutable MIN_CLAIM; |
| 41 | /// @inheritdoc IGovernance |
|
| 42 | 0 | uint256 public immutable MIN_ACCRUAL; |
| 43 | /// @inheritdoc IGovernance |
|
| 44 | 0 | uint256 public immutable REGISTRATION_FEE; |
| 45 | /// @inheritdoc IGovernance |
|
| 46 | 0 | uint256 public immutable REGISTRATION_THRESHOLD_FACTOR; |
| 47 | /// @inheritdoc IGovernance |
|
| 48 | 0 | uint256 public immutable UNREGISTRATION_THRESHOLD_FACTOR; |
| 49 | /// @inheritdoc IGovernance |
|
| 50 | 0 | uint256 public immutable REGISTRATION_WARM_UP_PERIOD; |
| 51 | /// @inheritdoc IGovernance |
|
| 52 | 0 | uint256 public immutable UNREGISTRATION_AFTER_EPOCHS; |
| 53 | /// @inheritdoc IGovernance |
|
| 54 | 0 | uint256 public immutable VOTING_THRESHOLD_FACTOR; |
| 55 | ||
| 56 | /// @inheritdoc IGovernance |
|
| 57 | 0 | uint256 public boldAccrued; |
| 58 | ||
| 59 | /// @inheritdoc IGovernance |
|
| 60 | 0 | VoteSnapshot public votesSnapshot; |
| 61 | /// @inheritdoc IGovernance |
|
| 62 | 0 | mapping(address => InitiativeVoteSnapshot) public votesForInitiativeSnapshot; |
| 63 | ||
| 64 | /// @inheritdoc IGovernance |
|
| 65 | 0 | GlobalState public globalState; |
| 66 | /// @inheritdoc IGovernance |
|
| 67 | 0 | mapping(address => UserState) public userStates; |
| 68 | /// @inheritdoc IGovernance |
|
| 69 | 0 | mapping(address => InitiativeState) public initiativeStates; |
| 70 | /// @inheritdoc IGovernance |
|
| 71 | 0 | mapping(address => mapping(address => Allocation)) public lqtyAllocatedByUserToInitiative; |
| 72 | /// @inheritdoc IGovernance |
|
| 73 | 8× | mapping(address => uint16) public override registeredInitiatives; |
| 74 | ||
| 75 | 3× | uint16 constant UNREGISTERED_INITIATIVE = type(uint16).max; |
| 76 | ||
| 77 | 0 | constructor( |
| 78 | address _lqty, |
|
| 79 | address _lusd, |
|
| 80 | address _stakingV1, |
|
| 81 | address _bold, |
|
| 82 | Configuration memory _config, |
|
| 83 | address[] memory _initiatives |
|
| 84 | 0 | ) UserProxyFactory(_lqty, _lusd, _stakingV1) { |
| 85 | 0 | stakingV1 = ILQTYStaking(_stakingV1); |
| 86 | 0 | lqty = IERC20(_lqty); |
| 87 | 0 | bold = IERC20(_bold); |
| 88 | 0 | require(_config.minClaim <= _config.minAccrual, "Gov: min-claim-gt-min-accrual"); |
| 89 | 0 | REGISTRATION_FEE = _config.registrationFee; |
| 90 | 0 | REGISTRATION_THRESHOLD_FACTOR = _config.registrationThresholdFactor; |
| 91 | 0 | UNREGISTRATION_THRESHOLD_FACTOR = _config.unregistrationThresholdFactor; |
| 92 | 0 | REGISTRATION_WARM_UP_PERIOD = _config.registrationWarmUpPeriod; |
| 93 | 0 | UNREGISTRATION_AFTER_EPOCHS = _config.unregistrationAfterEpochs; |
| 94 | 0 | VOTING_THRESHOLD_FACTOR = _config.votingThresholdFactor; |
| 95 | 0 | MIN_CLAIM = _config.minClaim; |
| 96 | 0 | MIN_ACCRUAL = _config.minAccrual; |
| 97 | 0 | EPOCH_START = _config.epochStart; |
| 98 | 0 | require(_config.epochDuration > 0, "Gov: epoch-duration-zero"); |
| 99 | 0 | EPOCH_DURATION = _config.epochDuration; |
| 100 | 0 | require(_config.epochVotingCutoff < _config.epochDuration, "Gov: epoch-voting-cutoff-gt-epoch-duration"); |
| 101 | 0 | EPOCH_VOTING_CUTOFF = _config.epochVotingCutoff; |
| 102 | 0 | for (uint256 i = 0; i < _initiatives.length; i++) { |
| 103 | 0 | initiativeStates[_initiatives[i]] = InitiativeState(0, 0, 0, 0, 0); |
| 104 | 0 | registeredInitiatives[_initiatives[i]] = 1; |
| 105 | } |
|
| 106 | } |
|
| 107 | ||
| 108 | 2× | function _averageAge(uint32 _currentTimestamp, uint32 _averageTimestamp) internal pure returns (uint32) { |
| 109 | 23× | if (_averageTimestamp == 0 || _currentTimestamp < _averageTimestamp) return 0; |
| 110 | 5× | return _currentTimestamp - _averageTimestamp; |
| 111 | } |
|
| 112 | ||
| 113 | 9× | function _calculateAverageTimestamp( |
| 114 | uint32 _prevOuterAverageTimestamp, |
|
| 115 | uint32 _newInnerAverageTimestamp, |
|
| 116 | uint88 _prevLQTYBalance, |
|
| 117 | uint88 _newLQTYBalance |
|
| 118 | 1× | ) internal view returns (uint32) { |
| 119 | 11× | if (_newLQTYBalance == 0) return 0; |
| 120 | ||
| 121 | 9× | uint32 prevOuterAverageAge = _averageAge(uint32(block.timestamp), _prevOuterAverageTimestamp); |
| 122 | 9× | uint32 newInnerAverageAge = _averageAge(uint32(block.timestamp), _newInnerAverageTimestamp); |
| 123 | ||
| 124 | 1× | uint88 newOuterAverageAge; |
| 125 | 17× | if (_prevLQTYBalance <= _newLQTYBalance) { |
| 126 | 8× | uint88 deltaLQTY = _newLQTYBalance - _prevLQTYBalance; |
| 127 | 12× | uint240 prevVotes = uint240(_prevLQTYBalance) * uint240(prevOuterAverageAge); |
| 128 | 13× | uint240 newVotes = uint240(deltaLQTY) * uint240(newInnerAverageAge); |
| 129 | 7× | uint240 votes = prevVotes + newVotes; |
| 130 | 21× | newOuterAverageAge = (_newLQTYBalance == 0) ? 0 : uint32(votes / uint240(_newLQTYBalance)); |
| 131 | 4× | } else { |
| 132 | 8× | uint88 deltaLQTY = _prevLQTYBalance - _newLQTYBalance; |
| 133 | 12× | uint240 prevVotes = uint240(_prevLQTYBalance) * uint240(prevOuterAverageAge); |
| 134 | 13× | uint240 newVotes = uint240(deltaLQTY) * uint240(newInnerAverageAge); |
| 135 | 19× | uint240 votes = (prevVotes >= newVotes) ? prevVotes - newVotes : 0; |
| 136 | 21× | newOuterAverageAge = (_newLQTYBalance == 0) ? 0 : uint32(votes / uint240(_newLQTYBalance)); |
| 137 | } |
|
| 138 | ||
| 139 | 17× | if (newOuterAverageAge > block.timestamp) return 0; |
| 140 | 13× | return uint32(block.timestamp - newOuterAverageAge); |
| 141 | } |
|
| 142 | ||
| 143 | /*////////////////////////////////////////////////////////////// |
|
| 144 | STAKING |
|
| 145 | //////////////////////////////////////////////////////////////*/ |
|
| 146 | ||
| 147 | 4× | function _deposit(uint88 _lqtyAmount) private returns (UserProxy) { |
| 148 | 17× | require(_lqtyAmount > 0, "Governance: zero-lqty-amount"); |
| 149 | ||
| 150 | 8× | address userProxyAddress = deriveUserProxyAddress(msg.sender); |
| 151 | ||
| 152 | 9× | if (userProxyAddress.code.length == 0) { |
| 153 | 0 | deployUserProxy(); |
| 154 | } |
|
| 155 | ||
| 156 | 4× | UserProxy userProxy = UserProxy(payable(userProxyAddress)); |
| 157 | ||
| 158 | 62× | uint88 lqtyStaked = uint88(stakingV1.stakes(userProxyAddress)); |
| 159 | ||
| 160 | // update the average staked timestamp for LQTY staked by the user |
|
| 161 | 41× | UserState memory userState = userStates[msg.sender]; |
| 162 | 14× | userState.averageStakingTimestamp = _calculateAverageTimestamp( |
| 163 | 5× | userState.averageStakingTimestamp, uint32(block.timestamp), lqtyStaked, lqtyStaked + _lqtyAmount |
| 164 | ); |
|
| 165 | 40× | userStates[msg.sender] = userState; |
| 166 | ||
| 167 | 11× | emit DepositLQTY(msg.sender, _lqtyAmount); |
| 168 | ||
| 169 | 2× | return userProxy; |
| 170 | } |
|
| 171 | ||
| 172 | /// @inheritdoc IGovernance |
|
| 173 | 18× | function depositLQTY(uint88 _lqtyAmount) external nonReentrant { |
| 174 | 8× | UserProxy userProxy = _deposit(_lqtyAmount); |
| 175 | 52× | userProxy.stake(_lqtyAmount, msg.sender); |
| 176 | } |
|
| 177 | ||
| 178 | /// @inheritdoc IGovernance |
|
| 179 | 0 | function depositLQTYViaPermit(uint88 _lqtyAmount, PermitParams calldata _permitParams) external nonReentrant { |
| 180 | 0 | UserProxy userProxy = _deposit(_lqtyAmount); |
| 181 | 0 | userProxy.stakeViaPermit(_lqtyAmount, msg.sender, _permitParams); |
| 182 | } |
|
| 183 | ||
| 184 | /// @inheritdoc IGovernance |
|
| 185 | 22× | function withdrawLQTY(uint88 _lqtyAmount) external nonReentrant { |
| 186 | 8× | UserProxy userProxy = UserProxy(payable(deriveUserProxyAddress(msg.sender))); |
| 187 | 9× | require(address(userProxy).code.length != 0, "Governance: user-proxy-not-deployed"); |
| 188 | ||
| 189 | 65× | uint88 lqtyStaked = uint88(stakingV1.stakes(address(userProxy))); |
| 190 | ||
| 191 | 12× | UserState storage userState = userStates[msg.sender]; |
| 192 | ||
| 193 | // check if user has enough unallocated lqty |
|
| 194 | 28× | require(_lqtyAmount <= lqtyStaked - userState.allocatedLQTY, "Governance: insufficient-unallocated-lqty"); |
| 195 | ||
| 196 | 67× | (uint256 accruedLUSD, uint256 accruedETH) = userProxy.unstake(_lqtyAmount, msg.sender, msg.sender); |
| 197 | ||
| 198 | 13× | emit WithdrawLQTY(msg.sender, _lqtyAmount, accruedLUSD, accruedETH); |
| 199 | } |
|
| 200 | ||
| 201 | /// @inheritdoc IGovernance |
|
| 202 | 28× | function claimFromStakingV1(address _rewardRecipient) external returns (uint256 accruedLUSD, uint256 accruedETH) { |
| 203 | 8× | address payable userProxyAddress = payable(deriveUserProxyAddress(msg.sender)); |
| 204 | 9× | require(userProxyAddress.code.length != 0, "Governance: user-proxy-not-deployed"); |
| 205 | 65× | return UserProxy(userProxyAddress).unstake(0, _rewardRecipient, _rewardRecipient); |
| 206 | } |
|
| 207 | ||
| 208 | /*////////////////////////////////////////////////////////////// |
|
| 209 | VOTING |
|
| 210 | //////////////////////////////////////////////////////////////*/ |
|
| 211 | ||
| 212 | /// @inheritdoc IGovernance |
|
| 213 | 26× | function epoch() public view returns (uint16) { |
| 214 | 14× | if (block.timestamp < EPOCH_START) return 0; |
| 215 | 36× | return uint16(((block.timestamp - EPOCH_START) / EPOCH_DURATION) + 1); |
| 216 | } |
|
| 217 | ||
| 218 | /// @inheritdoc IGovernance |
|
| 219 | 4× | function epochStart() public view returns (uint32) { |
| 220 | 7× | uint16 currentEpoch = epoch(); |
| 221 | 8× | if (currentEpoch == 0) return 0; |
| 222 | 24× | return uint32(EPOCH_START + (currentEpoch - 1) * EPOCH_DURATION); |
| 223 | } |
|
| 224 | ||
| 225 | /// @inheritdoc IGovernance |
|
| 226 | 6× | function secondsWithinEpoch() public view returns (uint32) { |
| 227 | 7× | if (block.timestamp < EPOCH_START) return 0; |
| 228 | 18× | return uint32((block.timestamp - EPOCH_START) % EPOCH_DURATION); |
| 229 | } |
|
| 230 | ||
| 231 | /// @inheritdoc IGovernance |
|
| 232 | 8× | function lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) |
| 233 | public |
|
| 234 | pure |
|
| 235 | 1× | returns (uint240) |
| 236 | { |
|
| 237 | 18× | return uint240(_lqtyAmount) * _averageAge(uint32(_currentTimestamp), _averageTimestamp); |
| 238 | } |
|
| 239 | ||
| 240 | /// @inheritdoc IGovernance |
|
| 241 | 7× | function calculateVotingThreshold() public view returns (uint256) { |
| 242 | 4× | uint256 snapshotVotes = votesSnapshot.votes; |
| 243 | 10× | if (snapshotVotes == 0) return 0; |
| 244 | ||
| 245 | 1× | uint256 minVotes; // to reach MIN_CLAIM: snapshotVotes * MIN_CLAIM / boldAccrued |
| 246 | 17× | uint256 payoutPerVote = boldAccrued * WAD / snapshotVotes; |
| 247 | 5× | if (payoutPerVote != 0) { |
| 248 | 14× | minVotes = MIN_CLAIM * WAD / payoutPerVote; |
| 249 | } |
|
| 250 | 22× | return max(snapshotVotes * VOTING_THRESHOLD_FACTOR / WAD, minVotes); |
| 251 | } |
|
| 252 | ||
| 253 | // Snapshots votes for the previous epoch and accrues funds for the current epoch |
|
| 254 | 5× | function _snapshotVotes() internal returns (VoteSnapshot memory snapshot, GlobalState memory state) { |
| 255 | 7× | uint16 currentEpoch = epoch(); |
| 256 | 30× | snapshot = votesSnapshot; |
| 257 | 25× | state = globalState; |
| 258 | 18× | if (snapshot.forEpoch < currentEpoch - 1) { |
| 259 | 21× | snapshot.votes = lqtyToVotes(state.countedVoteLQTY, epochStart(), state.countedVoteLQTYAverageTimestamp); |
| 260 | 14× | snapshot.forEpoch = currentEpoch - 1; |
| 261 | 13× | votesSnapshot = snapshot; |
| 262 | 61× | uint256 boldBalance = bold.balanceOf(address(this)); |
| 263 | 13× | boldAccrued = (boldBalance < MIN_ACCRUAL) ? 0 : boldBalance; |
| 264 | 22× | emit SnapshotVotes(snapshot.votes, snapshot.forEpoch); |
| 265 | } |
|
| 266 | } |
|
| 267 | ||
| 268 | // Snapshots votes for an initiative for the previous epoch but only count the votes |
|
| 269 | // if the received votes meet the voting threshold |
|
| 270 | 5× | function _snapshotVotesForInitiative(address _initiative) |
| 271 | internal |
|
| 272 | returns (InitiativeVoteSnapshot memory initiativeSnapshot, InitiativeState memory initiativeState) |
|
| 273 | 1× | { |
| 274 | 8× | uint16 currentEpoch = epoch(); |
| 275 | 70× | initiativeSnapshot = votesForInitiativeSnapshot[_initiative]; |
| 276 | 78× | initiativeState = initiativeStates[_initiative]; |
| 277 | 22× | if (initiativeSnapshot.forEpoch < currentEpoch - 1) { |
| 278 | 7× | uint256 votingThreshold = calculateVotingThreshold(); |
| 279 | 7× | uint32 start = epochStart(); |
| 280 | 3× | uint240 votes = |
| 281 | 15× | lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.averageStakingTimestampVoteLQTY); |
| 282 | 2× | uint240 vetos = |
| 283 | 15× | lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY); |
| 284 | // if the votes didn't meet the voting threshold then no votes qualify |
|
| 285 | /// @audit TODO TEST THIS |
|
| 286 | /// The change means that all logic for votes and rewards must be done in `getInitiativeState` |
|
| 287 | 6× | initiativeSnapshot.votes = uint224(votes); /// @audit TODO: We should change this to check the treshold, we should instead use the snapshot to just report all the valid data |
| 288 | ||
| 289 | 6× | initiativeSnapshot.vetos = uint224(vetos); /// @audit TODO: Overflow + order of operations |
| 290 | ||
| 291 | 12× | initiativeSnapshot.forEpoch = currentEpoch - 1; |
| 292 | ||
| 293 | /// @audit Conditional |
|
| 294 | /// If we meet the threshold then we increase this |
|
| 295 | /// TODO: Either simplify, or use this for the state machine as well |
|
| 296 | 4× | if( |
| 297 | 18× | initiativeSnapshot.votes > initiativeSnapshot.vetos && |
| 298 | 6× | initiativeSnapshot.votes >= votingThreshold |
| 299 | ) { |
|
| 300 | 12× | initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; /// @audit This updating makes it so that we lose track | TODO: Find a better way |
| 301 | } |
|
| 302 | ||
| 303 | 88× | votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; |
| 304 | 11× | emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch); |
| 305 | } |
|
| 306 | } |
|
| 307 | ||
| 308 | /// @inheritdoc IGovernance |
|
| 309 | 26× | function snapshotVotesForInitiative(address _initiative) |
| 310 | external |
|
| 311 | nonReentrant |
|
| 312 | returns (VoteSnapshot memory voteSnapshot, InitiativeVoteSnapshot memory initiativeVoteSnapshot) |
|
| 313 | { |
|
| 314 | 5× | (voteSnapshot,) = _snapshotVotes(); |
| 315 | 6× | (initiativeVoteSnapshot,) = _snapshotVotesForInitiative(_initiative); |
| 316 | } |
|
| 317 | ||
| 318 | ||
| 319 | /// @notice Given an initiative, return whether the initiative will be unregisted, whether it can claim and which epoch it last claimed at |
|
| 320 | enum InitiativeStatus { |
|
| 321 | SKIP, /// This epoch will result in no rewards and no unregistering |
|
| 322 | CLAIMABLE, /// This epoch will result in claiming rewards |
|
| 323 | CLAIMED, /// The rewards for this epoch have been claimed |
|
| 324 | UNREGISTERABLE, /// Can be unregistered |
|
| 325 | DISABLED // It was already Unregistered |
|
| 326 | } |
|
| 327 | /** |
|
| 328 | FSM: |
|
| 329 | - Can claim (false, true, epoch - 1 - X) |
|
| 330 | - Has claimed (false, false, epoch - 1) |
|
| 331 | - Cannot claim and should not be kicked (false, false, epoch - 1 - [0, X]) |
|
| 332 | - Should be kicked (true, false, epoch - 1 - [UNREGISTRATION_AFTER_EPOCHS, UNREGISTRATION_AFTER_EPOCHS + X]) |
|
| 333 | */ |
|
| 334 | ||
| 335 | 37× | function getInitiativeState(address _initiative) public returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { |
| 336 | 8× | (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); |
| 337 | 10× | (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); |
| 338 | ||
| 339 | 20× | lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; |
| 340 | ||
| 341 | // == Already Claimed Condition == // |
|
| 342 | 20× | if(lastEpochClaim >= epoch() - 1) { |
| 343 | // early return, we have already claimed |
|
| 344 | 8× | return (InitiativeStatus.CLAIMED, lastEpochClaim, claimableAmount); |
| 345 | } |
|
| 346 | ||
| 347 | // == Disabled Condition == // |
|
| 348 | // TODO: If a initiative is disabled, we return false and the last epoch claim |
|
| 349 | 20× | if(registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { |
| 350 | 7× | return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// @audit By definition it must have zero rewards |
| 351 | } |
|
| 352 | ||
| 353 | ||
| 354 | // == Unregister Condition == // |
|
| 355 | /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case | TODO: Double check | Worst case QA, off by one epoch |
|
| 356 | // TODO: IMO we can use the claimed variable here |
|
| 357 | /// This shifts the logic by 1 epoch |
|
| 358 | 35× | if((votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) |
| 359 | 19× | || votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes |
| 360 | 24× | && votesForInitiativeSnapshot_.vetos > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD |
| 361 | ) { |
|
| 362 | 7× | return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0); |
| 363 | } |
|
| 364 | ||
| 365 | // How do we know that they have canClaimRewards? |
|
| 366 | // They must have votes / totalVotes AND meet the Requirement AND not be vetoed |
|
| 367 | /// @audit if we already are above, then why are we re-computing this? |
|
| 368 | // Ultimately the checkpoint logic for initiative is fine, so we can skip this |
|
| 369 | ||
| 370 | // TODO: Where does this fit exactly? |
|
| 371 | // Edge case of 0 votes |
|
| 372 | 9× | if(votesSnapshot_.votes == 0) { |
| 373 | 7× | return (InitiativeStatus.SKIP, lastEpochClaim, 0); |
| 374 | } |
|
| 375 | ||
| 376 | ||
| 377 | // == Vetoed this Epoch Condition == // |
|
| 378 | 16× | if(votesForInitiativeSnapshot_.vetos >= votesForInitiativeSnapshot_.votes) { |
| 379 | 7× | return (InitiativeStatus.SKIP, lastEpochClaim, 0); /// @audit Technically VETOED |
| 380 | } |
|
| 381 | ||
| 382 | // == Not meeting threshold Condition == // |
|
| 383 | ||
| 384 | 13× | if(calculateVotingThreshold() > votesForInitiativeSnapshot_.votes) { |
| 385 | 7× | return (InitiativeStatus.SKIP, lastEpochClaim, 0); |
| 386 | } |
|
| 387 | ||
| 388 | // == Rewards Conditions (votes can be zero, logic is the same) == // |
|
| 389 | 25× | uint256 claim = votesForInitiativeSnapshot_.votes * boldAccrued / votesSnapshot_.votes; |
| 390 | 2× | return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim); |
| 391 | } |
|
| 392 | ||
| 393 | /// @inheritdoc IGovernance |
|
| 394 | 21× | function registerInitiative(address _initiative) external nonReentrant { |
| 395 | 10× | bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE); |
| 396 | ||
| 397 | 6× | require(_initiative != address(0), "Governance: zero-address"); |
| 398 | 29× | require(registeredInitiatives[_initiative] == 0, "Governance: initiative-already-registered"); |
| 399 | ||
| 400 | 8× | address userProxyAddress = deriveUserProxyAddress(msg.sender); |
| 401 | 5× | (VoteSnapshot memory snapshot,) = _snapshotVotes(); |
| 402 | 39× | UserState memory userState = userStates[msg.sender]; |
| 403 | ||
| 404 | // an initiative can be registered if the registrant has more voting power (LQTY * age) |
|
| 405 | // than the registration threshold derived from the previous epoch's total global votes |
|
| 406 | 12× | require( |
| 407 | 74× | lqtyToVotes(uint88(stakingV1.stakes(userProxyAddress)), block.timestamp, userState.averageStakingTimestamp) |
| 408 | 19× | >= snapshot.votes * REGISTRATION_THRESHOLD_FACTOR / WAD, |
| 409 | "Governance: insufficient-lqty" |
|
| 410 | ); |
|
| 411 | ||
| 412 | 5× | uint16 currentEpoch = epoch(); |
| 413 | ||
| 414 | 32× | registeredInitiatives[_initiative] = currentEpoch; |
| 415 | ||
| 416 | 12× | emit RegisterInitiative(_initiative, msg.sender, currentEpoch); |
| 417 | ||
| 418 | 43× | try IInitiative(_initiative).onRegisterInitiative(currentEpoch) {} catch {} |
| 419 | } |
|
| 420 | ||
| 421 | /// @inheritdoc IGovernance |
|
| 422 | 24× | function allocateLQTY( |
| 423 | address[] calldata _initiatives, |
|
| 424 | int88[] calldata _deltaLQTYVotes, |
|
| 425 | int88[] calldata _deltaLQTYVetos |
|
| 426 | ) external nonReentrant { |
|
| 427 | 12× | require( |
| 428 | 11× | _initiatives.length == _deltaLQTYVotes.length && _initiatives.length == _deltaLQTYVetos.length, |
| 429 | "Governance: array-length-mismatch" |
|
| 430 | ); |
|
| 431 | ||
| 432 | 8× | (, GlobalState memory state) = _snapshotVotes(); |
| 433 | ||
| 434 | 7× | uint256 votingThreshold = calculateVotingThreshold(); |
| 435 | 7× | uint16 currentEpoch = epoch(); |
| 436 | ||
| 437 | 40× | UserState memory userState = userStates[msg.sender]; |
| 438 | ||
| 439 | 16× | for (uint256 i = 0; i < _initiatives.length; i++) { |
| 440 | 27× | address initiative = _initiatives[i]; |
| 441 | 27× | int88 deltaLQTYVotes = _deltaLQTYVotes[i]; |
| 442 | 27× | int88 deltaLQTYVetos = _deltaLQTYVetos[i]; |
| 443 | ||
| 444 | // TODO: Better assertion |
|
| 445 | /// Can remove or add |
|
| 446 | /// But cannot add or remove both |
|
| 447 | ||
| 448 | // only allow vetoing post the voting cutoff |
|
| 449 | 12× | require( |
| 450 | 31× | deltaLQTYVotes <= 0 || deltaLQTYVotes >= 0 && secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF, |
| 451 | "Governance: epoch-voting-cutoff" |
|
| 452 | ); |
|
| 453 | ||
| 454 | 1× | { |
| 455 | 17× | uint16 registeredAtEpoch = registeredInitiatives[initiative]; |
| 456 | 18× | if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { |
| 457 | 29× | require(currentEpoch > registeredAtEpoch && registeredAtEpoch != 0, "Governance: initiative-not-active"); |
| 458 | } /// @audit TODO: We must allow removals for Proposals that are disabled | Should use the flag u16 |
|
| 459 | ||
| 460 | 6× | if(registeredAtEpoch == UNREGISTERED_INITIATIVE) { |
| 461 | 21× | require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); |
| 462 | } |
|
| 463 | } |
|
| 464 | // TODO: CHANGE |
|
| 465 | // Can add if active |
|
| 466 | // Can remove if inactive |
|
| 467 | // only allow allocations to initiatives that are active |
|
| 468 | // an initiative becomes active in the epoch after it is registered |
|
| 469 | ||
| 470 | ||
| 471 | 9× | (, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); |
| 472 | ||
| 473 | // deep copy of the initiative's state before the allocation |
|
| 474 | 40× | InitiativeState memory prevInitiativeState = InitiativeState( |
| 475 | 4× | initiativeState.voteLQTY, |
| 476 | 4× | initiativeState.vetoLQTY, |
| 477 | 4× | initiativeState.averageStakingTimestampVoteLQTY, |
| 478 | 4× | initiativeState.averageStakingTimestampVetoLQTY, |
| 479 | 4× | initiativeState.lastEpochClaim |
| 480 | ); |
|
| 481 | ||
| 482 | // update the average staking timestamp for the initiative based on the user's average staking timestamp |
|
| 483 | 8× | initiativeState.averageStakingTimestampVoteLQTY = _calculateAverageTimestamp( |
| 484 | 4× | initiativeState.averageStakingTimestampVoteLQTY, |
| 485 | 4× | userState.averageStakingTimestamp, |
| 486 | 4× | initiativeState.voteLQTY, |
| 487 | 8× | add(initiativeState.voteLQTY, deltaLQTYVotes) |
| 488 | ); |
|
| 489 | 9× | initiativeState.averageStakingTimestampVetoLQTY = _calculateAverageTimestamp( |
| 490 | 5× | initiativeState.averageStakingTimestampVetoLQTY, |
| 491 | 6× | userState.averageStakingTimestamp, |
| 492 | 5× | initiativeState.vetoLQTY, |
| 493 | 4× | add(initiativeState.vetoLQTY, deltaLQTYVetos) |
| 494 | ); |
|
| 495 | ||
| 496 | // allocate the voting and vetoing LQTY to the initiative |
|
| 497 | 12× | initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes); |
| 498 | 21× | initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos); |
| 499 | ||
| 500 | // update the initiative's state |
|
| 501 | 100× | initiativeStates[initiative] = initiativeState; |
| 502 | ||
| 503 | // update the average staking timestamp for all counted voting LQTY |
|
| 504 | 11× | state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( |
| 505 | 3× | state.countedVoteLQTYAverageTimestamp, |
| 506 | initiativeState.averageStakingTimestampVoteLQTY, |
|
| 507 | 4× | state.countedVoteLQTY, |
| 508 | 6× | state.countedVoteLQTY - prevInitiativeState.voteLQTY |
| 509 | ); |
|
| 510 | 15× | state.countedVoteLQTY -= prevInitiativeState.voteLQTY; |
| 511 | ||
| 512 | 11× | state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( |
| 513 | 5× | state.countedVoteLQTYAverageTimestamp, |
| 514 | 5× | initiativeState.averageStakingTimestampVoteLQTY, |
| 515 | 4× | state.countedVoteLQTY, |
| 516 | 6× | state.countedVoteLQTY + initiativeState.voteLQTY |
| 517 | ); |
|
| 518 | 18× | state.countedVoteLQTY += initiativeState.voteLQTY; |
| 519 | ||
| 520 | ||
| 521 | ||
| 522 | // allocate the voting and vetoing LQTY to the initiative |
|
| 523 | 67× | Allocation memory allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative]; |
| 524 | 10× | allocation.voteLQTY = add(allocation.voteLQTY, deltaLQTYVotes); |
| 525 | 18× | allocation.vetoLQTY = add(allocation.vetoLQTY, deltaLQTYVetos); |
| 526 | 7× | allocation.atEpoch = currentEpoch; |
| 527 | 31× | require(!(allocation.voteLQTY != 0 && allocation.vetoLQTY != 0), "Governance: vote-and-veto"); |
| 528 | 73× | lqtyAllocatedByUserToInitiative[msg.sender][initiative] = allocation; |
| 529 | ||
| 530 | 17× | userState.allocatedLQTY = add(userState.allocatedLQTY, deltaLQTYVotes + deltaLQTYVetos); |
| 531 | ||
| 532 | 14× | emit AllocateLQTY(msg.sender, initiative, deltaLQTYVotes, deltaLQTYVetos, currentEpoch); |
| 533 | ||
| 534 | 46× | try IInitiative(initiative).onAfterAllocateLQTY( |
| 535 | 7× | currentEpoch, msg.sender, allocation.voteLQTY, allocation.vetoLQTY |
| 536 | ) {} catch {} |
|
| 537 | } |
|
| 538 | ||
| 539 | 12× | require( |
| 540 | 10× | userState.allocatedLQTY == 0 |
| 541 | 76× | || userState.allocatedLQTY <= uint88(stakingV1.stakes(deriveUserProxyAddress(msg.sender))), |
| 542 | "Governance: insufficient-or-unallocated-lqty" |
|
| 543 | ); |
|
| 544 | ||
| 545 | 32× | globalState = state; |
| 546 | 39× | userStates[msg.sender] = userState; |
| 547 | } |
|
| 548 | ||
| 549 | /// @inheritdoc IGovernance |
|
| 550 | 27× | function unregisterInitiative(address _initiative) external nonReentrant { |
| 551 | 17× | uint16 registrationEpoch = registeredInitiatives[_initiative]; |
| 552 | 22× | require(registrationEpoch != 0, "Governance: initiative-not-registered"); |
| 553 | 7× | uint16 currentEpoch = epoch(); |
| 554 | 26× | require(registrationEpoch + REGISTRATION_WARM_UP_PERIOD < currentEpoch, "Governance: initiative-in-warm-up"); |
| 555 | ||
| 556 | 8× | (, GlobalState memory state) = _snapshotVotes(); |
| 557 | 6× | (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = |
| 558 | 5× | _snapshotVotesForInitiative(_initiative); |
| 559 | ||
| 560 | /// Invariant: Must only claim once or unregister |
|
| 561 | 26× | require(initiativeState.lastEpochClaim < epoch() - 1); |
| 562 | ||
| 563 | 8× | (InitiativeStatus status, , )= getInitiativeState(_initiative); |
| 564 | 23× | require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); |
| 565 | ||
| 566 | /// @audit TODO: Verify that the FSM here is correct |
|
| 567 | ||
| 568 | // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in |
|
| 569 | 13× | state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( |
| 570 | 5× | state.countedVoteLQTYAverageTimestamp, |
| 571 | 5× | initiativeState.averageStakingTimestampVoteLQTY, |
| 572 | 4× | state.countedVoteLQTY, |
| 573 | 7× | state.countedVoteLQTY - initiativeState.voteLQTY |
| 574 | ); |
|
| 575 | 18× | state.countedVoteLQTY -= initiativeState.voteLQTY; |
| 576 | 32× | globalState = state; |
| 577 | ||
| 578 | /// @audit removal math causes issues |
|
| 579 | // delete initiativeStates[_initiative]; |
|
| 580 | ||
| 581 | /// @audit Should not delete this |
|
| 582 | /// weeks * 2^16 > u32 so the contract will stop working before this is an issue |
|
| 583 | 24× | registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE; |
| 584 | ||
| 585 | 11× | emit UnregisterInitiative(_initiative, currentEpoch); |
| 586 | ||
| 587 | 46× | try IInitiative(_initiative).onUnregisterInitiative(currentEpoch) {} catch {} |
| 588 | } |
|
| 589 | ||
| 590 | /// @inheritdoc IGovernance |
|
| 591 | 22× | function claimForInitiative(address _initiative) external nonReentrant returns (uint256) { |
| 592 | 8× | (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); |
| 593 | 11× | (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); |
| 594 | ||
| 595 | // TODO: Return type from state FSM can be standardized |
|
| 596 | 11× | (InitiativeStatus status, , uint256 claimableAmount ) = getInitiativeState(_initiative); |
| 597 | ||
| 598 | /// @audit Return 0 if we cannot claim |
|
| 599 | /// INVARIANT: |
|
| 600 | /// We cannot claim only for 2 reasons: |
|
| 601 | /// We have already claimed |
|
| 602 | /// We do not meet the threshold |
|
| 603 | /// TODO: Enforce this with assertions |
|
| 604 | 14× | if(status != InitiativeStatus.CLAIMABLE) { |
| 605 | 10× | return 0; |
| 606 | } |
|
| 607 | ||
| 608 | 23× | assert(votesSnapshot_.forEpoch == epoch() - 1); /// @audit INVARIANT: You can only claim for previous epoch |
| 609 | /// All unclaimed rewards are always recycled |
|
| 610 | ||
| 611 | 48× | initiativeStates[_initiative].lastEpochClaim = epoch() - 1; |
| 612 | 76× | votesForInitiativeSnapshot[_initiative] = votesForInitiativeSnapshot_; // implicitly prevents double claiming |
| 613 | ||
| 614 | 8× | bold.safeTransfer(_initiative, claimableAmount); |
| 615 | ||
| 616 | 17× | emit ClaimForInitiative(_initiative, claimableAmount, votesSnapshot_.forEpoch); |
| 617 | ||
| 618 | 50× | try IInitiative(_initiative).onClaimForInitiative(votesSnapshot_.forEpoch, claimableAmount) {} catch {} |
| 619 | ||
| 620 | 1× | return claimableAmount; |
| 621 | } |
|
| 622 | } |
0%
src/UniV4Donations.sol
Lines covered: 0 / 67 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; |
|
| 5 | import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; |
|
| 6 | ||
| 7 | import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; |
|
| 8 | import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; |
|
| 9 | import {PoolKey} from "v4-core/src/types/PoolKey.sol"; |
|
| 10 | import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; |
|
| 11 | import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; |
|
| 12 | import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; |
|
| 13 | ||
| 14 | import {BaseHook, Hooks} from "./utils/BaseHook.sol"; |
|
| 15 | import {BribeInitiative} from "./BribeInitiative.sol"; |
|
| 16 | ||
| 17 | 0 | contract UniV4Donations is BribeInitiative, BaseHook { |
| 18 | using SafeERC20 for IERC20; |
|
| 19 | using CurrencyLibrary for Currency; |
|
| 20 | using PoolIdLibrary for PoolKey; |
|
| 21 | ||
| 22 | event DonateToPool(uint256 amount); |
|
| 23 | event RestartVesting(uint16 epoch, uint240 amount); |
|
| 24 | ||
| 25 | 0 | uint256 public immutable VESTING_EPOCH_START; |
| 26 | 0 | uint256 public immutable VESTING_EPOCH_DURATION; |
| 27 | ||
| 28 | address private immutable currency0; |
|
| 29 | address private immutable currency1; |
|
| 30 | uint24 private immutable fee; |
|
| 31 | int24 private immutable tickSpacing; |
|
| 32 | ||
| 33 | struct Vesting { |
|
| 34 | uint240 amount; |
|
| 35 | uint16 epoch; |
|
| 36 | uint256 released; |
|
| 37 | } |
|
| 38 | ||
| 39 | 0 | Vesting public vesting; |
| 40 | ||
| 41 | 0 | constructor( |
| 42 | address _governance, |
|
| 43 | address _bold, |
|
| 44 | address _bribeToken, |
|
| 45 | uint256 _vestingEpochStart, |
|
| 46 | uint256 _vestingEpochDuration, |
|
| 47 | address _poolManager, |
|
| 48 | address _token, |
|
| 49 | uint24 _fee, |
|
| 50 | int24 _tickSpacing |
|
| 51 | 0 | ) BribeInitiative(_governance, _bold, _bribeToken) BaseHook(IPoolManager(_poolManager)) { |
| 52 | 0 | VESTING_EPOCH_START = _vestingEpochStart; |
| 53 | 0 | VESTING_EPOCH_DURATION = _vestingEpochDuration; |
| 54 | ||
| 55 | 0 | if (uint256(uint160(address(_bold))) <= uint256(uint160(address(_token)))) { |
| 56 | 0 | currency0 = _bold; |
| 57 | 0 | currency1 = _token; |
| 58 | } else { |
|
| 59 | 0 | currency1 = _token; |
| 60 | 0 | currency0 = _bold; |
| 61 | } |
|
| 62 | 0 | fee = _fee; |
| 63 | 0 | tickSpacing = _tickSpacing; |
| 64 | } |
|
| 65 | ||
| 66 | 0 | function vestingEpoch() public view returns (uint16) { |
| 67 | 0 | return uint16(((block.timestamp - VESTING_EPOCH_START) / VESTING_EPOCH_DURATION)) + 1; |
| 68 | } |
|
| 69 | ||
| 70 | 0 | function vestingEpochStart() public view returns (uint256) { |
| 71 | 0 | return VESTING_EPOCH_START + ((vestingEpoch() - 1) * VESTING_EPOCH_DURATION); |
| 72 | } |
|
| 73 | ||
| 74 | 0 | function _restartVesting(uint240 claimed) internal returns (Vesting memory) { |
| 75 | 0 | uint16 epoch = vestingEpoch(); |
| 76 | 0 | Vesting memory _vesting = vesting; |
| 77 | 0 | if (_vesting.epoch < epoch) { |
| 78 | 0 | _vesting.amount = claimed + _vesting.amount - uint240(_vesting.released); // roll over unclaimed amount |
| 79 | 0 | _vesting.epoch = epoch; |
| 80 | 0 | _vesting.released = 0; |
| 81 | 0 | vesting = _vesting; |
| 82 | 0 | emit RestartVesting(epoch, _vesting.amount); |
| 83 | } |
|
| 84 | 0 | return _vesting; |
| 85 | } |
|
| 86 | ||
| 87 | 0 | function _donateToPool() internal returns (uint256) { |
| 88 | 0 | Vesting memory _vesting = _restartVesting(uint240(governance.claimForInitiative(address(this)))); |
| 89 | 0 | uint256 amount = |
| 90 | 0 | (_vesting.amount * (block.timestamp - vestingEpochStart()) / VESTING_EPOCH_DURATION) - _vesting.released; |
| 91 | ||
| 92 | 0 | if (amount != 0) { |
| 93 | 0 | PoolKey memory key = poolKey(); |
| 94 | ||
| 95 | 0 | manager.donate(key, amount, 0, bytes("")); |
| 96 | 0 | manager.sync(key.currency0); |
| 97 | 0 | IERC20(Currency.unwrap(key.currency0)).safeTransfer(address(manager), amount); |
| 98 | 0 | manager.settle(key.currency0); |
| 99 | ||
| 100 | 0 | vesting.released += amount; |
| 101 | ||
| 102 | 0 | emit DonateToPool(amount); |
| 103 | } |
|
| 104 | ||
| 105 | 0 | return amount; |
| 106 | } |
|
| 107 | ||
| 108 | 0 | function donateToPool() public returns (uint256) { |
| 109 | 0 | return abi.decode(manager.unlock(abi.encode(address(this), poolKey())), (uint256)); |
| 110 | } |
|
| 111 | ||
| 112 | 0 | function poolKey() public view returns (PoolKey memory key) { |
| 113 | 0 | key = PoolKey({ |
| 114 | 0 | currency0: Currency.wrap(currency0), |
| 115 | 0 | currency1: Currency.wrap(currency1), |
| 116 | 0 | fee: fee, |
| 117 | 0 | tickSpacing: tickSpacing, |
| 118 | 0 | hooks: IHooks(address(this)) |
| 119 | }); |
|
| 120 | } |
|
| 121 | ||
| 122 | 0 | function getHookPermissions() public pure override returns (Hooks.Permissions memory) { |
| 123 | 0 | return Hooks.Permissions({ |
| 124 | beforeInitialize: false, |
|
| 125 | afterInitialize: true, |
|
| 126 | beforeAddLiquidity: false, |
|
| 127 | beforeRemoveLiquidity: false, |
|
| 128 | afterAddLiquidity: true, |
|
| 129 | afterRemoveLiquidity: false, |
|
| 130 | beforeSwap: false, |
|
| 131 | afterSwap: false, |
|
| 132 | beforeDonate: false, |
|
| 133 | afterDonate: false, |
|
| 134 | beforeSwapReturnDelta: false, |
|
| 135 | afterSwapReturnDelta: false, |
|
| 136 | afterAddLiquidityReturnDelta: false, |
|
| 137 | afterRemoveLiquidityReturnDelta: false |
|
| 138 | }); |
|
| 139 | } |
|
| 140 | ||
| 141 | 0 | function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) |
| 142 | external |
|
| 143 | view |
|
| 144 | override |
|
| 145 | onlyByManager |
|
| 146 | 0 | returns (bytes4) |
| 147 | { |
|
| 148 | 0 | require(PoolId.unwrap(poolKey().toId()) == PoolId.unwrap(key.toId()), "UniV4Donations: invalid-pool-id"); |
| 149 | 0 | return this.afterInitialize.selector; |
| 150 | } |
|
| 151 | ||
| 152 | 0 | function afterAddLiquidity( |
| 153 | address, |
|
| 154 | PoolKey calldata key, |
|
| 155 | IPoolManager.ModifyLiquidityParams calldata, |
|
| 156 | BalanceDelta delta, |
|
| 157 | bytes calldata |
|
| 158 | 0 | ) external override onlyByManager returns (bytes4, BalanceDelta) { |
| 159 | 0 | require(PoolId.unwrap(poolKey().toId()) == PoolId.unwrap(key.toId()), "UniV4Donations: invalid-pool-id"); |
| 160 | 0 | _donateToPool(); |
| 161 | 0 | return (this.afterAddLiquidity.selector, delta); |
| 162 | } |
|
| 163 | ||
| 164 | 0 | function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { |
| 165 | 0 | (address sender, PoolKey memory key) = abi.decode(data, (address, PoolKey)); |
| 166 | 0 | require(sender == address(this), "UniV4Donations: invalid-sender"); |
| 167 | 0 | require(PoolId.unwrap(poolKey().toId()) == PoolId.unwrap(key.toId()), "UniV4Donations: invalid-pool-id"); |
| 168 | 0 | return abi.encode(_donateToPool()); |
| 169 | } |
|
| 170 | } |
65%
src/UserProxy.sol
Lines covered: 27 / 41 (65%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; |
|
| 5 | import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; |
|
| 6 | import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; |
|
| 7 | ||
| 8 | import {IUserProxy} from "./interfaces/IUserProxy.sol"; |
|
| 9 | import {ILQTYStaking} from "./interfaces/ILQTYStaking.sol"; |
|
| 10 | import {PermitParams} from "./utils/Types.sol"; |
|
| 11 | ||
| 12 | 167× | contract UserProxy is IUserProxy { |
| 13 | using SafeERC20 for IERC20; |
|
| 14 | ||
| 15 | /// @inheritdoc IUserProxy |
|
| 16 | 0 | IERC20 public immutable lqty; |
| 17 | /// @inheritdoc IUserProxy |
|
| 18 | 0 | IERC20 public immutable lusd; |
| 19 | ||
| 20 | /// @inheritdoc IUserProxy |
|
| 21 | 0 | ILQTYStaking public immutable stakingV1; |
| 22 | /// @inheritdoc IUserProxy |
|
| 23 | 0 | address public immutable stakingV2; |
| 24 | ||
| 25 | 27× | constructor(address _lqty, address _lusd, address _stakingV1) { |
| 26 | 5× | lqty = IERC20(_lqty); |
| 27 | 5× | lusd = IERC20(_lusd); |
| 28 | 3× | stakingV1 = ILQTYStaking(_stakingV1); |
| 29 | 3× | stakingV2 = msg.sender; |
| 30 | } |
|
| 31 | ||
| 32 | modifier onlyStakingV2() { |
|
| 33 | 16× | require(msg.sender == stakingV2, "UserProxy: caller-not-stakingV2"); |
| 34 | _; |
|
| 35 | } |
|
| 36 | ||
| 37 | /// @inheritdoc IUserProxy |
|
| 38 | 20× | function stake(uint256 _amount, address _lqtyFrom) public onlyStakingV2 { |
| 39 | 67× | lqty.transferFrom(_lqtyFrom, address(this), _amount); |
| 40 | 60× | lqty.approve(address(stakingV1), _amount); |
| 41 | 40× | stakingV1.stake(_amount); |
| 42 | 12× | emit Stake(_amount, _lqtyFrom); |
| 43 | } |
|
| 44 | ||
| 45 | /// @inheritdoc IUserProxy |
|
| 46 | 6× | function stakeViaPermit(uint256 _amount, address _lqtyFrom, PermitParams calldata _permitParams) |
| 47 | public |
|
| 48 | onlyStakingV2 |
|
| 49 | { |
|
| 50 | 0 | require(_lqtyFrom == _permitParams.owner, "UserProxy: owner-not-sender"); |
| 51 | 0 | try IERC20Permit(address(lqty)).permit( |
| 52 | 0 | _permitParams.owner, |
| 53 | 0 | _permitParams.spender, |
| 54 | 0 | _permitParams.value, |
| 55 | 0 | _permitParams.deadline, |
| 56 | 0 | _permitParams.v, |
| 57 | 0 | _permitParams.r, |
| 58 | 0 | _permitParams.s |
| 59 | ) {} catch {} |
|
| 60 | 1× | stake(_amount, _lqtyFrom); |
| 61 | } |
|
| 62 | ||
| 63 | /// @inheritdoc IUserProxy |
|
| 64 | 29× | function unstake(uint256 _amount, address _lqtyRecipient, address _lusdEthRecipient) |
| 65 | public |
|
| 66 | onlyStakingV2 |
|
| 67 | 2× | returns (uint256 lusdAmount, uint256 ethAmount) |
| 68 | 1× | { |
| 69 | 40× | stakingV1.unstake(_amount); |
| 70 | ||
| 71 | 62× | uint256 lqtyAmount = lqty.balanceOf(address(this)); |
| 72 | 13× | if (lqtyAmount > 0) lqty.safeTransfer(_lqtyRecipient, lqtyAmount); |
| 73 | 60× | lusdAmount = lusd.balanceOf(address(this)); |
| 74 | 5× | if (lusdAmount > 0) lusd.safeTransfer(_lusdEthRecipient, lusdAmount); |
| 75 | 2× | ethAmount = address(this).balance; |
| 76 | 5× | if (ethAmount > 0) { |
| 77 | 0 | (bool success,) = payable(_lusdEthRecipient).call{value: ethAmount}(""); |
| 78 | success; |
|
| 79 | } |
|
| 80 | ||
| 81 | 13× | emit Unstake(_amount, _lqtyRecipient, _lusdEthRecipient, lusdAmount, ethAmount); |
| 82 | } |
|
| 83 | ||
| 84 | /// @inheritdoc IUserProxy |
|
| 85 | 35× | function staked() external view returns (uint88) { |
| 86 | 61× | return uint88(stakingV1.stakes(address(this))); |
| 87 | } |
|
| 88 | ||
| 89 | receive() external payable {} |
|
| 90 | } |
40%
src/UserProxyFactory.sol
Lines covered: 4 / 10 (40%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {Clones} from "openzeppelin-contracts/contracts/proxy/Clones.sol"; |
|
| 5 | ||
| 6 | import {IUserProxyFactory} from "./interfaces/IUserProxyFactory.sol"; |
|
| 7 | import {UserProxy} from "./UserProxy.sol"; |
|
| 8 | ||
| 9 | 0 | contract UserProxyFactory is IUserProxyFactory { |
| 10 | /// @inheritdoc IUserProxyFactory |
|
| 11 | 0 | address public immutable userProxyImplementation; |
| 12 | ||
| 13 | 0 | constructor(address _lqty, address _lusd, address _stakingV1) { |
| 14 | 0 | userProxyImplementation = address(new UserProxy(_lqty, _lusd, _stakingV1)); |
| 15 | } |
|
| 16 | ||
| 17 | /// @inheritdoc IUserProxyFactory |
|
| 18 | 22× | function deriveUserProxyAddress(address _user) public view returns (address) { |
| 19 | 11× | return Clones.predictDeterministicAddress(userProxyImplementation, bytes32(uint256(uint160(_user)))); |
| 20 | } |
|
| 21 | ||
| 22 | /// @inheritdoc IUserProxyFactory |
|
| 23 | 18× | function deployUserProxy() public returns (address) { |
| 24 | // reverts if the user already has a proxy |
|
| 25 | 5× | address userProxy = Clones.cloneDeterministic(userProxyImplementation, bytes32(uint256(uint160(msg.sender)))); |
| 26 | ||
| 27 | 0 | emit DeployUserProxy(msg.sender, userProxy); |
| 28 | ||
| 29 | 0 | return userProxy; |
| 30 | } |
|
| 31 | } |
0%
src/utils/BaseHook.sol
Lines covered: 0 / 22 (0%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {Hooks} from "v4-core/src/libraries/Hooks.sol"; |
|
| 5 | import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; |
|
| 6 | import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; |
|
| 7 | import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; |
|
| 8 | import {PoolKey} from "v4-core/src/types/PoolKey.sol"; |
|
| 9 | import {BeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol"; |
|
| 10 | import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol"; |
|
| 11 | ||
| 12 | 0 | contract ImmutableState { |
| 13 | 0 | IPoolManager public immutable manager; |
| 14 | ||
| 15 | 0 | constructor(IPoolManager _manager) { |
| 16 | 0 | manager = _manager; |
| 17 | } |
|
| 18 | } |
|
| 19 | ||
| 20 | abstract contract SafeCallback is ImmutableState, IUnlockCallback { |
|
| 21 | error NotManager(); |
|
| 22 | ||
| 23 | modifier onlyByManager() { |
|
| 24 | 0 | if (msg.sender != address(manager)) revert NotManager(); |
| 25 | _; |
|
| 26 | } |
|
| 27 | ||
| 28 | /// @dev We force the onlyByManager modifier by exposing a virtual function after the onlyByManager check. |
|
| 29 | 0 | function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { |
| 30 | 0 | return _unlockCallback(data); |
| 31 | } |
|
| 32 | ||
| 33 | function _unlockCallback(bytes calldata data) internal virtual returns (bytes memory); |
|
| 34 | } |
|
| 35 | ||
| 36 | abstract contract BaseHook is IHooks, SafeCallback { |
|
| 37 | error NotSelf(); |
|
| 38 | error InvalidPool(); |
|
| 39 | error LockFailure(); |
|
| 40 | error HookNotImplemented(); |
|
| 41 | ||
| 42 | constructor(IPoolManager _manager) ImmutableState(_manager) { |
|
| 43 | 0 | validateHookAddress(this); |
| 44 | } |
|
| 45 | ||
| 46 | /// @dev Only this address may call this function |
|
| 47 | modifier selfOnly() { |
|
| 48 | if (msg.sender != address(this)) revert NotSelf(); |
|
| 49 | _; |
|
| 50 | } |
|
| 51 | ||
| 52 | /// @dev Only pools with hooks set to this contract may call this function |
|
| 53 | modifier onlyValidPools(IHooks hooks) { |
|
| 54 | if (hooks != this) revert InvalidPool(); |
|
| 55 | _; |
|
| 56 | } |
|
| 57 | ||
| 58 | function getHookPermissions() public pure virtual returns (Hooks.Permissions memory); |
|
| 59 | ||
| 60 | // this function is virtual so that we can override it during testing, |
|
| 61 | // which allows us to deploy an implementation to any address |
|
| 62 | // and then etch the bytecode into the correct address |
|
| 63 | 0 | function validateHookAddress(BaseHook _this) internal pure virtual { |
| 64 | 0 | Hooks.validateHookPermissions(_this, getHookPermissions()); |
| 65 | } |
|
| 66 | ||
| 67 | function _unlockCallback(bytes calldata data) internal virtual override returns (bytes memory) { |
|
| 68 | (bool success, bytes memory returnData) = address(this).call(data); |
|
| 69 | if (success) return returnData; |
|
| 70 | if (returnData.length == 0) revert LockFailure(); |
|
| 71 | // if the call failed, bubble up the reason |
|
| 72 | /// @solidity memory-safe-assembly |
|
| 73 | assembly { |
|
| 74 | revert(add(returnData, 32), mload(returnData)) |
|
| 75 | } |
|
| 76 | } |
|
| 77 | ||
| 78 | 0 | function beforeInitialize(address, PoolKey calldata, uint160, bytes calldata) external virtual returns (bytes4) { |
| 79 | revert HookNotImplemented(); |
|
| 80 | } |
|
| 81 | ||
| 82 | function afterInitialize(address, PoolKey calldata, uint160, int24, bytes calldata) |
|
| 83 | external |
|
| 84 | virtual |
|
| 85 | returns (bytes4) |
|
| 86 | { |
|
| 87 | revert HookNotImplemented(); |
|
| 88 | } |
|
| 89 | ||
| 90 | function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) |
|
| 91 | external |
|
| 92 | virtual |
|
| 93 | returns (bytes4) |
|
| 94 | { |
|
| 95 | revert HookNotImplemented(); |
|
| 96 | } |
|
| 97 | ||
| 98 | 0 | function beforeRemoveLiquidity( |
| 99 | address, |
|
| 100 | PoolKey calldata, |
|
| 101 | IPoolManager.ModifyLiquidityParams calldata, |
|
| 102 | bytes calldata |
|
| 103 | 0 | ) external virtual returns (bytes4) { |
| 104 | 0 | revert HookNotImplemented(); |
| 105 | } |
|
| 106 | ||
| 107 | function afterAddLiquidity( |
|
| 108 | address, |
|
| 109 | PoolKey calldata, |
|
| 110 | IPoolManager.ModifyLiquidityParams calldata, |
|
| 111 | BalanceDelta, |
|
| 112 | bytes calldata |
|
| 113 | ) external virtual returns (bytes4, BalanceDelta) { |
|
| 114 | revert HookNotImplemented(); |
|
| 115 | } |
|
| 116 | ||
| 117 | 0 | function afterRemoveLiquidity( |
| 118 | address, |
|
| 119 | PoolKey calldata, |
|
| 120 | IPoolManager.ModifyLiquidityParams calldata, |
|
| 121 | BalanceDelta, |
|
| 122 | bytes calldata |
|
| 123 | 0 | ) external virtual returns (bytes4, BalanceDelta) { |
| 124 | 0 | revert HookNotImplemented(); |
| 125 | } |
|
| 126 | ||
| 127 | 0 | function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) |
| 128 | external |
|
| 129 | virtual |
|
| 130 | 0 | returns (bytes4, BeforeSwapDelta, uint24) |
| 131 | { |
|
| 132 | 0 | revert HookNotImplemented(); |
| 133 | } |
|
| 134 | ||
| 135 | 0 | function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) |
| 136 | external |
|
| 137 | virtual |
|
| 138 | returns (bytes4, int128) |
|
| 139 | { |
|
| 140 | revert HookNotImplemented(); |
|
| 141 | } |
|
| 142 | ||
| 143 | 0 | function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) |
| 144 | external |
|
| 145 | virtual |
|
| 146 | returns (bytes4) |
|
| 147 | { |
|
| 148 | revert HookNotImplemented(); |
|
| 149 | } |
|
| 150 | ||
| 151 | function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) |
|
| 152 | external |
|
| 153 | virtual |
|
| 154 | returns (bytes4) |
|
| 155 | { |
|
| 156 | revert HookNotImplemented(); |
|
| 157 | } |
|
| 158 | } |
68%
src/utils/DoubleLinkedList.sol
Lines covered: 15 / 22 (68%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | /// @title DoubleLinkedList |
|
| 5 | /// @notice Implements a double linked list where the head is defined as the null item's prev pointer |
|
| 6 | /// and the tail is defined as the null item's next pointer ([tail][prev][item][next][head]) |
|
| 7 | 0 | library DoubleLinkedList { |
| 8 | struct Item { |
|
| 9 | uint88 value; |
|
| 10 | uint16 prev; |
|
| 11 | uint16 next; |
|
| 12 | } |
|
| 13 | ||
| 14 | struct List { |
|
| 15 | mapping(uint16 => Item) items; |
|
| 16 | } |
|
| 17 | ||
| 18 | error IdIsZero(); |
|
| 19 | error ItemNotInList(); |
|
| 20 | error ItemInList(); |
|
| 21 | ||
| 22 | /// @notice Returns the head item id of the list |
|
| 23 | /// @param list Linked list which contains the item |
|
| 24 | /// @return _ Id of the head item |
|
| 25 | function getHead(List storage list) internal view returns (uint16) { |
|
| 26 | 28× | return list.items[0].prev; |
| 27 | } |
|
| 28 | ||
| 29 | /// @notice Returns the tail item id of the list |
|
| 30 | /// @param list Linked list which contains the item |
|
| 31 | /// @return _ Id of the tail item |
|
| 32 | function getTail(List storage list) internal view returns (uint16) { |
|
| 33 | 0 | return list.items[0].next; |
| 34 | } |
|
| 35 | ||
| 36 | /// @notice Returns the item id which follows item `id`. Returns the head item id of the list if the `id` is 0. |
|
| 37 | /// @param list Linked list which contains the items |
|
| 38 | /// @param id Id of the current item |
|
| 39 | /// @return _ Id of the current item's next item |
|
| 40 | 0 | function getNext(List storage list, uint16 id) internal view returns (uint16) { |
| 41 | 0 | return list.items[id].next; |
| 42 | } |
|
| 43 | ||
| 44 | /// @notice Returns the item id which precedes item `id`. Returns the tail item id of the list if the `id` is 0. |
|
| 45 | /// @param list Linked list which contains the items |
|
| 46 | /// @param id Id of the current item |
|
| 47 | /// @return _ Id of the current item's previous item |
|
| 48 | function getPrev(List storage list, uint16 id) internal view returns (uint16) { |
|
| 49 | 0 | return list.items[id].prev; |
| 50 | } |
|
| 51 | ||
| 52 | /// @notice Returns the value of item `id` |
|
| 53 | /// @param list Linked list which contains the item |
|
| 54 | /// @param id Id of the item |
|
| 55 | /// @return _ Value of the item |
|
| 56 | 0 | function getValue(List storage list, uint16 id) internal view returns (uint88) { |
| 57 | 27× | return list.items[id].value; |
| 58 | } |
|
| 59 | ||
| 60 | /// @notice Returns the item `id` |
|
| 61 | /// @param list Linked list which contains the item |
|
| 62 | /// @param id Id of the item |
|
| 63 | /// @return _ Item |
|
| 64 | 0 | function getItem(List storage list, uint16 id) internal view returns (Item memory) { |
| 65 | 46× | return list.items[id]; |
| 66 | } |
|
| 67 | ||
| 68 | /// @notice Returns whether the list contains item `id` |
|
| 69 | /// @param list Linked list which should contain the item |
|
| 70 | /// @param id Id of the item to check |
|
| 71 | /// @return _ True if the list contains the item, false otherwise |
|
| 72 | 7× | function contains(List storage list, uint16 id) internal view returns (bool) { |
| 73 | 8× | if (id == 0) revert IdIsZero(); |
| 74 | 76× | return (list.items[id].prev != 0 || list.items[id].next != 0 || list.items[0].next == id); |
| 75 | } |
|
| 76 | ||
| 77 | /// @notice Inserts an item with `id` in the list before item `next` |
|
| 78 | /// - if `next` is 0, the item is inserted at the start (head) of the list |
|
| 79 | /// @dev This function should not be called with an `id` that is already in the list. |
|
| 80 | /// @param list Linked list which contains the next item and into which the new item will be inserted |
|
| 81 | /// @param id Id of the item to insert |
|
| 82 | /// @param value Value of the item to insert |
|
| 83 | /// @param next Id of the item which should follow item `id` |
|
| 84 | 2× | function insert(List storage list, uint16 id, uint88 value, uint16 next) internal { |
| 85 | 10× | if (contains(list, id)) revert ItemInList(); |
| 86 | 14× | if (next != 0 && !contains(list, next)) revert ItemNotInList(); |
| 87 | 26× | uint16 prev = list.items[next].prev; |
| 88 | 27× | list.items[prev].next = id; |
| 89 | 11× | list.items[next].prev = id; |
| 90 | 13× | list.items[id].prev = prev; |
| 91 | 15× | list.items[id].next = next; |
| 92 | 10× | list.items[id].value = value; |
| 93 | } |
|
| 94 | } |
100%
src/utils/Math.sol
Lines covered: 8 / 8 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | 10× | function add(uint88 a, int88 b) pure returns (uint88) { |
| 5 | 18× | if (b < 0) { |
| 6 | 30× | return a - abs(b); |
| 7 | } |
|
| 8 | 23× | return a + abs(b); |
| 9 | } |
|
| 10 | ||
| 11 | 4× | function max(uint256 a, uint256 b) pure returns (uint256) { |
| 12 | 11× | return a > b ? a : b; |
| 13 | } |
|
| 14 | ||
| 15 | 4× | function abs(int88 a) pure returns (uint88) { |
| 16 | 36× | return a < 0 ? uint88(uint256(-int256(a))) : uint88(a); |
| 17 | } |
22%
src/utils/Multicall.sol
Lines covered: 2 / 9 (22%)
| 1 | // SPDX-License-Identifier: GPL-2.0-or-later |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {IMulticall} from "../interfaces/IMulticall.sol"; |
|
| 5 | ||
| 6 | /// Copied from: https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/Multicall.sol |
|
| 7 | /// @title Multicall |
|
| 8 | /// @notice Enables calling multiple methods in a single call to the contract |
|
| 9 | abstract contract Multicall is IMulticall { |
|
| 10 | /// @inheritdoc IMulticall |
|
| 11 | 5× | function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) { |
| 12 | 0 | results = new bytes[](data.length); |
| 13 | 2× | for (uint256 i = 0; i < data.length; i++) { |
| 14 | 0 | (bool success, bytes memory result) = address(this).delegatecall(data[i]); |
| 15 | ||
| 16 | 0 | if (!success) { |
| 17 | // Next 5 lines from https://ethereum.stackexchange.com/a/83577 |
|
| 18 | 0 | if (result.length < 68) revert(); |
| 19 | assembly { |
|
| 20 | 0 | result := add(result, 0x04) |
| 21 | } |
|
| 22 | 0 | revert(abi.decode(result, (string))); |
| 23 | } |
|
| 24 | ||
| 25 | 0 | results[i] = result; |
| 26 | } |
|
| 27 | } |
|
| 28 | } |
100%
src/utils/Types.sol
Lines covered: 1 / 1 (100%)
| 1 | // SPDX-License-Identifier: MIT |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | struct PermitParams { |
|
| 5 | address owner; |
|
| 6 | address spender; |
|
| 7 | uint256 value; |
|
| 8 | uint256 deadline; |
|
| 9 | uint8 v; |
|
| 10 | bytes32 r; |
|
| 11 | bytes32 s; |
|
| 12 | } |
|
| 13 | ||
| 14 | 6× | uint256 constant WAD = 1e18; |
12%
test/mocks/MockERC20Tester.sol
Lines covered: 1 / 8 (12%)
| 1 | // SPDX-License-Identifier: GPL-2.0 |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | import {MockERC20} from "forge-std/mocks/MockERC20.sol"; |
|
| 5 | ||
| 6 | 279× | contract MockERC20Tester is MockERC20 { |
| 7 | address owner; |
|
| 8 | ||
| 9 | modifier onlyOwner() { |
|
| 10 | 0 | require(msg.sender == owner); |
| 11 | _; |
|
| 12 | } |
|
| 13 | ||
| 14 | 0 | constructor(address recipient, uint256 mintAmount, string memory name, string memory symbol, uint8 decimals) { |
| 15 | 0 | super.initialize(name, symbol, decimals); |
| 16 | 0 | _mint(recipient, mintAmount); |
| 17 | ||
| 18 | 0 | owner = msg.sender; |
| 19 | } |
|
| 20 | ||
| 21 | 0 | function mint(address to, uint256 amount) public onlyOwner { |
| 22 | 0 | _mint(to, amount); |
| 23 | } |
|
| 24 | } |
90%
test/mocks/MockStakingV1.sol
Lines covered: 10 / 11 (90%)
| 1 | // SPDX-License-Identifier: UNLICENSED |
|
| 2 | pragma solidity ^0.8.24; |
|
| 3 | ||
| 4 | import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; |
|
| 5 | ||
| 6 | 115× | contract MockStakingV1 { |
| 7 | 0 | IERC20 public immutable lqty; |
| 8 | ||
| 9 | 66× | mapping(address => uint256) public stakes; |
| 10 | ||
| 11 | 27× | constructor(address _lqty) { |
| 12 | 3× | lqty = IERC20(_lqty); |
| 13 | } |
|
| 14 | ||
| 15 | 11× | function stake(uint256 _LQTYamount) external { |
| 16 | 27× | stakes[msg.sender] += _LQTYamount; |
| 17 | 17× | lqty.transferFrom(msg.sender, address(this), _LQTYamount); |
| 18 | } |
|
| 19 | ||
| 20 | 15× | function unstake(uint256 _LQTYamount) external { |
| 21 | 27× | stakes[msg.sender] -= _LQTYamount; |
| 22 | 62× | lqty.transfer(msg.sender, _LQTYamount); |
| 23 | } |
|
| 24 | } |
100%
test/recon/BeforeAfter.sol
Lines covered: 22 / 22 (100%)
| 1 | ||
| 2 | // SPDX-License-Identifier: GPL-2.0 |
|
| 3 | pragma solidity ^0.8.0; |
|
| 4 | ||
| 5 | import {Asserts} from "@chimera/Asserts.sol"; |
|
| 6 | import {Setup} from "./Setup.sol"; |
|
| 7 | import {IGovernance} from "../../src/interfaces/IGovernance.sol"; |
|
| 8 | import {IBribeInitiative} from "../../src/interfaces/IBribeInitiative.sol"; |
|
| 9 | import {Governance} from "../../src/Governance.sol"; |
|
| 10 | ||
| 11 | ||
| 12 | abstract contract BeforeAfter is Setup, Asserts { |
|
| 13 | struct Vars { |
|
| 14 | uint16 epoch; |
|
| 15 | mapping(address => Governance.InitiativeStatus) initiativeStatus; |
|
| 16 | // initiative => user => epoch => claimed |
|
| 17 | mapping(address => mapping(address => mapping(uint16 => bool))) claimedBribeForInitiativeAtEpoch; |
|
| 18 | uint128 lqtyBalance; |
|
| 19 | uint128 lusdBalance; |
|
| 20 | } |
|
| 21 | ||
| 22 | Vars internal _before; |
|
| 23 | Vars internal _after; |
|
| 24 | ||
| 25 | modifier withChecks { |
|
| 26 | 60× | __before(); |
| 27 | _; |
|
| 28 | 28× | __after(); |
| 29 | } |
|
| 30 | ||
| 31 | 2× | function __before() internal { |
| 32 | 70× | uint16 currentEpoch = governance.epoch(); |
| 33 | 12× | _before.epoch = currentEpoch; |
| 34 | 22× | for(uint8 i; i < deployedInitiatives.length; i++) { |
| 35 | 27× | address initiative = deployedInitiatives[i]; |
| 36 | 69× | (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); |
| 37 | 29× | _before.initiativeStatus[initiative] = status; |
| 38 | 119× | _before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] = IBribeInitiative(initiative).claimedBribeAtEpoch(user, currentEpoch); |
| 39 | } |
|
| 40 | ||
| 41 | 81× | _before.lqtyBalance = uint128(lqty.balanceOf(user)); |
| 42 | 82× | _before.lusdBalance = uint128(lusd.balanceOf(user)); |
| 43 | } |
|
| 44 | ||
| 45 | 2× | function __after() internal { |
| 46 | 70× | uint16 currentEpoch = governance.epoch(); |
| 47 | 12× | _after.epoch = currentEpoch; |
| 48 | 22× | for(uint8 i; i < deployedInitiatives.length; i++) { |
| 49 | 27× | address initiative = deployedInitiatives[i]; |
| 50 | 69× | (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); |
| 51 | 29× | _after.initiativeStatus[initiative] = status; |
| 52 | 119× | _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] = IBribeInitiative(initiative).claimedBribeAtEpoch(user, currentEpoch); |
| 53 | } |
|
| 54 | ||
| 55 | 81× | _after.lqtyBalance = uint128(lqty.balanceOf(user)); |
| 56 | 82× | _after.lusdBalance = uint128(lusd.balanceOf(user)); |
| 57 | } |
|
| 58 | } |
100%
test/recon/CryticTester.sol
Lines covered: 2 / 2 (100%)
| 1 | // SPDX-License-Identifier: GPL-2.0 |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | import {TargetFunctions} from "./TargetFunctions.sol"; |
|
| 5 | import {CryticAsserts} from "@chimera/CryticAsserts.sol"; |
|
| 6 | ||
| 7 | // echidna . --contract CryticTester --config echidna.yaml |
|
| 8 | // medusa fuzz |
|
| 9 | 372× | contract CryticTester is TargetFunctions, CryticAsserts { |
| 10 | constructor() payable { |
|
| 11 | 4× | setup(); |
| 12 | } |
|
| 13 | } |
63%
test/recon/Properties.sol
Lines covered: 14 / 22 (63%)
| 1 | // SPDX-License-Identifier: GPL-2.0 |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | import {GovernanceProperties} from "./properties/GovernanceProperties.sol"; |
|
| 5 | import {BribeInitiativeProperties} from "./properties/BribeInitiativeProperties.sol"; |
|
| 6 | ||
| 7 | abstract contract Properties is GovernanceProperties, BribeInitiativeProperties { |
|
| 8 | string internal constant ASSERTION_CANARY = "!!! canary assertion"; |
|
| 9 | string internal constant ASSERTION_GV01 = |
|
| 10 | "!!! GV-01: Initiative state should only return one state per epoch"; |
|
| 11 | string internal constant ASSERTION_BI01_LQTY = |
|
| 12 | "!!! BI-01: User should receive percentage of bribes corresponding to their allocation"; |
|
| 13 | string internal constant ASSERTION_BI01_BOLD = |
|
| 14 | "!!! BI-01: User should receive percentage of BOLD bribes corresponding to their allocation"; |
|
| 15 | string internal constant ASSERTION_BI02 = |
|
| 16 | "!!! BI-02: User can only claim bribes once in an epoch"; |
|
| 17 | string internal constant ASSERTION_BI03 = |
|
| 18 | "!!! BI-03: Accounting for user allocation amount is always correct"; |
|
| 19 | string internal constant ASSERTION_BI04 = |
|
| 20 | "!!! BI-04: Accounting for total allocation amount is always correct"; |
|
| 21 | string internal constant ASSERTION_BI05_BRIBE_DUST = |
|
| 22 | "!!! BI-05: Bribe token dust amount remaining after claiming should be less than 100 million wei"; |
|
| 23 | string internal constant ASSERTION_BI05_BOLD_DUST = |
|
| 24 | "!!! BI-05: Bold token dust amount remaining after claiming should be less than 100 million wei"; |
|
| 25 | string internal constant INVARIANT_CANARY_GLOBAL_INVARIANT_FAILURE = |
|
| 26 | "Canary invariant"; |
|
| 27 | ||
| 28 | 4× | function _assertionGV01() internal pure virtual override returns (string memory) { |
| 29 | 19× | return ASSERTION_GV01; |
| 30 | } |
|
| 31 | ||
| 32 | 0 | function _assertionBI01Lqty() internal pure virtual override returns (string memory) { |
| 33 | 0 | return ASSERTION_BI01_LQTY; |
| 34 | } |
|
| 35 | ||
| 36 | 0 | function _assertionBI01Bold() internal pure virtual override returns (string memory) { |
| 37 | 0 | return ASSERTION_BI01_BOLD; |
| 38 | } |
|
| 39 | ||
| 40 | 4× | function _assertionBI02() internal pure virtual override returns (string memory) { |
| 41 | 19× | return ASSERTION_BI02; |
| 42 | } |
|
| 43 | ||
| 44 | 4× | function _assertionBI03() internal pure virtual override returns (string memory) { |
| 45 | 19× | return ASSERTION_BI03; |
| 46 | } |
|
| 47 | ||
| 48 | 4× | function _assertionBI04() internal pure virtual override returns (string memory) { |
| 49 | 19× | return ASSERTION_BI04; |
| 50 | } |
|
| 51 | ||
| 52 | 0 | function _assertionBI05BribeDust() internal pure virtual override returns (string memory) { |
| 53 | 0 | return ASSERTION_BI05_BRIBE_DUST; |
| 54 | } |
|
| 55 | ||
| 56 | 0 | function _assertionBI05BoldDust() internal pure virtual override returns (string memory) { |
| 57 | 0 | return ASSERTION_BI05_BOLD_DUST; |
| 58 | } |
|
| 59 | ||
| 60 | /// @dev Canary assertion helper. A failing input is expected to be discovered during fuzzing. |
|
| 61 | 11× | function assert_canary_ASSERTION_CANARY(uint256 entropy) public { |
| 62 | 23× | t(entropy > 0, ASSERTION_CANARY); |
| 63 | } |
|
| 64 | ||
| 65 | /// @dev Canary global invariant expected to fail immediately. |
|
| 66 | 17× | function invariant_canary() |
| 67 | public |
|
| 68 | virtual |
|
| 69 | 1× | returns (bool) |
| 70 | { |
|
| 71 | 22× | t(false, INVARIANT_CANARY_GLOBAL_INVARIANT_FAILURE); |
| 72 | 2× | return true; |
| 73 | } |
|
| 74 | } |
100%
test/recon/Setup.sol
Lines covered: 39 / 39 (100%)
| 1 | ||
| 2 | // SPDX-License-Identifier: GPL-2.0 |
|
| 3 | pragma solidity ^0.8.0; |
|
| 4 | ||
| 5 | import {BaseSetup} from "@chimera/BaseSetup.sol"; |
|
| 6 | import {console2} from "forge-std/Test.sol"; |
|
| 7 | import {vm} from "@chimera/Hevm.sol"; |
|
| 8 | import {IERC20} from "forge-std/interfaces/IERC20.sol"; |
|
| 9 | ||
| 10 | import {MockERC20Tester} from "../mocks/MockERC20Tester.sol"; |
|
| 11 | import {MockStakingV1} from "../mocks/MockStakingV1.sol"; |
|
| 12 | import {MaliciousInitiative} from "../mocks/MaliciousInitiative.sol"; |
|
| 13 | import {Governance} from "src/Governance.sol"; |
|
| 14 | import {BribeInitiative} from "../../src/BribeInitiative.sol"; |
|
| 15 | import {IBribeInitiative} from "../../src/interfaces/IBribeInitiative.sol"; |
|
| 16 | import {IGovernance} from "src/interfaces/IGovernance.sol"; |
|
| 17 | import {IInitiative} from "src/interfaces/IInitiative.sol"; |
|
| 18 | ||
| 19 | abstract contract Setup is BaseSetup { |
|
| 20 | Governance governance; |
|
| 21 | MockERC20Tester internal lqty; |
|
| 22 | MockERC20Tester internal lusd; |
|
| 23 | IBribeInitiative internal initiative1; |
|
| 24 | ||
| 25 | 11× | address internal user = address(this); |
| 26 | 10× | address internal user2 = address(0x537C8f3d3E18dF5517a58B3fB9D9143697996802); // derived using makeAddrAndKey |
| 27 | address internal stakingV1; |
|
| 28 | address internal userProxy; |
|
| 29 | 26× | address[] internal users = new address[](2); |
| 30 | address[] internal deployedInitiatives; |
|
| 31 | 3× | uint256 internal user2Pk = 23868421370328131711506074113045611601786642648093516849953535378706721142721; // derived using makeAddrAndKey |
| 32 | bool internal claimedTwice; |
|
| 33 | ||
| 34 | mapping(uint16 => uint88) internal ghostTotalAllocationAtEpoch; |
|
| 35 | mapping(address => uint88) internal ghostLqtyAllocationByUserAtEpoch; |
|
| 36 | ||
| 37 | 1× | uint128 internal constant REGISTRATION_FEE = 1e18; |
| 38 | 1× | uint128 internal constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; |
| 39 | 1× | uint128 internal constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; |
| 40 | 1× | uint16 internal constant REGISTRATION_WARM_UP_PERIOD = 4; |
| 41 | uint16 internal constant UNREGISTRATION_AFTER_EPOCHS = 4; |
|
| 42 | 1× | uint128 internal constant VOTING_THRESHOLD_FACTOR = 0.04e18; |
| 43 | 1× | uint88 internal constant MIN_CLAIM = 500e18; |
| 44 | 1× | uint88 internal constant MIN_ACCRUAL = 1000e18; |
| 45 | 1× | uint32 internal constant EPOCH_DURATION = 604800; |
| 46 | 1× | uint32 internal constant EPOCH_VOTING_CUTOFF = 518400; |
| 47 | ||
| 48 | ||
| 49 | 3× | function setup() internal virtual override { |
| 50 | 32× | users.push(user); |
| 51 | 26× | users.push(user2); |
| 52 | ||
| 53 | 3× | uint256 initialMintAmount = type(uint88).max; |
| 54 | 41× | lqty = new MockERC20Tester(user, initialMintAmount, "Liquity", "LQTY", 18); |
| 55 | 41× | lusd = new MockERC20Tester(user, initialMintAmount, "Liquity USD", "LUSD", 18); |
| 56 | 45× | lqty.mint(user2, initialMintAmount); |
| 57 | ||
| 58 | 43× | stakingV1 = address(new MockStakingV1(address(lqty))); |
| 59 | 51× | governance = new Governance( |
| 60 | 6× | address(lqty), |
| 61 | 7× | address(lusd), |
| 62 | stakingV1, |
|
| 63 | address(lusd), // bold |
|
| 64 | 57× | IGovernance.Configuration({ |
| 65 | registrationFee: REGISTRATION_FEE, |
|
| 66 | registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, |
|
| 67 | unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, |
|
| 68 | registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, |
|
| 69 | unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, |
|
| 70 | votingThresholdFactor: VOTING_THRESHOLD_FACTOR, |
|
| 71 | minClaim: MIN_CLAIM, |
|
| 72 | minAccrual: MIN_ACCRUAL, |
|
| 73 | 1× | epochStart: uint32(block.timestamp), |
| 74 | epochDuration: EPOCH_DURATION, |
|
| 75 | epochVotingCutoff: EPOCH_VOTING_CUTOFF |
|
| 76 | }), |
|
| 77 | 2× | deployedInitiatives // no initial initiatives passed in because don't have cheatcodes for calculating address where gov will be deployed |
| 78 | ); |
|
| 79 | ||
| 80 | // deploy proxy so user can approve it |
|
| 81 | 81× | userProxy = governance.deployUserProxy(); |
| 82 | 60× | lqty.approve(address(userProxy), initialMintAmount); |
| 83 | 64× | lusd.approve(address(userProxy), initialMintAmount); |
| 84 | ||
| 85 | // approve governance for user's tokens |
|
| 86 | 67× | lqty.approve(address(governance), initialMintAmount); |
| 87 | 67× | lusd.approve(address(governance), initialMintAmount); |
| 88 | ||
| 89 | // register one of the initiatives, leave the other for registering/unregistering via TargetFunction |
|
| 90 | 57× | initiative1 = IBribeInitiative(address(new BribeInitiative(address(governance), address(lusd), address(lqty)))); |
| 91 | 21× | deployedInitiatives.push(address(initiative1)); |
| 92 | ||
| 93 | 53× | governance.registerInitiative(address(initiative1)); |
| 94 | } |
|
| 95 | ||
| 96 | 5× | function _getDeployedInitiative(uint8 index) internal returns (address initiative) { |
| 97 | 32× | return deployedInitiatives[index % deployedInitiatives.length]; |
| 98 | } |
|
| 99 | ||
| 100 | function _getClampedTokenBalance(address token, address holder) internal returns (uint256 balance) { |
|
| 101 | return IERC20(token).balanceOf(holder); |
|
| 102 | } |
|
| 103 | ||
| 104 | 3× | function _getRandomUser(uint8 index) internal returns (address randomUser) { |
| 105 | 11× | return users[index % users.length]; |
| 106 | } |
|
| 107 | ||
| 108 | } |
100%
test/recon/TargetFunctions.sol
Lines covered: 6 / 6 (100%)
| 1 | ||
| 2 | // SPDX-License-Identifier: GPL-2.0 |
|
| 3 | pragma solidity ^0.8.0; |
|
| 4 | ||
| 5 | import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; |
|
| 6 | import {vm} from "@chimera/Hevm.sol"; |
|
| 7 | import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; |
|
| 8 | import {console2} from "forge-std/Test.sol"; |
|
| 9 | ||
| 10 | import {Properties} from "./Properties.sol"; |
|
| 11 | import {GovernanceTargets} from "./targets/GovernanceTargets.sol"; |
|
| 12 | import {BribeInitiativeTargets} from "./targets/BribeInitiativeTargets.sol"; |
|
| 13 | import {MaliciousInitiative} from "../mocks/MaliciousInitiative.sol"; |
|
| 14 | import {BribeInitiative} from "../../src/BribeInitiative.sol"; |
|
| 15 | import {ILQTYStaking} from "../../src/interfaces/ILQTYStaking.sol"; |
|
| 16 | import {IInitiative} from "../../src/interfaces/IInitiative.sol"; |
|
| 17 | import {IUserProxy} from "../../src/interfaces/IUserProxy.sol"; |
|
| 18 | import {PermitParams} from "../../src/utils/Types.sol"; |
|
| 19 | ||
| 20 | ||
| 21 | abstract contract TargetFunctions is GovernanceTargets, BribeInitiativeTargets { |
|
| 22 | ||
| 23 | // helper to deploy initiatives for registering that results in more bold transferred to the Governance contract |
|
| 24 | 6× | function helper_deployInitiative() withChecks public { |
| 25 | 43× | address initiative = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); |
| 26 | 29× | deployedInitiatives.push(initiative); |
| 27 | } |
|
| 28 | ||
| 29 | // helper to simulate bold accrual in Governance contract |
|
| 30 | 11× | function helper_accrueBold(uint88 boldAmount) withChecks public { |
| 31 | 73× | boldAmount = uint88(boldAmount % lusd.balanceOf(user)); |
| 32 | // target contract is the user so it can transfer directly |
|
| 33 | 69× | lusd.transfer(address(governance), boldAmount); |
| 34 | } |
|
| 35 | } |
50%
test/recon/properties/BribeInitiativeProperties.sol
Lines covered: 33 / 65 (50%)
| 1 | // SPDX-License-Identifier: GPL-2.0 |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | import {BeforeAfter} from "../BeforeAfter.sol"; |
|
| 5 | import {IBribeInitiative} from "../../../src/interfaces/IBribeInitiative.sol"; |
|
| 6 | ||
| 7 | abstract contract BribeInitiativeProperties is BeforeAfter { |
|
| 8 | function _assertionBI01Lqty() internal pure virtual returns (string memory); |
|
| 9 | function _assertionBI01Bold() internal pure virtual returns (string memory); |
|
| 10 | function _assertionBI02() internal pure virtual returns (string memory); |
|
| 11 | function _assertionBI03() internal pure virtual returns (string memory); |
|
| 12 | function _assertionBI04() internal pure virtual returns (string memory); |
|
| 13 | function _assertionBI05BribeDust() internal pure virtual returns (string memory); |
|
| 14 | function _assertionBI05BoldDust() internal pure virtual returns (string memory); |
|
| 15 | ||
| 16 | 6× | function invariant_BI01() public returns (bool) { |
| 17 | 72× | uint16 currentEpoch = governance.epoch(); |
| 18 | 22× | for (uint8 i; i < deployedInitiatives.length; i++) { |
| 19 | 30× | address initiative = deployedInitiatives[i]; |
| 20 | // if the bool switches, the user has claimed their bribe for the epoch |
|
| 21 | 3× | if ( |
| 22 | 37× | _before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] |
| 23 | 40× | != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] |
| 24 | 0 | ) { |
| 25 | // calculate user balance delta of the bribe tokens |
|
| 26 | 0 | uint128 lqtyBalanceDelta = _after.lqtyBalance - _before.lqtyBalance; |
| 27 | 0 | uint128 lusdBalanceDelta = _after.lusdBalance - _before.lusdBalance; |
| 28 | ||
| 29 | // calculate balance delta as a percentage of the total bribe for this epoch |
|
| 30 | 0 | (uint128 bribeBoldAmount, uint128 bribeBribeTokenAmount) = |
| 31 | 0 | IBribeInitiative(initiative).bribeByEpoch(currentEpoch); |
| 32 | 0 | uint128 lqtyPercentageOfBribe = (lqtyBalanceDelta / bribeBribeTokenAmount) * 10_000; |
| 33 | 0 | uint128 lusdPercentageOfBribe = (lusdBalanceDelta / bribeBoldAmount) * 10_000; |
| 34 | ||
| 35 | // Shift right by 40 bits (128 - 88) to get the 88 most significant bits |
|
| 36 | 0 | uint88 lqtyPercentageOfBribe88 = uint88(lqtyPercentageOfBribe >> 40); |
| 37 | 0 | uint88 lusdPercentageOfBribe88 = uint88(lusdPercentageOfBribe >> 40); |
| 38 | ||
| 39 | // calculate user allocation percentage of total for this epoch |
|
| 40 | 0 | uint88 lqtyAllocatedByUserAtEpoch = |
| 41 | 0 | IBribeInitiative(initiative).lqtyAllocatedByUserAtEpoch(user, currentEpoch); |
| 42 | 0 | uint88 totalLQTYAllocatedAtEpoch = IBribeInitiative(initiative).totalLQTYAllocatedByEpoch(currentEpoch); |
| 43 | 0 | uint88 allocationPercentageOfTotal = (lqtyAllocatedByUserAtEpoch / totalLQTYAllocatedAtEpoch) * 10_000; |
| 44 | ||
| 45 | // check that allocation percentage and received bribe percentage match |
|
| 46 | 0 | eq( |
| 47 | 0 | lqtyPercentageOfBribe88, |
| 48 | 0 | allocationPercentageOfTotal, |
| 49 | 0 | _assertionBI01Lqty() |
| 50 | ); |
|
| 51 | 0 | eq( |
| 52 | 0 | lusdPercentageOfBribe88, |
| 53 | 0 | allocationPercentageOfTotal, |
| 54 | 0 | _assertionBI01Bold() |
| 55 | ); |
|
| 56 | } |
|
| 57 | } |
|
| 58 | return true; |
|
| 59 | } |
|
| 60 | ||
| 61 | 7× | function invariant_BI02() public returns (bool) { |
| 62 | 13× | t(!claimedTwice, _assertionBI02()); |
| 63 | return true; |
|
| 64 | } |
|
| 65 | ||
| 66 | 6× | function invariant_BI03() public returns (bool) { |
| 67 | 72× | uint16 currentEpoch = governance.epoch(); |
| 68 | 25× | for (uint8 i; i < deployedInitiatives.length; i++) { |
| 69 | 27× | IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); |
| 70 | 61× | uint88 lqtyAllocatedByUserAtEpoch = initiative.lqtyAllocatedByUserAtEpoch(user, currentEpoch); |
| 71 | 5× | eq( |
| 72 | 20× | ghostLqtyAllocationByUserAtEpoch[user], |
| 73 | lqtyAllocatedByUserAtEpoch, |
|
| 74 | 3× | _assertionBI03() |
| 75 | ); |
|
| 76 | } |
|
| 77 | return true; |
|
| 78 | } |
|
| 79 | ||
| 80 | 8× | function invariant_BI04() public returns (bool) { |
| 81 | 72× | uint16 currentEpoch = governance.epoch(); |
| 82 | 27× | for (uint8 i; i < deployedInitiatives.length; i++) { |
| 83 | 28× | IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); |
| 84 | 56× | uint88 totalLQTYAllocatedAtEpoch = initiative.totalLQTYAllocatedByEpoch(currentEpoch); |
| 85 | 7× | eq( |
| 86 | 19× | ghostTotalAllocationAtEpoch[currentEpoch], |
| 87 | totalLQTYAllocatedAtEpoch, |
|
| 88 | 4× | _assertionBI04() |
| 89 | ); |
|
| 90 | } |
|
| 91 | 4× | return true; |
| 92 | } |
|
| 93 | ||
| 94 | // TODO: double check that this implementation is correct |
|
| 95 | 6× | function invariant_BI05() public returns (bool) { |
| 96 | 72× | uint16 currentEpoch = governance.epoch(); |
| 97 | 22× | for (uint8 i; i < deployedInitiatives.length; i++) { |
| 98 | 30× | address initiative = deployedInitiatives[i]; |
| 99 | // if the bool switches, the user has claimed their bribe for the epoch |
|
| 100 | 3× | if ( |
| 101 | 37× | _before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] |
| 102 | 40× | != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] |
| 103 | 0 | ) { |
| 104 | // check that the remaining bribe amount left over is less than 100 million wei |
|
| 105 | 0 | uint256 bribeTokenBalanceInitiative = lqty.balanceOf(initiative); |
| 106 | 0 | uint256 boldTokenBalanceInitiative = lusd.balanceOf(initiative); |
| 107 | ||
| 108 | 0 | lte( |
| 109 | 0 | bribeTokenBalanceInitiative, |
| 110 | 0 | 1e8, |
| 111 | 0 | _assertionBI05BribeDust() |
| 112 | ); |
|
| 113 | 0 | lte( |
| 114 | 0 | boldTokenBalanceInitiative, |
| 115 | 0 | 1e8, |
| 116 | 0 | _assertionBI05BoldDust() |
| 117 | ); |
|
| 118 | } |
|
| 119 | } |
|
| 120 | return true; |
|
| 121 | } |
|
| 122 | } |
100%
test/recon/properties/GovernanceProperties.sol
Lines covered: 9 / 9 (100%)
| 1 | // SPDX-License-Identifier: GPL-2.0 |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | import {BeforeAfter} from "../BeforeAfter.sol"; |
|
| 5 | ||
| 6 | abstract contract GovernanceProperties is BeforeAfter { |
|
| 7 | function _assertionGV01() internal pure virtual returns (string memory); |
|
| 8 | ||
| 9 | 8× | function invariant_GV01() public returns (bool) { |
| 10 | // first check that epoch hasn't changed after the operation |
|
| 11 | 13× | if(_before.epoch == _after.epoch) { |
| 12 | // loop through the initiatives and check that their status hasn't changed |
|
| 13 | 24× | for(uint8 i; i < deployedInitiatives.length; i++) { |
| 14 | 28× | address initiative = deployedInitiatives[i]; |
| 15 | 3× | eq( |
| 16 | 21× | uint256(_before.initiativeStatus[initiative]), |
| 17 | 23× | uint256(_after.initiativeStatus[initiative]), |
| 18 | 3× | _assertionGV01() |
| 19 | ); |
|
| 20 | } |
|
| 21 | } |
|
| 22 | 2× | return true; |
| 23 | } |
|
| 24 | ||
| 25 | } |
89%
test/recon/targets/BribeInitiativeTargets.sol
Lines covered: 17 / 19 (89%)
| 1 | // SPDX-License-Identifier: GPL-2.0 |
|
| 2 | pragma solidity ^0.8.0; |
|
| 3 | ||
| 4 | import {Test} from "forge-std/Test.sol"; |
|
| 5 | import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; |
|
| 6 | import {vm} from "@chimera/Hevm.sol"; |
|
| 7 | ||
| 8 | import {IInitiative} from "../../../src/interfaces/IInitiative.sol"; |
|
| 9 | import {IBribeInitiative} from "../../../src/interfaces/IBribeInitiative.sol"; |
|
| 10 | import {DoubleLinkedList} from "../../../src/utils/DoubleLinkedList.sol"; |
|
| 11 | import {Properties} from "../Properties.sol"; |
|
| 12 | ||
| 13 | ||
| 14 | abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Properties { |
|
| 15 | using DoubleLinkedList for DoubleLinkedList.List; |
|
| 16 | ||
| 17 | // NOTE: initiatives that get called here are deployed but not necessarily registered |
|
| 18 | ||
| 19 | 17× | function initiative_depositBribe(uint128 boldAmount, uint128 bribeTokenAmount, uint16 epoch, uint8 initiativeIndex) withChecks public { |
| 20 | 8× | IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); |
| 21 | ||
| 22 | // clamp token amounts using user balance |
|
| 23 | 74× | boldAmount = uint128(boldAmount % lusd.balanceOf(user)); |
| 24 | 74× | bribeTokenAmount = uint128(bribeTokenAmount % lqty.balanceOf(user)); |
| 25 | ||
| 26 | 51× | initiative.depositBribe(boldAmount, bribeTokenAmount, epoch); |
| 27 | } |
|
| 28 | ||
| 29 | 11× | function initiative_claimBribes(uint16 epoch, uint16 prevAllocationEpoch, uint16 prevTotalAllocationEpoch, uint8 initiativeIndex) withChecks public { |
| 30 | 8× | IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); |
| 31 | ||
| 32 | // clamp epochs by using the current governance epoch |
|
| 33 | 77× | epoch = epoch % governance.epoch(); |
| 34 | 77× | prevAllocationEpoch = prevAllocationEpoch % governance.epoch(); |
| 35 | 77× | prevTotalAllocationEpoch = prevTotalAllocationEpoch % governance.epoch(); |
| 36 | ||
| 37 | 36× | IBribeInitiative.ClaimData[] memory claimData = new IBribeInitiative.ClaimData[](1); |
| 38 | 44× | claimData[0] = IBribeInitiative.ClaimData({ |
| 39 | 1× | epoch: epoch, |
| 40 | 1× | prevLQTYAllocationEpoch: prevAllocationEpoch, |
| 41 | 1× | prevTotalLQTYAllocationEpoch: prevTotalAllocationEpoch |
| 42 | }); |
|
| 43 | ||
| 44 | 67× | bool alreadyClaimed = initiative.claimedBribeAtEpoch(user, epoch); |
| 45 | ||
| 46 | 43× | initiative.claimBribes(claimData); |
| 47 | ||
| 48 | // check if the bribe was already claimed at the given epoch |
|
| 49 | 0 | if(alreadyClaimed) { |
| 50 | // toggle canary that breaks the BI-02 property |
|
| 51 | 0 | claimedTwice = true; |
| 52 | } |
|
| 53 | } |
|
| 54 | } |
84%
test/recon/targets/GovernanceTargets.sol
Lines covered: 48 / 57 (84%)
| 1 | ||
| 2 | // SPDX-License-Identifier: GPL-2.0 |
|
| 3 | pragma solidity ^0.8.0; |
|
| 4 | ||
| 5 | import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; |
|
| 6 | import {vm} from "@chimera/Hevm.sol"; |
|
| 7 | import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; |
|
| 8 | import {console2} from "forge-std/Test.sol"; |
|
| 9 | ||
| 10 | import {Properties} from "../Properties.sol"; |
|
| 11 | import {MaliciousInitiative} from "../../mocks/MaliciousInitiative.sol"; |
|
| 12 | import {BribeInitiative} from "../../../src/BribeInitiative.sol"; |
|
| 13 | import {ILQTYStaking} from "../../../src/interfaces/ILQTYStaking.sol"; |
|
| 14 | import {IInitiative} from "../../../src/interfaces/IInitiative.sol"; |
|
| 15 | import {IUserProxy} from "../../../src/interfaces/IUserProxy.sol"; |
|
| 16 | import {PermitParams} from "../../../src/utils/Types.sol"; |
|
| 17 | import {add} from "../../../src/utils/Math.sol"; |
|
| 18 | ||
| 19 | ||
| 20 | ||
| 21 | abstract contract GovernanceTargets is BaseTargetFunctions, Properties { |
|
| 22 | ||
| 23 | // clamps to a single initiative to ensure coverage in case both haven't been registered yet |
|
| 24 | 22× | function governance_allocateLQTY_clamped_single_initiative(uint8 initiativesIndex, uint96 deltaLQTYVotes, uint96 deltaLQTYVetos) withChecks public { |
| 25 | 72× | uint16 currentEpoch = governance.epoch(); |
| 26 | 134× | uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance |
| 27 | ||
| 28 | 30× | address[] memory initiatives = new address[](1); |
| 29 | 31× | initiatives[0] = _getDeployedInitiative(initiativesIndex); |
| 30 | 28× | int88[] memory deltaLQTYVotesArray = new int88[](1); |
| 31 | 32× | deltaLQTYVotesArray[0] = int88(uint88(deltaLQTYVotes % stakedAmount)); |
| 32 | 28× | int88[] memory deltaLQTYVetosArray = new int88[](1); |
| 33 | 32× | deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % stakedAmount)); |
| 34 | ||
| 35 | 66× | governance.allocateLQTY(initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); |
| 36 | ||
| 37 | // if call was successful update the ghost tracking variables |
|
| 38 | // allocation only allows voting OR vetoing at a time so need to check which was executed |
|
| 39 | 26× | if(deltaLQTYVotesArray[0] > 0) { |
| 40 | 66× | ghostLqtyAllocationByUserAtEpoch[user] = add(ghostLqtyAllocationByUserAtEpoch[user], deltaLQTYVotesArray[0]); |
| 41 | 54× | ghostTotalAllocationAtEpoch[currentEpoch] = add(ghostTotalAllocationAtEpoch[currentEpoch], deltaLQTYVotesArray[0]); |
| 42 | } else { |
|
| 43 | 57× | ghostLqtyAllocationByUserAtEpoch[user] = add(ghostLqtyAllocationByUserAtEpoch[user], deltaLQTYVetosArray[0]); |
| 44 | 54× | ghostTotalAllocationAtEpoch[currentEpoch] = add(ghostTotalAllocationAtEpoch[currentEpoch], deltaLQTYVetosArray[0]); |
| 45 | } |
|
| 46 | } |
|
| 47 | ||
| 48 | 11× | function governance_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) withChecks public { |
| 49 | 65× | governance.allocateLQTY(deployedInitiatives, _deltaLQTYVotes, _deltaLQTYVetos); |
| 50 | } |
|
| 51 | ||
| 52 | 11× | function governance_claimForInitiative(uint8 initiativeIndex) withChecks public { |
| 53 | 8× | address initiative = _getDeployedInitiative(initiativeIndex); |
| 54 | 65× | governance.claimForInitiative(initiative); |
| 55 | } |
|
| 56 | ||
| 57 | 12× | function governance_claimFromStakingV1(uint8 recipientIndex) withChecks public { |
| 58 | 8× | address rewardRecipient = _getRandomUser(recipientIndex); |
| 59 | 68× | governance.claimFromStakingV1(rewardRecipient); |
| 60 | } |
|
| 61 | ||
| 62 | 5× | function governance_deployUserProxy() withChecks public { |
| 63 | 48× | governance.deployUserProxy(); |
| 64 | } |
|
| 65 | ||
| 66 | 11× | function governance_depositLQTY(uint88 lqtyAmount) withChecks public { |
| 67 | 75× | lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); |
| 68 | 19× | governance.depositLQTY(lqtyAmount); |
| 69 | } |
|
| 70 | ||
| 71 | 13× | function governance_depositLQTYViaPermit(uint88 _lqtyAmount) withChecks public { |
| 72 | // Get the current block timestamp for the deadline |
|
| 73 | 9× | uint256 deadline = block.timestamp + 1 hours; |
| 74 | ||
| 75 | // Create the permit message |
|
| 76 | 4× | bytes32 permitTypeHash = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); |
| 77 | 68× | bytes32 domainSeparator = IERC20Permit(address(lqty)).DOMAIN_SEPARATOR(); |
| 78 | ||
| 79 | ||
| 80 | 67× | uint256 nonce = IERC20Permit(address(lqty)).nonces(user); |
| 81 | ||
| 82 | 27× | bytes32 structHash = keccak256(abi.encode( |
| 83 | permitTypeHash, |
|
| 84 | 7× | user, |
| 85 | 9× | address(governance), |
| 86 | _lqtyAmount, |
|
| 87 | nonce, |
|
| 88 | deadline |
|
| 89 | )); |
|
| 90 | ||
| 91 | 25× | bytes32 digest = keccak256(abi.encodePacked( |
| 92 | "\x19\x01", |
|
| 93 | domainSeparator, |
|
| 94 | structHash |
|
| 95 | )); |
|
| 96 | ||
| 97 | 60× | (uint8 v, bytes32 r, bytes32 s) = vm.sign(user2Pk, digest); |
| 98 | ||
| 99 | 0 | PermitParams memory permitParams = PermitParams({ |
| 100 | 0 | owner: user2, |
| 101 | 0 | spender: user, |
| 102 | 0 | value: _lqtyAmount, |
| 103 | 0 | deadline: deadline, |
| 104 | 0 | v: v, |
| 105 | 0 | r: r, |
| 106 | 0 | s: s |
| 107 | }); |
|
| 108 | ||
| 109 | 0 | governance.depositLQTYViaPermit(_lqtyAmount, permitParams); |
| 110 | } |
|
| 111 | ||
| 112 | 12× | function governance_registerInitiative(uint8 initiativeIndex) withChecks public { |
| 113 | 8× | address initiative = _getDeployedInitiative(initiativeIndex); |
| 114 | 59× | governance.registerInitiative(initiative); |
| 115 | } |
|
| 116 | ||
| 117 | 11× | function governance_snapshotVotesForInitiative(address _initiative) withChecks public { |
| 118 | 68× | governance.snapshotVotesForInitiative(_initiative); |
| 119 | } |
|
| 120 | ||
| 121 | 11× | function governance_unregisterInitiative(uint8 initiativeIndex) withChecks public { |
| 122 | 8× | address initiative = _getDeployedInitiative(initiativeIndex); |
| 123 | 22× | governance.unregisterInitiative(initiative); |
| 124 | } |
|
| 125 | ||
| 126 | 11× | function governance_withdrawLQTY(uint88 _lqtyAmount) withChecks public { |
| 127 | 57× | governance.withdrawLQTY(_lqtyAmount); |
| 128 | } |
|
| 129 | } |