Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/MessageVault.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {IReceiver} from "./keystone/IReceiver.sol";
import {IERC165} from "./keystone/IERC165.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/// @title MessageVault - A contract for storing message hashes on-chain with workflow validation
/// @notice This contract stores message hashes and emits full content in events
/// @dev Uses workflow owner and name validation. Contract owner can update expected values.
contract MessageVault is IReceiver, Ownable {
// Expected workflow validation values (modifiable by owner)
address public expectedWorkflowOwner;
bytes10 public expectedWorkflowName;
// Trusted Chainlink KeystoneForwarder address (immutable)
address public immutable FORWARDER_ADDRESS;
// Custom errors
error InvalidSender(address sender, address expected);
error InvalidWorkflowOwner(address received, address expected);
error InvalidWorkflowName(bytes10 received, bytes10 expected);
// Events for configuration changes
event ExpectedWorkflowOwnerUpdated(address indexed oldOwner, address indexed newOwner);
event ExpectedWorkflowNameUpdated(bytes10 indexed oldName, bytes10 indexed newName);
struct MessageRecord {
bytes32 contentHash;
uint256 timestamp;
}
// Array of all message records (only hashes stored)
MessageRecord[] public messageRecords;
// Total message count
uint256 private totalMessages;
event MessageStored(uint256 indexed messageId, bytes32 indexed messageHash, string message, uint256 timestamp);
/// @notice Constructor to set initial expected workflow values and forwarder
/// @param _forwarderAddress Address of the Chainlink KeystoneForwarder
/// @param _expectedWorkflowOwner Expected workflow owner address
/// @param _expectedWorkflowName Expected workflow name
constructor(address _forwarderAddress, address _expectedWorkflowOwner, bytes10 _expectedWorkflowName)
Ownable(msg.sender)
{
FORWARDER_ADDRESS = _forwarderAddress;
expectedWorkflowOwner = _expectedWorkflowOwner;
expectedWorkflowName = _expectedWorkflowName;
}
/// @notice Update expected workflow owner (only callable by contract owner)
/// @param _newWorkflowOwner New workflow owner address
function setExpectedWorkflowOwner(address _newWorkflowOwner) external onlyOwner {
address oldOwner = expectedWorkflowOwner;
expectedWorkflowOwner = _newWorkflowOwner;
emit ExpectedWorkflowOwnerUpdated(oldOwner, _newWorkflowOwner);
}
/// @notice Update expected workflow name (only callable by contract owner)
/// @param _newWorkflowName New workflow name (10 bytes)
function setExpectedWorkflowName(bytes10 _newWorkflowName) external onlyOwner {
bytes10 oldName = expectedWorkflowName;
expectedWorkflowName = _newWorkflowName;
emit ExpectedWorkflowNameUpdated(oldName, _newWorkflowName);
}
/// @notice IReceiver implementation for Chainlink CRE
/// @dev This is called when the workflow sends a report to this contract
/// @dev rawReport format: ABI-encoded (string[] messages)
/// @dev Multi-layered security: forwarder check + workflow validation
function onReport(bytes calldata metadata, bytes calldata rawReport) external override {
// Layer 1: Verify caller is the trusted forwarder
if (msg.sender != FORWARDER_ADDRESS) {
revert InvalidSender(msg.sender, FORWARDER_ADDRESS);
}
// Layer 2: Decode and validate workflow metadata
(address workflowOwner, bytes10 workflowName) = _decodeMetadata(metadata);
if (workflowOwner != expectedWorkflowOwner) {
revert InvalidWorkflowOwner(workflowOwner, expectedWorkflowOwner);
}
if (workflowName != expectedWorkflowName) {
revert InvalidWorkflowName(workflowName, expectedWorkflowName);
}
// Decode ABI-encoded data: (string[])
string[] memory messages = abi.decode(rawReport, (string[]));
require(messages.length > 0, "At least one message required");
// Store each message hash and emit full content
uint256 storedCount = 0;
uint256 timestamp = block.timestamp;
for (uint256 i = 0; i < messages.length; i++) {
// Skip empty messages
if (bytes(messages[i]).length == 0) {
continue;
}
bytes32 hash = keccak256(abi.encodePacked(messages[i]));
// Create and store message record (only hash, not content)
MessageRecord memory newRecord = MessageRecord({contentHash: hash, timestamp: timestamp});
messageRecords.push(newRecord);
uint256 messageId = messageRecords.length - 1;
totalMessages++;
storedCount++;
// Emit event with full content (for off-chain indexing)
emit MessageStored(messageId, hash, messages[i], timestamp);
}
require(storedCount > 0, "No valid messages to store");
}
/// @notice Get total number of messages stored
function getTotalMessages() external view returns (uint256) {
return totalMessages;
}
/// @notice Get a specific message record by ID (hash and timestamp only, no content)
function getMessageRecord(uint256 messageId) external view returns (bytes32 contentHash, uint256 timestamp) {
require(messageId < messageRecords.length, "Message does not exist");
MessageRecord memory record = messageRecords[messageId];
return (record.contentHash, record.timestamp);
}
/// @notice Verify a message content against a stored hash
function verifyMessage(uint256 messageId, string calldata content) external view returns (bool) {
require(messageId < messageRecords.length, "Message does not exist");
bytes32 hash = keccak256(abi.encodePacked(content));
return messageRecords[messageId].contentHash == hash;
}
/// @notice Extracts the workflow name and the workflow owner from the metadata parameter of onReport
/// @param metadata The metadata in bytes format
/// @return workflowOwner The owner of the workflow
/// @return workflowName The name of the workflow
function _decodeMetadata(bytes memory metadata) internal pure returns (address, bytes10) {
address workflowOwner;
bytes10 workflowName;
// (first 32 bytes contain length of the byte array)
// workflow_id // offset 32, size 32
// workflow_name // offset 64, size 10
// workflow_owner // offset 74, size 20
// report_name // offset 94, size 2
assembly {
// no shifting needed for bytes10 type
workflowName := mload(add(metadata, 64))
workflowOwner := shr(mul(12, 8), mload(add(metadata, 74)))
}
return (workflowOwner, workflowName);
}
/// @notice Override supportsInterface to include IReceiver
function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) {
return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
"
},
"src/keystone/IReceiver.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IERC165} from "./IERC165.sol";
/// @title IReceiver - receives keystone reports
/// @notice Implementations must support the IReceiver interface through ERC165.
interface IReceiver is IERC165 {
/// @notice Handles incoming keystone reports.
/// @dev If this function call reverts, it can be retried with a higher gas
/// limit. The receiver is responsible for discarding stale reports.
/// @param metadata Report's metadata.
/// @param report Workflow report.
function onReport(bytes calldata metadata, bytes calldata report) external;
}
"
},
"src/keystone/IERC165.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
"
},
"lib/openzeppelin-contracts/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
"
}
},
"settings": {
"remappings": [
"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
"erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
"forge-std/=lib/forge-std/src/",
"halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/",
"openzeppelin-contracts/=lib/openzeppelin-contracts/"
],
"optimizer": {
"enabled": false,
"runs": 200
},
"metadata": {
"useLiteralContent": false,
"bytecodeHash": "ipfs",
"appendCBOR": true
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
},
"evmVersion": "prague",
"viaIR": false
}
}}
Submitted on: 2025-11-03 15:25:21
Comments
Log in to comment.
No comments yet.