Skip to content

Commit

Permalink
Merge pull request #2 from blockful-io/feat#3SessionBadges
Browse files Browse the repository at this point in the history
Feat#3 session badges
  • Loading branch information
LeonardoVieira1630 authored Oct 28, 2024
2 parents c5835e7 + ef72547 commit 8e727e6
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 3 deletions.
17 changes: 17 additions & 0 deletions src/Common.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,26 @@ struct Attestation {
bytes data; // Custom attestation data.
}

/// @notice A struct representing a single Session.
struct Session {
address host; // Host of the session
string title; // Title of the session
uint256 startTime; // The time when the session was created (Unix timestamp).
uint256 endTime; // The time when the session was ended (Unix timestamp).
}

/// @notice A helper function to work with unchecked iterators in loops.
function uncheckedInc(uint256 i) pure returns (uint256 j) {
unchecked {
j = i + 1;
}
}

/// @dev Helper function to slice a byte array
function slice(bytes memory data, uint256 start, uint256 length) pure returns (bytes memory) {
bytes memory result = new bytes(length);
for (uint i = 0; i < length; i++) {
result[i] = data[start + i];
}
return result;
}
5 changes: 5 additions & 0 deletions src/interfaces/IResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ interface IResolver {
/// @param uid The UID of the schema.
/// @param action The action that the role can perform on the schema.
function setSchema(bytes32 uid, uint256 action) external;

function createSession(
uint256 duration,
string memory sessionTitle
) external returns (bytes32 sessionId);
}
70 changes: 69 additions & 1 deletion src/resolver/Resolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity ^0.8.4;
import { IEAS, Attestation } from "../interfaces/IEAS.sol";
import { IResolver } from "../interfaces/IResolver.sol";
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
import { AccessDenied, InvalidEAS, InvalidLength, uncheckedInc, EMPTY_UID, NO_EXPIRATION_TIME } from "../Common.sol";
import { AccessDenied, InvalidEAS, InvalidLength, uncheckedInc, EMPTY_UID, NO_EXPIRATION_TIME, Session, slice } from "../Common.sol";

error AlreadyHasResponse();
error InsufficientValue();
Expand All @@ -17,6 +17,8 @@ error InvalidRole();
error InvalidWithdraw();
error NotPayable();
error Unauthorized();
error InvalidSession();
error NotHostOfTheSession();

/// @author Blockful | 0xneves
/// @notice ZuVillage Resolver contract for Ethereum Attestation Service.
Expand All @@ -41,9 +43,15 @@ contract Resolver is IResolver, AccessControl {
// Maps schemas ID and role ID to action
mapping(bytes32 => Action) private _allowedSchemas;

// Maps session ids and sessions Structures
mapping(bytes32 => Session) private _session;

// Maps all attestation titles (badge titles) to be retrieved by the frontend
string[] private _attestationTitles;

// Define a constant for default SESSION_DURATION (30 days in seconds)
uint256 private constant DEFAULT_SESSION_DURATION = 30 days;

/// @dev Creates a new resolver.
/// @param eas The address of the global EAS contract.
constructor(IEAS eas) {
Expand Down Expand Up @@ -205,9 +213,32 @@ contract Resolver is IResolver, AccessControl {
(string memory title, ) = abi.decode(attestation.data, (string, string));
if (!_allowedAttestationTitles[keccak256(abi.encode(title))]) revert InvalidAttestationTitle();

// Check if it is a host-only attestation and if the attester is the host
if (isHostOnlyAttestation(title)) {
if (!isAttesterHost(attestation.attester, title)) revert NotHostOfTheSession();
}

return true;
}

/// @dev Checks if the attestation is a host-only attestation
function isHostOnlyAttestation(string memory title) internal pure returns (bool) {
bytes memory titleBytes = bytes(title);
return
titleBytes.length >= 5 &&
(keccak256(abi.encodePacked(slice(titleBytes, 0, 5))) == keccak256("Host_") ||
keccak256(abi.encodePacked(slice(titleBytes, 0, 9))) == keccak256("Attendee_"));
}

/// @dev Checks if the attester is the host of the session
function isAttesterHost(address attester, string memory title) internal view returns (bool) {
bytes memory titleBytes = bytes(title);
string memory sessionTitle = string(slice(titleBytes, 5, titleBytes.length - 5));
bytes32 sessionId = keccak256(abi.encodePacked(attester, sessionTitle));

return _session[sessionId].host == attester;
}

/// @dev Attest a response to an event badge emitted by {attestEvent}.
function attestResponse(Attestation calldata attestation) internal returns (bool) {
if (!attestation.revocable) revert InvalidRevocability();
Expand Down Expand Up @@ -259,6 +290,43 @@ contract Resolver is IResolver, AccessControl {
_allowedSchemas[uid] = Action(action);
}

/// @dev creates a new session
function createSession(
uint256 duration,
string memory sessionTitle
) public returns (bytes32 sessionId) {
if (!hasRole(VILLAGER_ROLE, msg.sender)) revert InvalidRole();
if (duration == 0) revert InvalidSession();
if (bytes(sessionTitle).length == 0) revert InvalidSession();

// Generate a unique session ID
sessionId = keccak256(abi.encodePacked(msg.sender, sessionTitle));

// Check if the session already exists
if (_session[sessionId].host != address(0)) {
revert InvalidSession();
}

uint256 sessionDuration = duration > 0 ? duration : DEFAULT_SESSION_DURATION;
Session memory session = Session({
host: msg.sender,
title: sessionTitle,
startTime: block.timestamp,
endTime: block.timestamp + sessionDuration
});

//Store the session
_session[sessionId] = session;

//Enable the host and attendee attestation related to the session
string memory hostAttestationTitle = string(abi.encodePacked("Host_", sessionTitle));
_allowedAttestationTitles[keccak256(abi.encode(hostAttestationTitle))] = true;
string memory attendeeAttestationTitle = string(abi.encodePacked("Attendee_", sessionTitle));
_allowedAttestationTitles[keccak256(abi.encode(attendeeAttestationTitle))] = true;

return sessionId;
}

/// @dev ETH callback.
receive() external payable virtual {
if (!isPayable()) {
Expand Down
182 changes: 180 additions & 2 deletions test/Resolver.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
pragma solidity ^0.8.13;

import { Test, console2 } from "forge-std/src/Test.sol";
import { Resolver } from "../src/resolver/Resolver.sol";
import { Resolver, NotHostOfTheSession, InvalidRole, InvalidSession } from "../src/resolver/Resolver.sol";
import { IResolver } from "../src/interfaces/IResolver.sol";
import { ISchemaRegistry } from "../src/interfaces/ISchemaRegistry.sol";
import { IEAS } from "../src/interfaces/IEAS.sol";
import { IEAS, AttestationRequest, AttestationRequestData } from "../src/interfaces/IEAS.sol";
import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol";

contract ResolverTest is Test {
Expand Down Expand Up @@ -171,4 +171,182 @@ contract ResolverTest is Test {
function revokeRole(bytes32 role, address account) public {
IAccessControl(address(resolver)).revokeRole(role, account);
}

function test_host_attestation_allowed() public {
bytes32[] memory uids = test_access_control_add_schemas();
address host = roleReceiver;
address atendee = address(0x5678);

string memory sessionTitle = "TestSession";
grantRole(VILLAGER_ROLE, roleReceiver);
grantRole(VILLAGER_ROLE, atendee);

// Create a session
vm.startPrank(host);
resolver.createSession(10, sessionTitle);

// Prepare attestation data
string memory hostTitle = string(abi.encodePacked("Host_", sessionTitle));
bytes memory attestationData = abi.encode(hostTitle, "Test comment");

// Create attestation request
AttestationRequest memory request = AttestationRequest({
schema: uids[2],
data: AttestationRequestData({
recipient: address(0x5678),
expirationTime: 0,
revocable: false,
refUID: bytes32(0),
data: attestationData,
value: 0
})
});

bytes32 attestationUID = eas.attest(request);
vm.stopPrank();
assertTrue(eas.isAttestationValid(attestationUID), "Attestation should be valid");
}

function test_host_attestation_not_allowed() public {
// Setup
bytes32[] memory uids = test_access_control_add_schemas();
address host = roleReceiver;
address atendee = address(0x5678);
grantRole(VILLAGER_ROLE, atendee);
grantRole(VILLAGER_ROLE, host);

string memory sessionTitle = "TestSession";

// Create a session
vm.startPrank(host);
resolver.createSession(10, sessionTitle);

// Prepare attestation data
string memory hostTitle = string(abi.encodePacked("Host_", sessionTitle));
bytes memory attestationData = abi.encode(hostTitle, "Test comment");

// Create attestation request
AttestationRequest memory request = AttestationRequest({
schema: uids[2],
data: AttestationRequestData({
recipient: host,
expirationTime: 0,
revocable: false,
refUID: bytes32(0),
data: attestationData,
value: 0
})
});

// Attempt to attest as a non-host
vm.startPrank(atendee);
vm.expectRevert(NotHostOfTheSession.selector);
eas.attest(request);
vm.stopPrank();
}

function test_create_session_as_villager() public {
address villager = roleReceiver;
string memory sessionTitle = "Test Session";
uint256 duration = 1 days;

grantRole(VILLAGER_ROLE, villager);

vm.startPrank(villager);
bytes32 sessionId = resolver.createSession(duration, sessionTitle);
vm.stopPrank();

assert(sessionId != bytes32(0));
}

function test_create_session_as_non_villager() public {
address nonVillager = address(0x5678);
string memory sessionTitle = "Test Session";
uint256 duration = 1 days;
grantRole(VILLAGER_ROLE, roleReceiver);

vm.startPrank(nonVillager);
vm.expectRevert(InvalidRole.selector);
resolver.createSession(duration, sessionTitle);
}

function test_create_duplicate_session() public {
address villager = roleReceiver;
string memory sessionTitle = "Test Session";
uint256 duration = 1 days;

grantRole(VILLAGER_ROLE, villager);

vm.startPrank(villager);
resolver.createSession(duration, sessionTitle);

vm.expectRevert(InvalidSession.selector);
resolver.createSession(duration, sessionTitle);
vm.stopPrank();
}

function test_session_attestation_titles_allowed() public {
address villager = roleReceiver;
string memory sessionTitle = "Test Session";
uint256 duration = 1 days;

grantRole(VILLAGER_ROLE, villager);

vm.startPrank(villager);
resolver.createSession(duration, sessionTitle);
vm.stopPrank();

string memory hostTitle = string(abi.encodePacked("Host_", sessionTitle));
string memory attendeeTitle = string(abi.encodePacked("Attendee_", sessionTitle));

assert(resolver.allowedAttestationTitles(hostTitle));
assert(resolver.allowedAttestationTitles(attendeeTitle));
}

function test_create_session_with_zero_duration() public {
address villager = roleReceiver;
string memory sessionTitle = "Zero Duration Session";
uint256 duration = 0;

grantRole(VILLAGER_ROLE, villager);

vm.startPrank(villager);
vm.expectRevert(InvalidSession.selector);
resolver.createSession(duration, sessionTitle);
vm.stopPrank();
}

function test_create_session_with_empty_title() public {
address villager = roleReceiver;
string memory sessionTitle = "";
uint256 duration = 1 days;

grantRole(VILLAGER_ROLE, villager);

vm.startPrank(villager);
vm.expectRevert(InvalidSession.selector);
resolver.createSession(duration, sessionTitle);
vm.stopPrank();
}

function test_create_multiple_sessions() public {
address villager = roleReceiver;
string memory sessionTitle1 = "First Session";
string memory sessionTitle2 = "Second Session";
uint256 duration = 1 days;

grantRole(VILLAGER_ROLE, villager);

vm.startPrank(villager);

bytes32 sessionId1 = resolver.createSession(duration, sessionTitle1);
assert(sessionId1 != bytes32(0));

bytes32 sessionId2 = resolver.createSession(duration, sessionTitle2);
assert(sessionId2 != bytes32(0));

assert(sessionId1 != sessionId2);

vm.stopPrank();
}
}

0 comments on commit 8e727e6

Please sign in to comment.