All events
On-chain verification
SogliaTreasury
A smart contract records contributions. Soglia reads the receipt on Base, Ethereum or Arbitrum and confirms the pledge only when recipient, asset and amount match.
Source
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
/// @title SogliaTreasury
/// @notice Permissionless event treasuries. First openEvent wins the slug hash.
/// ETH and ERC-20 land on the organiser payout address. Soglia verifies
/// ContributionRecorded (or a direct transfer to the same payout) on-chain.
contract SogliaTreasury {
struct EventConfig {
address payout;
address opener;
bool active;
}
mapping(bytes32 eventId => EventConfig) public events;
event EventOpened(bytes32 indexed eventId, address payout, address opener);
event ContributionRecorded(
bytes32 indexed eventId,
bytes32 indexed listId,
address indexed donor,
address token,
uint256 amount,
bytes32 receiptId
);
error EventExists();
error UnknownEvent();
error ZeroAddress();
error ZeroAmount();
error PayoutFailed();
error TransferFailed();
function openEvent(bytes32 eventId, address payout) external {
if (payout == address(0)) revert ZeroAddress();
if (events[eventId].active) revert EventExists();
events[eventId] = EventConfig({ payout: payout, opener: msg.sender, active: true });
emit EventOpened(eventId, payout, msg.sender);
}
function contribute(bytes32 eventId, bytes32 listId) external payable {
EventConfig memory ev = events[eventId];
if (!ev.active) revert UnknownEvent();
if (msg.value == 0) revert ZeroAmount();
bytes32 receiptId = keccak256(
abi.encode(block.chainid, block.number, msg.sender, eventId, listId, msg.value, address(0))
);
emit ContributionRecorded(eventId, listId, msg.sender, address(0), msg.value, receiptId);
(bool ok, ) = ev.payout.call{ value: msg.value }("");
if (!ok) revert PayoutFailed();
}
function contributeToken(bytes32 eventId, bytes32 listId, address token, uint256 amount) external {
EventConfig memory ev = events[eventId];
if (!ev.active) revert UnknownEvent();
if (token == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
bool sent = IERC20(token).transferFrom(msg.sender, ev.payout, amount);
if (!sent) revert TransferFailed();
bytes32 receiptId = keccak256(
abi.encode(block.chainid, block.number, msg.sender, eventId, listId, amount, token)
);
emit ContributionRecorded(eventId, listId, msg.sender, token, amount, receiptId);
}
}