Commit 4f2853a1 authored by Mark Tyneway's avatar Mark Tyneway

ctp: add foundry tests for Drippie

This commit adds foundry tests for the `Drippie` smart contract.
Test coverage is improved as additional events that were not tested
before are now being tested for. Also allows a future version of
`Drippie` to be more easily invariant tested.

All hardhat tests are ported 1:1 with additional assertion of
specific `CALL`s and events being emitted.
parent a49e1040
......@@ -13,6 +13,24 @@ AssetReceiverTest:test_withdrawETHwithAmount() (gas: 26108)
AssetReceiverTest:test_attest_bulk() (gas: 611440)
AssetReceiverTest:test_attest_individual() (gas: 538514)
AssetReceiverTest:test_attest_single() (gas: 558962)
Drippie_Test:test_create_fails_twice() (gas: 169499)
Drippie_Test:test_create_success() (gas: 183929)
Drippie_Test:test_drip_amount() (gas: 286145)
Drippie_Test:test_drip_not_exist_fails() (gas: 15158)
Drippie_Test:test_fails_unauthorized() (gas: 15100)
Drippie_Test:test_name_not_exist_fails() (gas: 16157)
Drippie_Test:test_not_active_fails() (gas: 171861)
Drippie_Test:test_set_status_none_fails() (gas: 169439)
Drippie_Test:test_set_status_same_fails() (gas: 169828)
Drippie_Test:test_set_status_success() (gas: 199182)
Drippie_Test:test_should_archive_if_paused_success() (gas: 177944)
Drippie_Test:test_should_not_allow_active_if_archived_fails() (gas: 175362)
Drippie_Test:test_should_not_allow_paused_if_archived_fails() (gas: 175361)
Drippie_Test:test_should_not_archive_if_active_fails() (gas: 176490)
Drippie_Test:test_status_unauthorized_fails() (gas: 167971)
Drippie_Test:test_trigger_one_function() (gas: 335266)
Drippie_Test:test_trigger_two_functions() (gas: 489313)
Drippie_Test:test_twice_in_one_interval_fails() (gas: 304971)
OptimistTest:test_optimist_baseURI() (gas: 116809)
OptimistTest:test_optimist_burn() (gas: 77526)
OptimistTest:test_optimist_initialize() (gas: 23095)
......
//SPDX-License-Identifier: MIT
pragma solidity 0.8.16;
import { Test } from "forge-std/Test.sol";
import { Drippie } from "../universal/drippie/Drippie.sol";
import { IDripCheck } from "../universal/drippie/IDripCheck.sol";
import { CheckTrue } from "../universal/drippie/dripchecks/CheckTrue.sol";
import { SimpleStorage } from "../testing/helpers/SimpleStorage.sol";
/**
* @title TestDrippie
* @notice This is a wrapper contract around Drippie used for testing.
* Returning an entire `Drippie.DripState` causes stack too
* deep errors without `--vir-ir` which causes the compile time
* to go up by ~4x. Each of the methods is a simple getter around
* parts of the `DripState`.
*/
contract TestDrippie is Drippie {
constructor(address owner) Drippie(owner) {}
function dripStatus(string memory name) external view returns (Drippie.DripStatus) {
return drips[name].status;
}
function dripStateLast(string memory name) external view returns (uint256) {
return drips[name].last;
}
function dripConfig(string memory name) external view returns (Drippie.DripConfig memory) {
return drips[name].config;
}
function dripConfigActions(string memory name)
external
view
returns (Drippie.DripAction[] memory)
{
return drips[name].config.actions;
}
function dripConfigCheckParams(string memory name) external view returns (bytes memory) {
return drips[name].config.checkparams;
}
function dripConfigCheckAddress(string memory name) external view returns (address) {
return address(drips[name].config.dripcheck);
}
}
/**
* @title Drippie_Test
* @notice Test coverage of the Drippie contract.
*/
contract Drippie_Test is Test {
/**
* @notice Emitted when a drip is executed.
*/
event DripExecuted(string indexed nameref, string name, address executor, uint256 timestamp);
/**
* @notice Emitted when a drip's status is updated.
*/
event DripStatusUpdated(string indexed nameref, string name, Drippie.DripStatus status);
/**
* @notice Emitted when a drip is created.
*/
event DripCreated(string indexed nameref, string name, Drippie.DripConfig config);
/**
* @notice Address of the test DripCheck. CheckTrue is deployed
* here so it always returns true.
*/
IDripCheck check;
/**
* @notice Address of a SimpleStorage contract. Used to test that
* calls are actually made by Drippie.
*/
SimpleStorage simpleStorage;
/**
* @notice Address of the Drippie contract.
*/
TestDrippie drippie;
/**
* @notice Address of the Drippie owner
*/
address constant alice = address(0x42);
/**
* @notice The name of the default drip
*/
string constant dripName = "foo";
/**
* @notice Set up the test suite by deploying a CheckTrue DripCheck.
* This is a mock that always returns true. Alice is the owner
* and also give drippie 1 ether so that it can balance transfer.
*/
function setUp() external {
check = IDripCheck(new CheckTrue());
simpleStorage = new SimpleStorage();
drippie = new TestDrippie(alice);
vm.deal(address(drippie), 1 ether);
}
/**
* @notice Builds a default Drippie.DripConfig. Uses CheckTrue as the
* dripcheck so it will always be able to do its DripActions.
* Gives a dummy DripAction that does a simple transfer to
* a dummy address.
*/
function _defaultConfig() internal view returns (Drippie.DripConfig memory) {
Drippie.DripAction[] memory actions = new Drippie.DripAction[](1);
actions[0] = Drippie.DripAction({ target: payable(address(0x44)), data: hex"", value: 1 });
return
Drippie.DripConfig({
interval: 100,
dripcheck: check,
reentrant: false,
checkparams: hex"",
actions: actions
});
}
/**
* @notice Creates a default drip using the default drip config.
*/
function _createDefaultDrip(string memory name) internal {
address owner = drippie.owner();
Drippie.DripConfig memory cfg = _defaultConfig();
vm.prank(owner);
drippie.create(name, cfg);
}
/**
* @notice Moves the block's timestamp forward so that it is
* possible to execute a drip.
*/
function _warpToExecutable(string memory name) internal {
Drippie.DripConfig memory config = drippie.dripConfig(name);
vm.warp(config.interval + drippie.dripStateLast(name));
}
/**
* @notice Creates a drip and asserts that it was configured as expected.
*/
function test_create_success() external {
Drippie.DripConfig memory cfg = _defaultConfig();
vm.expectEmit(true, true, true, true);
emit DripCreated(dripName, dripName, cfg);
vm.prank(drippie.owner());
drippie.create(dripName, cfg);
Drippie.DripStatus status = drippie.dripStatus(dripName);
Drippie.DripConfig memory config = drippie.dripConfig(dripName);
assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED));
assertEq(config.interval, cfg.interval);
assertEq(config.reentrant, cfg.reentrant);
assertEq(address(config.dripcheck), address(cfg.dripcheck));
assertEq(config.checkparams, cfg.checkparams);
assertEq(config.actions.length, cfg.actions.length);
for (uint256 i; i < config.actions.length; i++) {
Drippie.DripAction memory a = config.actions[i];
Drippie.DripAction memory b = cfg.actions[i];
assertEq(a.target, b.target);
assertEq(a.data, b.data);
assertEq(a.value, b.value);
}
}
/**
* @notice Ensures that the same drip cannot be created two times.
*/
function test_create_fails_twice() external {
vm.startPrank(drippie.owner());
Drippie.DripConfig memory cfg = _defaultConfig();
drippie.create(dripName, cfg);
vm.expectRevert("Drippie: drip with that name already exists");
drippie.create(dripName, cfg);
vm.stopPrank();
}
/**
* @notice Ensures that only the owner of Drippie can create a drip.
*/
function test_fails_unauthorized() external {
vm.expectRevert("UNAUTHORIZED");
drippie.create(dripName, _defaultConfig());
}
/**
* @notice The owner should be able to set the status of the drip.
*/
function test_set_status_success() external {
vm.expectEmit(true, true, true, true);
emit DripCreated(dripName, dripName, _defaultConfig());
_createDefaultDrip(dripName);
address owner = drippie.owner();
{
Drippie.DripStatus status = drippie.dripStatus(dripName);
assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED));
}
vm.prank(owner);
vm.expectEmit(true, true, true, true);
emit DripStatusUpdated({
nameref: dripName,
name: dripName,
status: Drippie.DripStatus.ACTIVE
});
drippie.status(dripName, Drippie.DripStatus.ACTIVE);
{
Drippie.DripStatus status = drippie.dripStatus(dripName);
assertEq(uint256(status), uint256(Drippie.DripStatus.ACTIVE));
}
vm.prank(owner);
vm.expectEmit(true, true, true, true);
emit DripStatusUpdated({
nameref: dripName,
name: dripName,
status: Drippie.DripStatus.PAUSED
});
drippie.status(dripName, Drippie.DripStatus.PAUSED);
{
Drippie.DripStatus status = drippie.dripStatus(dripName);
assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED));
}
}
/**
* @notice The drip status cannot be set back to NONE after it is created.
*/
function test_set_status_none_fails() external {
_createDefaultDrip(dripName);
vm.prank(drippie.owner());
vm.expectRevert("Drippie: drip status can never be set back to NONE after creation");
drippie.status(dripName, Drippie.DripStatus.NONE);
}
/**
* @notice The owner cannot set the status of the drip to the status that
* it is already set as.
*/
function test_set_status_same_fails() external {
_createDefaultDrip(dripName);
vm.prank(drippie.owner());
vm.expectRevert("Drippie: cannot set drip status to the same status as its current status");
drippie.status(dripName, Drippie.DripStatus.PAUSED);
}
/**
* @notice The owner should be able to archive the drip if it is in the
* paused state.
*/
function test_should_archive_if_paused_success() external {
_createDefaultDrip(dripName);
address owner = drippie.owner();
vm.prank(owner);
vm.expectEmit(true, true, true, true);
emit DripStatusUpdated({
nameref: dripName,
name: dripName,
status: Drippie.DripStatus.ARCHIVED
});
drippie.status(dripName, Drippie.DripStatus.ARCHIVED);
Drippie.DripStatus status = drippie.dripStatus(dripName);
assertEq(uint256(status), uint256(Drippie.DripStatus.ARCHIVED));
}
/**
* @notice The owner should not be able to archive the drip if it is in the
* active state.
*/
function test_should_not_archive_if_active_fails() external {
_createDefaultDrip(dripName);
vm.prank(drippie.owner());
drippie.status(dripName, Drippie.DripStatus.ACTIVE);
vm.prank(drippie.owner());
vm.expectRevert("Drippie: drip must first be paused before being archived");
drippie.status(dripName, Drippie.DripStatus.ARCHIVED);
}
/**
* @notice The owner should not be allowed to pause the drip if it
* has already been archived.
*/
function test_should_not_allow_paused_if_archived_fails() external {
_createDefaultDrip(dripName);
_notAllowFromArchive(dripName, Drippie.DripStatus.PAUSED);
}
/**
* @notice The owner should not be allowed to make the drip active again if
* it has already been archived.
*/
function test_should_not_allow_active_if_archived_fails() external {
_createDefaultDrip(dripName);
_notAllowFromArchive(dripName, Drippie.DripStatus.ACTIVE);
}
/**
* @notice Archive the drip and then attempt to set the status to the passed
* in status.
*/
function _notAllowFromArchive(string memory name, Drippie.DripStatus status) internal {
address owner = drippie.owner();
vm.prank(owner);
drippie.status(name, Drippie.DripStatus.ARCHIVED);
vm.expectRevert("Drippie: drip with that name has been archived and cannot be updated");
vm.prank(owner);
drippie.status(name, status);
}
/**
* @notice Attempt to update a drip that does not exist.
*/
function test_name_not_exist_fails() external {
string memory otherName = "bar";
vm.prank(drippie.owner());
vm.expectRevert("Drippie: drip with that name does not exist and cannot be updated");
assertFalse(keccak256(abi.encode(dripName)) == keccak256(abi.encode(otherName)));
drippie.status(otherName, Drippie.DripStatus.ARCHIVED);
}
/**
* @notice Expect a revert when attempting to set the status when not the owner.
*/
function test_status_unauthorized_fails() external {
_createDefaultDrip(dripName);
vm.expectRevert("UNAUTHORIZED");
drippie.status(dripName, Drippie.DripStatus.ACTIVE);
}
/**
* @notice The drip should execute and be able to transfer value.
*/
function test_drip_amount() external {
_createDefaultDrip(dripName);
vm.prank(drippie.owner());
drippie.status(dripName, Drippie.DripStatus.ACTIVE);
_warpToExecutable(dripName);
vm.expectEmit(true, true, true, true);
emit DripExecuted({
nameref: dripName,
name: dripName,
executor: address(this),
timestamp: block.timestamp
});
Drippie.DripAction[] memory actions = drippie.dripConfigActions(dripName);
assertEq(actions.length, 1);
vm.expectCall(
drippie.dripConfigCheckAddress(dripName),
drippie.dripConfigCheckParams(dripName)
);
vm.expectCall(actions[0].target, actions[0].value, actions[0].data);
drippie.drip(dripName);
}
/**
* @notice A single DripAction should be able to make a state modifying call.
*/
function test_trigger_one_function() external {
Drippie.DripConfig memory cfg = _defaultConfig();
bytes32 key = bytes32(uint256(2));
bytes32 value = bytes32(uint256(3));
// Add in an action
cfg.actions[0] = Drippie.DripAction({
target: payable(address(simpleStorage)),
data: abi.encodeWithSelector(SimpleStorage.set.selector, key, value),
value: 0
});
vm.prank(drippie.owner());
drippie.create(dripName, cfg);
_warpToExecutable(dripName);
vm.prank(drippie.owner());
drippie.status(dripName, Drippie.DripStatus.ACTIVE);
vm.expectCall(
address(simpleStorage),
0,
abi.encodeWithSelector(SimpleStorage.set.selector, key, value)
);
drippie.drip(dripName);
assertEq(simpleStorage.get(key), value);
}
/**
* @notice Multiple drip actions should be able to be triggered with the same check.
*/
function test_trigger_two_functions() external {
Drippie.DripConfig memory cfg = _defaultConfig();
Drippie.DripAction[] memory actions = new Drippie.DripAction[](2);
bytes32 keyOne = bytes32(uint256(2));
bytes32 valueOne = bytes32(uint256(3));
actions[0] = Drippie.DripAction({
target: payable(address(simpleStorage)),
data: abi.encodeWithSelector(simpleStorage.set.selector, keyOne, valueOne),
value: 0
});
bytes32 keyTwo = bytes32(uint256(4));
bytes32 valueTwo = bytes32(uint256(5));
actions[1] = Drippie.DripAction({
target: payable(address(simpleStorage)),
data: abi.encodeWithSelector(simpleStorage.set.selector, keyTwo, valueTwo),
value: 0
});
cfg.actions = actions;
vm.prank(drippie.owner());
drippie.create(dripName, cfg);
_warpToExecutable(dripName);
vm.prank(drippie.owner());
drippie.status(dripName, Drippie.DripStatus.ACTIVE);
vm.expectCall(
drippie.dripConfigCheckAddress(dripName),
drippie.dripConfigCheckParams(dripName)
);
vm.expectCall(
address(simpleStorage),
0,
abi.encodeWithSelector(SimpleStorage.set.selector, keyOne, valueOne)
);
vm.expectCall(
address(simpleStorage),
0,
abi.encodeWithSelector(SimpleStorage.set.selector, keyTwo, valueTwo)
);
drippie.drip(dripName);
assertEq(simpleStorage.get(keyOne), valueOne);
assertEq(simpleStorage.get(keyTwo), valueTwo);
}
/**
* @notice The drips can only be triggered once per interval. Attempt to
* trigger the same drip multiple times in the same interval. Then
* move forward to the next interval and it should trigger.
*/
function test_twice_in_one_interval_fails() external {
_createDefaultDrip(dripName);
vm.prank(drippie.owner());
drippie.status(dripName, Drippie.DripStatus.ACTIVE);
_warpToExecutable(dripName);
vm.prank(drippie.owner());
drippie.drip(dripName);
vm.prank(drippie.owner());
vm.expectRevert("Drippie: drip interval has not elapsed since last drip");
drippie.drip(dripName);
_warpToExecutable(dripName);
vm.expectEmit(true, true, true, true);
emit DripExecuted({
nameref: dripName,
name: dripName,
executor: address(this),
timestamp: block.timestamp
});
drippie.drip(dripName);
}
/**
* @notice It should revert if attempting to trigger a drip that does not exist.
* Note that the drip was never created at the beginning of the test.
*/
function test_drip_not_exist_fails() external {
vm.prank(drippie.owner());
vm.expectRevert("Drippie: selected drip does not exist or is not currently active");
drippie.drip(dripName);
}
/**
* @notice The owner cannot trigger the drip when it is paused.
*/
function test_not_active_fails() external {
_createDefaultDrip(dripName);
Drippie.DripStatus status = drippie.dripStatus(dripName);
assertEq(uint256(status), uint256(Drippie.DripStatus.PAUSED));
vm.prank(drippie.owner());
vm.expectRevert("Drippie: selected drip does not exist or is not currently active");
drippie.drip(dripName);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment