github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/periphery/drippie/Drippie.t.sol (about) 1 // SPDX-License-Identifier: MIT 2 pragma solidity 0.8.15; 3 4 import { Test } from "forge-std/Test.sol"; 5 import { Drippie } from "src/periphery/drippie/Drippie.sol"; 6 import { IDripCheck } from "src/periphery/drippie/IDripCheck.sol"; 7 import { CheckTrue } from "src/periphery/drippie/dripchecks/CheckTrue.sol"; 8 import { SimpleStorage } from "test/mocks/SimpleStorage.sol"; 9 10 /// @title TestDrippie 11 /// @notice This is a wrapper contract around Drippie used for testing. 12 /// Returning an entire `Drippie.DripState` causes stack too 13 /// deep errors without `--vir-ir` which causes the compile time 14 /// to go up by ~4x. Each of the methods is a simple getter around 15 /// parts of the `DripState`. 16 contract TestDrippie is Drippie { 17 constructor(address owner) Drippie(owner) { } 18 19 function dripStatus(string memory name) external view returns (Drippie.DripStatus) { 20 return drips[name].status; 21 } 22 23 function dripStateLast(string memory name) external view returns (uint256) { 24 return drips[name].last; 25 } 26 27 function dripConfig(string memory name) external view returns (Drippie.DripConfig memory) { 28 return drips[name].config; 29 } 30 31 function dripConfigActions(string memory name) external view returns (Drippie.DripAction[] memory) { 32 return drips[name].config.actions; 33 } 34 35 function dripConfigCheckParams(string memory name) external view returns (bytes memory) { 36 return drips[name].config.checkparams; 37 } 38 39 function dripConfigCheckAddress(string memory name) external view returns (address) { 40 return address(drips[name].config.dripcheck); 41 } 42 } 43 44 /// @title Drippie_Test 45 /// @notice Test coverage of the Drippie contract. 46 contract Drippie_Test is Test { 47 /// @notice Emitted when a drip is executed. 48 event DripExecuted(string indexed nameref, string name, address executor, uint256 timestamp); 49 50 /// @notice Emitted when a drip's status is updated. 51 event DripStatusUpdated(string indexed nameref, string name, Drippie.DripStatus status); 52 53 /// @notice Emitted when a drip is created. 54 event DripCreated(string indexed nameref, string name, Drippie.DripConfig config); 55 56 /// @notice Address of the test DripCheck. CheckTrue is deployed 57 /// here so it always returns true. 58 IDripCheck check; 59 60 /// @notice Address of a SimpleStorage contract. Used to test that 61 /// calls are actually made by Drippie. 62 SimpleStorage simpleStorage; 63 64 /// @notice Address of the Drippie contract. 65 TestDrippie drippie; 66 67 /// @notice Address of the Drippie owner 68 address constant alice = address(0x42); 69 70 /// @notice The name of the default drip 71 string constant dripName = "foo"; 72 73 /// @notice Set up the test suite by deploying a CheckTrue DripCheck. 74 /// This is a mock that always returns true. Alice is the owner 75 /// and also give drippie 1 ether so that it can balance transfer. 76 function setUp() external { 77 check = IDripCheck(new CheckTrue()); 78 simpleStorage = new SimpleStorage(); 79 80 drippie = new TestDrippie(alice); 81 vm.deal(address(drippie), 1 ether); 82 } 83 84 /// @notice Builds a default Drippie.DripConfig. Uses CheckTrue as the 85 /// dripcheck so it will always be able to do its DripActions. 86 /// Gives a dummy DripAction that does a simple transfer to 87 /// a dummy address. 88 function _defaultConfig() internal view returns (Drippie.DripConfig memory) { 89 Drippie.DripAction[] memory actions = new Drippie.DripAction[](1); 90 actions[0] = Drippie.DripAction({ target: payable(address(0x44)), data: hex"", value: 1 }); 91 92 return Drippie.DripConfig({ 93 interval: 100, 94 dripcheck: check, 95 reentrant: false, 96 checkparams: hex"", 97 actions: actions 98 }); 99 } 100 101 /// @notice Creates a default drip using the default drip config. 102 function _createDefaultDrip(string memory name) internal { 103 address owner = drippie.owner(); 104 Drippie.DripConfig memory cfg = _defaultConfig(); 105 vm.prank(owner); 106 drippie.create(name, cfg); 107 } 108 109 /// @notice Moves the block's timestamp forward so that it is 110 /// possible to execute a drip. 111 function _warpToExecutable(string memory name) internal { 112 Drippie.DripConfig memory config = drippie.dripConfig(name); 113 vm.warp(config.interval + drippie.dripStateLast(name)); 114 } 115 116 /// @notice Creates a drip and asserts that it was configured as expected. 117 function test_create_succeeds() external { 118 Drippie.DripConfig memory cfg = _defaultConfig(); 119 vm.expectEmit(address(drippie)); 120 emit DripCreated(dripName, dripName, cfg); 121 122 if (cfg.reentrant) { 123 assertEq(cfg.interval, 0); 124 } else { 125 assertTrue(cfg.interval > 0); 126 } 127 128 vm.prank(drippie.owner()); 129 drippie.create(dripName, cfg); 130 131 Drippie.DripStatus status = drippie.dripStatus(dripName); 132 Drippie.DripConfig memory config = drippie.dripConfig(dripName); 133 134 assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED)); 135 136 assertEq(config.interval, cfg.interval); 137 assertEq(config.reentrant, cfg.reentrant); 138 assertEq(address(config.dripcheck), address(cfg.dripcheck)); 139 assertEq(config.checkparams, cfg.checkparams); 140 141 assertEq(config.actions.length, cfg.actions.length); 142 143 for (uint256 i; i < config.actions.length; i++) { 144 Drippie.DripAction memory a = config.actions[i]; 145 Drippie.DripAction memory b = cfg.actions[i]; 146 147 assertEq(a.target, b.target); 148 assertEq(a.data, b.data); 149 assertEq(a.value, b.value); 150 } 151 } 152 153 /// @notice Ensures that the same drip cannot be created two times. 154 function test_create_calledTwice_reverts() external { 155 vm.startPrank(drippie.owner()); 156 Drippie.DripConfig memory cfg = _defaultConfig(); 157 drippie.create(dripName, cfg); 158 vm.expectRevert("Drippie: drip with that name already exists"); 159 drippie.create(dripName, cfg); 160 vm.stopPrank(); 161 } 162 163 /// @notice Ensures that only the owner of Drippie can create a drip. 164 function testFuzz_owner_unauthorized_reverts(address caller) external { 165 vm.assume(caller != drippie.owner()); 166 vm.prank(caller); 167 vm.expectRevert("UNAUTHORIZED"); 168 drippie.create(dripName, _defaultConfig()); 169 } 170 171 /// @notice The owner should be able to set the status of the drip. 172 function test_set_status_succeeds() external { 173 vm.expectEmit(address(drippie)); 174 emit DripCreated(dripName, dripName, _defaultConfig()); 175 _createDefaultDrip(dripName); 176 177 address owner = drippie.owner(); 178 179 { 180 Drippie.DripStatus status = drippie.dripStatus(dripName); 181 assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED)); 182 } 183 184 vm.prank(owner); 185 186 vm.expectEmit(address(drippie)); 187 emit DripStatusUpdated({ nameref: dripName, name: dripName, status: Drippie.DripStatus.ACTIVE }); 188 189 drippie.status(dripName, Drippie.DripStatus.ACTIVE); 190 191 { 192 Drippie.DripStatus status = drippie.dripStatus(dripName); 193 assertEq(uint256(status), uint256(Drippie.DripStatus.ACTIVE)); 194 } 195 196 vm.prank(owner); 197 198 vm.expectEmit(address(drippie)); 199 emit DripStatusUpdated({ nameref: dripName, name: dripName, status: Drippie.DripStatus.PAUSED }); 200 201 drippie.status(dripName, Drippie.DripStatus.PAUSED); 202 203 { 204 Drippie.DripStatus status = drippie.dripStatus(dripName); 205 assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED)); 206 } 207 } 208 209 /// @notice The drip status cannot be set back to NONE after it is created. 210 function test_set_statusNone_reverts() external { 211 _createDefaultDrip(dripName); 212 213 vm.prank(drippie.owner()); 214 215 vm.expectRevert("Drippie: drip status can never be set back to NONE after creation"); 216 217 drippie.status(dripName, Drippie.DripStatus.NONE); 218 } 219 220 /// @notice The owner cannot set the status of the drip to the status that 221 /// it is already set as. 222 function test_set_statusSame_reverts() external { 223 _createDefaultDrip(dripName); 224 225 vm.prank(drippie.owner()); 226 227 vm.expectRevert("Drippie: cannot set drip status to the same status as its current status"); 228 229 drippie.status(dripName, Drippie.DripStatus.PAUSED); 230 } 231 232 /// @notice The owner should be able to archive the drip if it is in the 233 /// paused state. 234 function test_shouldArchive_ifPaused_succeeds() external { 235 _createDefaultDrip(dripName); 236 237 address owner = drippie.owner(); 238 239 vm.prank(owner); 240 241 vm.expectEmit(address(drippie)); 242 emit DripStatusUpdated({ nameref: dripName, name: dripName, status: Drippie.DripStatus.ARCHIVED }); 243 244 drippie.status(dripName, Drippie.DripStatus.ARCHIVED); 245 246 Drippie.DripStatus status = drippie.dripStatus(dripName); 247 assertEq(uint256(status), uint256(Drippie.DripStatus.ARCHIVED)); 248 } 249 250 /// @notice The owner should not be able to archive the drip if it is in the 251 /// active state. 252 function test_shouldNotArchive_ifActive_reverts() external { 253 _createDefaultDrip(dripName); 254 255 vm.prank(drippie.owner()); 256 drippie.status(dripName, Drippie.DripStatus.ACTIVE); 257 258 vm.prank(drippie.owner()); 259 260 vm.expectRevert("Drippie: drip must first be paused before being archived"); 261 262 drippie.status(dripName, Drippie.DripStatus.ARCHIVED); 263 } 264 265 /// @notice The owner should not be allowed to pause the drip if it 266 /// has already been archived. 267 function test_shouldNotAllowPaused_ifArchived_reverts() external { 268 _createDefaultDrip(dripName); 269 270 _notAllowFromArchive(dripName, Drippie.DripStatus.PAUSED); 271 } 272 273 /// @notice The owner should not be allowed to make the drip active again if 274 /// it has already been archived. 275 function test_shouldNotAllowActive_ifArchived_reverts() external { 276 _createDefaultDrip(dripName); 277 278 _notAllowFromArchive(dripName, Drippie.DripStatus.ACTIVE); 279 } 280 281 /// @notice Archive the drip and then attempt to set the status to the passed 282 /// in status. 283 function _notAllowFromArchive(string memory name, Drippie.DripStatus status) internal { 284 address owner = drippie.owner(); 285 vm.prank(owner); 286 drippie.status(name, Drippie.DripStatus.ARCHIVED); 287 288 vm.expectRevert("Drippie: drip with that name has been archived and cannot be updated"); 289 290 vm.prank(owner); 291 drippie.status(name, status); 292 } 293 294 /// @notice Attempt to update a drip that does not exist. 295 function test_name_notExist_reverts() external { 296 string memory otherName = "bar"; 297 298 vm.prank(drippie.owner()); 299 300 vm.expectRevert("Drippie: drip with that name does not exist and cannot be updated"); 301 302 assertFalse(keccak256(abi.encode(dripName)) == keccak256(abi.encode(otherName))); 303 304 drippie.status(otherName, Drippie.DripStatus.ARCHIVED); 305 } 306 307 /// @notice Expect a revert when attempting to set the status when not the owner. 308 function test_status_unauthorized_reverts() external { 309 _createDefaultDrip(dripName); 310 311 vm.expectRevert("UNAUTHORIZED"); 312 drippie.status(dripName, Drippie.DripStatus.ACTIVE); 313 } 314 315 /// @notice The drip should execute and be able to transfer value. 316 function test_drip_amount_succeeds() external { 317 _createDefaultDrip(dripName); 318 319 vm.prank(drippie.owner()); 320 drippie.status(dripName, Drippie.DripStatus.ACTIVE); 321 322 _warpToExecutable(dripName); 323 324 vm.expectEmit(address(drippie)); 325 emit DripExecuted({ nameref: dripName, name: dripName, executor: address(this), timestamp: block.timestamp }); 326 327 Drippie.DripAction[] memory actions = drippie.dripConfigActions(dripName); 328 assertEq(actions.length, 1); 329 330 vm.expectCall(drippie.dripConfigCheckAddress(dripName), drippie.dripConfigCheckParams(dripName)); 331 332 vm.expectCall(actions[0].target, actions[0].value, actions[0].data); 333 334 drippie.drip(dripName); 335 } 336 337 /// @notice A single DripAction should be able to make a state modifying call. 338 function test_trigger_oneFunction_succeeds() external { 339 Drippie.DripConfig memory cfg = _defaultConfig(); 340 341 bytes32 key = bytes32(uint256(2)); 342 bytes32 value = bytes32(uint256(3)); 343 344 // Add in an action 345 cfg.actions[0] = Drippie.DripAction({ 346 target: payable(address(simpleStorage)), 347 data: abi.encodeWithSelector(SimpleStorage.set.selector, key, value), 348 value: 0 349 }); 350 351 vm.prank(drippie.owner()); 352 drippie.create(dripName, cfg); 353 354 _warpToExecutable(dripName); 355 356 vm.prank(drippie.owner()); 357 drippie.status(dripName, Drippie.DripStatus.ACTIVE); 358 359 vm.expectCall(address(simpleStorage), 0, abi.encodeWithSelector(SimpleStorage.set.selector, key, value)); 360 361 vm.expectEmit(address(drippie)); 362 emit DripExecuted(dripName, dripName, address(this), block.timestamp); 363 drippie.drip(dripName); 364 365 assertEq(simpleStorage.get(key), value); 366 } 367 368 /// @notice Multiple drip actions should be able to be triggered with the same check. 369 function test_trigger_twoFunctions_succeeds() external { 370 Drippie.DripConfig memory cfg = _defaultConfig(); 371 Drippie.DripAction[] memory actions = new Drippie.DripAction[](2); 372 373 bytes32 keyOne = bytes32(uint256(2)); 374 bytes32 valueOne = bytes32(uint256(3)); 375 actions[0] = Drippie.DripAction({ 376 target: payable(address(simpleStorage)), 377 data: abi.encodeWithSelector(simpleStorage.set.selector, keyOne, valueOne), 378 value: 0 379 }); 380 381 bytes32 keyTwo = bytes32(uint256(4)); 382 bytes32 valueTwo = bytes32(uint256(5)); 383 actions[1] = Drippie.DripAction({ 384 target: payable(address(simpleStorage)), 385 data: abi.encodeWithSelector(simpleStorage.set.selector, keyTwo, valueTwo), 386 value: 0 387 }); 388 389 cfg.actions = actions; 390 391 vm.prank(drippie.owner()); 392 drippie.create(dripName, cfg); 393 394 _warpToExecutable(dripName); 395 396 vm.prank(drippie.owner()); 397 drippie.status(dripName, Drippie.DripStatus.ACTIVE); 398 399 vm.expectCall(drippie.dripConfigCheckAddress(dripName), drippie.dripConfigCheckParams(dripName)); 400 401 vm.expectCall(address(simpleStorage), 0, abi.encodeWithSelector(SimpleStorage.set.selector, keyOne, valueOne)); 402 403 vm.expectCall(address(simpleStorage), 0, abi.encodeWithSelector(SimpleStorage.set.selector, keyTwo, valueTwo)); 404 405 vm.expectEmit(address(drippie)); 406 emit DripExecuted(dripName, dripName, address(this), block.timestamp); 407 drippie.drip(dripName); 408 409 assertEq(simpleStorage.get(keyOne), valueOne); 410 411 assertEq(simpleStorage.get(keyTwo), valueTwo); 412 } 413 414 /// @notice The drips can only be triggered once per interval. Attempt to 415 /// trigger the same drip multiple times in the same interval. Then 416 /// move forward to the next interval and it should trigger. 417 function test_twice_inOneInterval_reverts() external { 418 _createDefaultDrip(dripName); 419 420 vm.prank(drippie.owner()); 421 drippie.status(dripName, Drippie.DripStatus.ACTIVE); 422 423 _warpToExecutable(dripName); 424 425 vm.prank(drippie.owner()); 426 427 drippie.drip(dripName); 428 429 vm.prank(drippie.owner()); 430 431 vm.expectRevert("Drippie: drip interval has not elapsed since last drip"); 432 433 drippie.drip(dripName); 434 435 _warpToExecutable(dripName); 436 437 vm.expectEmit(address(drippie)); 438 emit DripExecuted({ nameref: dripName, name: dripName, executor: address(this), timestamp: block.timestamp }); 439 440 drippie.drip(dripName); 441 } 442 443 /// @notice It should revert if attempting to trigger a drip that does not exist. 444 /// Note that the drip was never created at the beginning of the test. 445 function test_drip_notExist_reverts() external { 446 vm.prank(drippie.owner()); 447 448 vm.expectRevert("Drippie: selected drip does not exist or is not currently active"); 449 450 drippie.drip(dripName); 451 } 452 453 /// @notice The owner cannot trigger the drip when it is paused. 454 function test_not_active_reverts() external { 455 _createDefaultDrip(dripName); 456 457 Drippie.DripStatus status = drippie.dripStatus(dripName); 458 assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED)); 459 460 vm.prank(drippie.owner()); 461 462 vm.expectRevert("Drippie: selected drip does not exist or is not currently active"); 463 464 drippie.drip(dripName); 465 } 466 467 /// @notice The interval must be 0 if reentrant is set on the config. 468 function test_reentrant_succeeds() external { 469 address owner = drippie.owner(); 470 Drippie.DripConfig memory cfg = _defaultConfig(); 471 cfg.reentrant = true; 472 cfg.interval = 0; 473 474 vm.prank(owner); 475 vm.expectEmit(address(drippie)); 476 emit DripCreated(dripName, dripName, cfg); 477 drippie.create(dripName, cfg); 478 479 Drippie.DripConfig memory _cfg = drippie.dripConfig(dripName); 480 assertEq(_cfg.reentrant, true); 481 assertEq(_cfg.interval, 0); 482 } 483 484 /// @notice A non zero interval when reentrant is true will cause a revert 485 /// when creating a drip. 486 function test_drip_reentrant_reverts() external { 487 address owner = drippie.owner(); 488 Drippie.DripConfig memory cfg = _defaultConfig(); 489 cfg.reentrant = true; 490 cfg.interval = 1; 491 492 vm.prank(owner); 493 494 vm.expectRevert("Drippie: if allowing reentrant drip, must set interval to zero"); 495 drippie.create(dripName, cfg); 496 } 497 498 /// @notice If reentrant is false and the interval is 0 then it should 499 /// revert when the drip is created. 500 function test_notReentrant_zeroInterval_reverts() external { 501 address owner = drippie.owner(); 502 Drippie.DripConfig memory cfg = _defaultConfig(); 503 cfg.reentrant = false; 504 cfg.interval = 0; 505 506 vm.prank(owner); 507 508 vm.expectRevert("Drippie: interval must be greater than zero if drip is not reentrant"); 509 drippie.create(dripName, cfg); 510 } 511 }