diff --git a/packages/contracts-bedrock/snapshots/abi/CheckSecrets.json b/packages/contracts-bedrock/snapshots/abi/CheckSecrets.json new file mode 100644 index 000000000000..b0be49aadd11 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/CheckSecrets.json @@ -0,0 +1,102 @@ +[ + { + "inputs": [ + { + "internalType": "bytes", + "name": "_params", + "type": "bytes" + } + ], + "name": "check", + "outputs": [ + { + "internalType": "bool", + "name": "execute_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_secret", + "type": "bytes" + } + ], + "name": "reveal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "revealedSecrets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "secretHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "secret", + "type": "bytes" + } + ], + "name": "SecretRevealed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "delay", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "secretHashMustExist", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "secretHashMustNotExist", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct CheckSecrets.Params", + "name": "params", + "type": "tuple" + } + ], + "name": "_EventToExposeStructInABI__Params", + "type": "event" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/CheckSecrets.json b/packages/contracts-bedrock/snapshots/storageLayout/CheckSecrets.json new file mode 100644 index 000000000000..accf9c6d643b --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/CheckSecrets.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "32", + "label": "revealedSecrets", + "offset": 0, + "slot": "0", + "type": "mapping(bytes32 => uint256)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/periphery/drippie/dripchecks/CheckSecrets.sol b/packages/contracts-bedrock/src/periphery/drippie/dripchecks/CheckSecrets.sol new file mode 100644 index 000000000000..f008f62cb5d9 --- /dev/null +++ b/packages/contracts-bedrock/src/periphery/drippie/dripchecks/CheckSecrets.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { IDripCheck } from "../IDripCheck.sol"; + +/// @title CheckSecrets +/// @notice DripCheck that checks if specific secrets exist (or not). Supports having a secret that +/// must exist for the check to pass as well as a second secret that must not exist. First +/// secret can be revealed to begin the drip, second secret can be revealed to stop it. +contract CheckSecrets is IDripCheck { + struct Params { + uint256 delay; + bytes32 secretHashMustExist; + bytes32 secretHashMustNotExist; + } + + /// @notice External event used to help client-side tooling encode parameters. + /// @param params Parameters to encode. + event _EventToExposeStructInABI__Params(Params params); + + /// @notice Event emitted when a secret is revealed. + event SecretRevealed(bytes32 indexed secretHash, bytes secret); + + /// @notice Keeps track of when secrets were revealed. + mapping(bytes32 => uint256) public revealedSecrets; + + /// @inheritdoc IDripCheck + function check(bytes memory _params) external view returns (bool execute_) { + Params memory params = abi.decode(_params, (Params)); + + // Check that the secrets have/have not been revealed. + execute_ = ( + revealedSecrets[params.secretHashMustExist] > 0 + && block.timestamp >= revealedSecrets[params.secretHashMustExist] + params.delay + && revealedSecrets[params.secretHashMustNotExist] == 0 + ); + } + + /// @notice Reveal a secret. + /// @param _secret Secret to reveal. + function reveal(bytes memory _secret) external { + bytes32 secretHash = keccak256(_secret); + require(revealedSecrets[secretHash] == 0, "CheckSecrets: secret already revealed"); + revealedSecrets[secretHash] = block.timestamp; + emit SecretRevealed(secretHash, _secret); + } +} diff --git a/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol new file mode 100644 index 000000000000..5afb843df3a5 --- /dev/null +++ b/packages/contracts-bedrock/test/periphery/drippie/dripchecks/CheckSecrets.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { CheckSecrets } from "src/periphery/drippie/dripchecks/CheckSecrets.sol"; + +/// @title CheckSecretsTest +contract CheckSecretsTest is Test { + /// @notice Event emitted when a secret is revealed. + event SecretRevealed(bytes32 indexed secretHash, bytes secret); + + /// @notice An instance of the CheckSecrets contract. + CheckSecrets c; + + /// @notice A secret that must exist. + bytes secretMustExist = bytes(string("secretMustExist")); + + /// @notice A secret that must not exist. + bytes secretMustNotExist = bytes(string("secretMustNotExist")); + + /// @notice A delay period for the check. + uint256 delay = 100; + + /// @notice Deploy the `CheckSecrets` contract. + function setUp() external { + c = new CheckSecrets(); + } + + /// @notice Test that basic secret revealing works. + function test_reveal_succeeds() external { + // Simple reveal and check assertions. + vm.expectEmit(address(c)); + emit SecretRevealed(keccak256(secretMustExist), secretMustExist); + c.reveal(secretMustExist); + assertEq(c.revealedSecrets(keccak256(secretMustExist)), block.timestamp); + } + + /// @notice Test that revealing the same secret twice does not work. + function test_reveal_twice_fails() external { + // Reveal the secret once. + uint256 ts = block.timestamp; + c.reveal(secretMustExist); + assertEq(c.revealedSecrets(keccak256(secretMustExist)), ts); + + // Forward time and reveal again, should fail, same original timestamp. + vm.warp(ts + 1); + vm.expectRevert("CheckSecrets: secret already revealed"); + c.reveal(secretMustExist); + assertEq(c.revealedSecrets(keccak256(secretMustExist)), ts); + } + + /// @notice Test that the check function returns true when the first secret is revealed but the + /// second secret is still hidden and the delay period has elapsed when the delay + /// period is non-zero. Here we warp to exactly the delay period. + function test_check_secretRevealedWithDelayEq_succeeds() external { + CheckSecrets.Params memory p = CheckSecrets.Params({ + delay: delay, + secretHashMustExist: keccak256(secretMustExist), + secretHashMustNotExist: keccak256(secretMustNotExist) + }); + + // Reveal the secret that must exist. + c.reveal(secretMustExist); + + // Forward time to the delay period. + vm.warp(block.timestamp + delay); + + // Beyond the delay, secret revealed, check should succeed. + assertEq(c.check(abi.encode(p)), true); + } + + /// @notice Test that the check function returns true when the first secret is revealed but the + /// second secret is still hidden and the delay period has elapsed when the delay + /// period is non-zero. Here we warp to after the delay period. + function test_check_secretRevealedWithDelayGt_succeeds() external { + CheckSecrets.Params memory p = CheckSecrets.Params({ + delay: delay, + secretHashMustExist: keccak256(secretMustExist), + secretHashMustNotExist: keccak256(secretMustNotExist) + }); + + // Reveal the secret that must exist. + c.reveal(secretMustExist); + + // Forward time to after the delay period. + vm.warp(block.timestamp + delay + 1); + + // Beyond the delay, secret revealed, check should succeed. + assertEq(c.check(abi.encode(p)), true); + } + + /// @notice Test that the check function returns true when the first secret is revealed but the + /// second secret is still hidden and the delay period is zero, meaning the reveal can + /// happen in the same block as the execution. + function test_check_secretRevealedZeroDelay_succeeds() external { + CheckSecrets.Params memory p = CheckSecrets.Params({ + delay: 0, + secretHashMustExist: keccak256(secretMustExist), + secretHashMustNotExist: keccak256(secretMustNotExist) + }); + + // Reveal the secret that must exist. + c.reveal(secretMustExist); + + // Note we don't need to forward time here. + // Secret revealed, no delay, check should succeed. + assertEq(c.check(abi.encode(p)), true); + } + + /// @notice Test that the check function returns false when the first secret is revealed but + /// the delay period has not yet elapsed. + function test_check_secretRevealedBeforeDelay_fails() external { + CheckSecrets.Params memory p = CheckSecrets.Params({ + delay: delay, + secretHashMustExist: keccak256(secretMustExist), + secretHashMustNotExist: keccak256(secretMustNotExist) + }); + + // Reveal the secret that must exist. + c.reveal(secretMustExist); + + // Forward time to before the delay period. + vm.warp(block.timestamp + delay - 1); + + // Not beyond the delay, check should fail. + assertEq(c.check(abi.encode(p)), false); + } + + /// @notice Test that the check function returns false when the first secret is not revealed. + function test_check_secretNotRevealed_fails() external { + CheckSecrets.Params memory p = CheckSecrets.Params({ + delay: delay, + secretHashMustExist: keccak256(secretMustExist), + secretHashMustNotExist: keccak256(secretMustNotExist) + }); + + // Forward beyond the delay period. + vm.warp(block.timestamp + delay + 1); + + // Secret not revealed, check should fail. + assertEq(c.check(abi.encode(p)), false); + } + + /// @notice Test that the check function returns false when the second secret is revealed. + function test_check_secondSecretRevealed_fails() external { + CheckSecrets.Params memory p = CheckSecrets.Params({ + delay: delay, + secretHashMustExist: keccak256(secretMustExist), + secretHashMustNotExist: keccak256(secretMustNotExist) + }); + + // Reveal the secret that must not exist. + c.reveal(secretMustNotExist); + + // Forward beyond the delay period. + vm.warp(block.timestamp + delay + 1); + + // Both secrets revealed, check should fail. + assertEq(c.check(abi.encode(p)), false); + } + + /// @notice Test that the check function returns false when the second secret is revealed even + /// though the first secret is also revealed. + function test_check_firstAndSecondSecretRevealed_fails() external { + CheckSecrets.Params memory p = CheckSecrets.Params({ + delay: delay, + secretHashMustExist: keccak256(secretMustExist), + secretHashMustNotExist: keccak256(secretMustNotExist) + }); + + // Reveal the secret that must exist. + c.reveal(secretMustExist); + + // Reveal the secret that must not exist. + c.reveal(secretMustNotExist); + + // Forward beyond the delay period. + vm.warp(block.timestamp + delay + 1); + + // Both secrets revealed, check should fail. + assertEq(c.check(abi.encode(p)), false); + } +}