Configurando o Projeto Foundry
Vamos criar um novo projeto Foundry para nosso smart contract.
Se você está usando um computador Windows, o Projeto Foundry só pode ser criado usando o Git Bash, não no
Command PromptouPowershell.
Vá para o diretório prediction-market:
# Criar um novo projeto Foundry
forge init contracts
Você verá algo assim:
Initializing forge project...
Installing dependencies...
Installed forge-std
Criar os Arquivos do Contrato
Vá para a pasta contracts:
cd contracts
- Instale os Contratos OpenZeppelin (necessários pelo ReceiverTemplate):
forge install OpenZeppelin/openzeppelin-contracts
- Crie o diretório de interfaces:
Na pasta contracts, crie a pasta src/interfaces:
mkdir -p src/interfaces
- Crie os arquivos de interface:
Crie src/interfaces/IReceiver.sol:
Copie e cole o código abaixo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
interface IReceiver is IERC165 {
function onReport(bytes calldata metadata, bytes calldata report) external;
}
Crie src/interfaces/ReceiverTemplate.sol:
O ReceiverTemplate fornece validação do endereço do forwarder, validação opcional do workflow, suporte ERC165 e utilitários de decodificação de metadados. Copie a implementação completa:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IReceiver} from "./IReceiver.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/// @title ReceiverTemplate - Abstract receiver with optional permission controls
/// @notice Provides flexible, updatable security checks for receiving workflow reports
/// @dev The forwarder address is required at construction time for security.
/// Additional permission fields can be configured using setter functions.
abstract contract ReceiverTemplate is IReceiver, Ownable {
// Required permission field at deployment, configurable after
address private s_forwarderAddress; // If set, only this address can call onReport
// Optional permission fields (all default to zero = disabled)
address private s_expectedAuthor; // If set, only reports from this workflow owner are accepted
bytes10 private s_expectedWorkflowName; // Only validated when s_expectedAuthor is also set
bytes32 private s_expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted
// Hex character lookup table for bytes-to-hex conversion
bytes private constant HEX_CHARS = "0123456789abcdef";
// Custom errors
error InvalidForwarderAddress();
error InvalidSender(address sender, address expected);
error InvalidAuthor(address received, address expected);
error InvalidWorkflowName(bytes10 received, bytes10 expected);
error InvalidWorkflowId(bytes32 received, bytes32 expected);
error WorkflowNameRequiresAuthorValidation();
// Events
event ForwarderAddressUpdated(address indexed previousForwarder, address indexed newForwarder);
event ExpectedAuthorUpdated(address indexed previousAuthor, address indexed newAuthor);
event ExpectedWorkflowNameUpdated(bytes10 indexed previousName, bytes10 indexed newName);
event ExpectedWorkflowIdUpdated(bytes32 indexed previousId, bytes32 indexed newId);
event SecurityWarning(string message);
/// @notice Constructor sets msg.sender as the owner and configures the forwarder address
/// @param _forwarderAddress The address of the Chainlink Forwarder contract (cannot be address(0))
/// @dev The forwarder address is required for security - it ensures only verified reports are processed
constructor(
address _forwarderAddress
) Ownable(msg.sender) {
if (_forwarderAddress == address(0)) {
revert InvalidForwarderAddress();
}
s_forwarderAddress = _forwarderAddress;
emit ForwarderAddressUpdated(address(0), _forwarderAddress);
}
/// @notice Returns the configured forwarder address
/// @return The forwarder address (address(0) if disabled)
function getForwarderAddress() external view returns (address) {
return s_forwarderAddress;
}
/// @notice Returns the expected workflow author address
/// @return The expected author address (address(0) if not set)
function getExpectedAuthor() external view returns (address) {
return s_expectedAuthor;
}
/// @notice Returns the expected workflow name
/// @return The expected workflow name (bytes10(0) if not set)
function getExpectedWorkflowName() external view returns (bytes10) {
return s_expectedWorkflowName;
}
/// @notice Returns the expected workflow ID
/// @return The expected workflow ID (bytes32(0) if not set)
function getExpectedWorkflowId() external view returns (bytes32) {
return s_expectedWorkflowId;
}
/// @inheritdoc IReceiver
/// @dev Performs optional validation checks based on which permission fields are set
function onReport(
bytes calldata metadata,
bytes calldata report
) external override {
// Security Check 1: Verify caller is the trusted Chainlink Forwarder (if configured)
if (s_forwarderAddress != address(0) && msg.sender != s_forwarderAddress) {
revert InvalidSender(msg.sender, s_forwarderAddress);
}
// Security Checks 2-4: Verify workflow identity - ID, owner, and/or name (if any are configured)
if (s_expectedWorkflowId != bytes32(0) || s_expectedAuthor != address(0) || s_expectedWorkflowName != bytes10(0)) {
(bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata);
if (s_expectedWorkflowId != bytes32(0) && workflowId != s_expectedWorkflowId) {
revert InvalidWorkflowId(workflowId, s_expectedWorkflowId);
}
if (s_expectedAuthor != address(0) && workflowOwner != s_expectedAuthor) {
revert InvalidAuthor(workflowOwner, s_expectedAuthor);
}
// ================================================================
// WORKFLOW NAME VALIDATION - REQUIRES AUTHOR VALIDATION
// ================================================================
// Do not rely on workflow name validation alone. Workflow names are unique
// per owner, but not across owners.
// Furthermore, workflow names use 40-bit truncation (bytes10), making collisions possible.
// Therefore, workflow name validation REQUIRES author (workflow owner) validation.
// The code enforces this dependency at runtime.
// ================================================================
if (s_expectedWorkflowName != bytes10(0)) {
// Author must be configured if workflow name is used
if (s_expectedAuthor == address(0)) {
revert WorkflowNameRequiresAuthorValidation();
}
// Validate workflow name matches (author already validated above)
if (workflowName != s_expectedWorkflowName) {
revert InvalidWorkflowName(workflowName, s_expectedWorkflowName);
}
}
}
_processReport(report);
}
/// @notice Updates the forwarder address that is allowed to call onReport
/// @param _forwarder The new forwarder address
/// @dev WARNING: Setting to address(0) disables forwarder validation.
/// This makes your contract INSECURE - anyone can call onReport() with arbitrary data.
/// Only use address(0) if you fully understand the security implications.
function setForwarderAddress(
address _forwarder
) external onlyOwner {
address previousForwarder = s_forwarderAddress;
// Emit warning if disabling forwarder check
if (_forwarder == address(0)) {
emit SecurityWarning("Forwarder address set to zero - contract is now INSECURE");
}
s_forwarderAddress = _forwarder;
emit ForwarderAddressUpdated(previousForwarder, _forwarder);
}
/// @notice Updates the expected workflow owner address
/// @param _author The new expected author address (use address(0) to disable this check)
function setExpectedAuthor(
address _author
) external onlyOwner {
address previousAuthor = s_expectedAuthor;
s_expectedAuthor = _author;
emit ExpectedAuthorUpdated(previousAuthor, _author);
}
/// @notice Updates the expected workflow name from a plaintext string
/// @param _name The workflow name as a string (use empty string "" to disable this check)
/// @dev IMPORTANT: Workflow name validation REQUIRES author validation to be enabled.
/// The workflow name uses only 40-bit truncation, making collision attacks feasible
/// when used alone. However, since workflow names are unique per owner, validating
/// both the name AND the author address provides adequate security.
/// You must call setExpectedAuthor() before or after calling this function.
/// The name is hashed using SHA256 and truncated to bytes10.
function setExpectedWorkflowName(
string calldata _name
) external onlyOwner {
bytes10 previousName = s_expectedWorkflowName;
if (bytes(_name).length == 0) {
s_expectedWorkflowName = bytes10(0);
emit ExpectedWorkflowNameUpdated(previousName, bytes10(0));
return;
}
// Convert workflow name to bytes10:
// SHA256 hash → hex encode → take first 10 chars → hex encode those chars
bytes32 hash = sha256(bytes(_name));
bytes memory hexString = _bytesToHexString(abi.encodePacked(hash));
bytes memory first10 = new bytes(10);
for (uint256 i = 0; i < 10; i++) {
first10[i] = hexString[i];
}
s_expectedWorkflowName = bytes10(first10);
emit ExpectedWorkflowNameUpdated(previousName, s_expectedWorkflowName);
}
/// @notice Updates the expected workflow ID
/// @param _id The new expected workflow ID (use bytes32(0) to disable this check)
function setExpectedWorkflowId(
bytes32 _id
) external onlyOwner {
bytes32 previousId = s_expectedWorkflowId;
s_expectedWorkflowId = _id;
emit ExpectedWorkflowIdUpdated(previousId, _id);
}
/// @notice Helper function to convert bytes to hex string
/// @param data The bytes to convert
/// @return The hex string representation
function _bytesToHexString(
bytes memory data
) private pure returns (bytes memory) {
bytes memory hexString = new bytes(data.length * 2);
for (uint256 i = 0; i < data.length; i++) {
hexString[i * 2] = HEX_CHARS[uint8(data[i] >> 4)];
hexString[i * 2 + 1] = HEX_CHARS[uint8(data[i] & 0x0f)];
}
return hexString;
}
/// @notice Extracts all metadata fields from the onReport metadata parameter
/// @param metadata The metadata bytes encoded using abi.encodePacked(workflowId, workflowName, workflowOwner)
/// @return workflowId The unique identifier of the workflow (bytes32)
/// @return workflowName The name of the workflow (bytes10)
/// @return workflowOwner The owner address of the workflow
function _decodeMetadata(
bytes memory metadata
) internal pure returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner) {
// Metadata structure (encoded using abi.encodePacked by the Forwarder):
// - First 32 bytes: length of the byte array (standard for dynamic bytes)
// - Offset 32, size 32: workflow_id (bytes32)
// - Offset 64, size 10: workflow_name (bytes10)
// - Offset 74, size 20: workflow_owner (address)
assembly {
workflowId := mload(add(metadata, 32))
workflowName := mload(add(metadata, 64))
workflowOwner := shr(mul(12, 8), mload(add(metadata, 74)))
}
return (workflowId, workflowName, workflowOwner);
}
/// @notice Abstract function to process the report data
/// @param report The report calldata containing your workflow's encoded data
/// @dev Implement this function with your contract's business logic
function _processReport(
bytes calldata report
) internal virtual;
/// @inheritdoc IERC165
function supportsInterface(
bytes4 interfaceId
) public pure virtual override returns (bool) {
return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
- Na pasta
contracts, Atualizefoundry.tomlpara adicionar o remapping do OpenZeppelin:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
"@openzeppelin/=lib/openzeppelin-contracts/"
]
🎉 Está tudo pronto para entender e criar o smart contract principal: PredictionMarket.sol no próximo passo!