Skip to content

Commit

Permalink
feat: cannot hold 2 responses for the same attestation
Browse files Browse the repository at this point in the history
  • Loading branch information
0xneves committed Jul 14, 2024
1 parent 7176ae8 commit c9bc183
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 5 deletions.
3 changes: 3 additions & 0 deletions src/interfaces/IResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ interface IResolver {
/// @dev Checks if a title is allowed to be attested.
function allowedAttestationTitles(string memory title) external view returns (bool);

/// @dev Validates if an attestation can have a response.
function cannotReply(bytes32 uid) external view returns (bool);

/// @dev Checks which action a role can perform on a schema.
function allowedSchemas(bytes32 uid) external view returns (Action);

Expand Down
22 changes: 18 additions & 4 deletions src/resolver/Resolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"
import { AccessDenied, InvalidEAS, InvalidLength, uncheckedInc, EMPTY_UID, NO_EXPIRATION_TIME } from "../Common.sol";

error AlreadyCheckedOut();
error AlreadyHasResponse();
error InsufficientValue();
error InvalidAttestationTitle();
error InvalidExpiration();
Expand All @@ -32,12 +33,15 @@ contract Resolver is IResolver, AccessControl {
// Maps addresses to booleans to check if a Villager has checked out
mapping(address => bool) private _checkedOutVillagers;

// Maps schemas ID and role ID to action
mapping(bytes32 => Action) private _allowedSchemas;

// Maps allowed attestations (Hashed titles that can be attested)
mapping(bytes32 => bool) private _allowedAttestationTitles;

// Maps attestation IDs to boolans (each attestation can only have one active response)
mapping(bytes32 => bool) private _cannotReply;

// Maps schemas ID and role ID to action
mapping(bytes32 => Action) private _allowedSchemas;

/// @dev Creates a new resolver.
/// @param eas The address of the global EAS contract.
constructor(IEAS eas) {
Expand Down Expand Up @@ -76,6 +80,11 @@ contract Resolver is IResolver, AccessControl {
return _allowedAttestationTitles[keccak256(abi.encode(title))];
}

/// @inheritdoc IResolver
function cannotReply(bytes32 uid) public view returns (bool) {
return _cannotReply[uid];
}

/// @inheritdoc IResolver
function allowedSchemas(bytes32 uid) public view returns (Action) {
return _allowedSchemas[uid];
Expand Down Expand Up @@ -126,6 +135,7 @@ contract Resolver is IResolver, AccessControl {
// Schema to revoke a response ( true / false )
if (isActionAllowed(attestation.schema, Action.REPLY)) {
_checkRole(VILLAGER_ROLE, attestation.attester);
_cannotReply[attestation.refUID] = false;
return true;
}

Expand Down Expand Up @@ -201,8 +211,9 @@ contract Resolver is IResolver, AccessControl {
}

/// @dev Attest a response to an event badge emitted by {attestEvent}.
function attestResponse(Attestation calldata attestation) internal view returns (bool) {
function attestResponse(Attestation calldata attestation) internal returns (bool) {
if (!attestation.revocable) revert InvalidRevocability();
if (_cannotReply[attestation.refUID]) revert AlreadyHasResponse();
_checkRole(VILLAGER_ROLE, attestation.attester);

// Checks if the attestation has a non empty reference
Expand All @@ -212,6 +223,9 @@ contract Resolver is IResolver, AccessControl {
// The response is designed to be a reply to a previous attestation
if (attesterRef.recipient != attestation.attester) revert InvalidRefUID();

// Cannot create new responses until this attestation is revoked
_cannotReply[attestation.refUID] = true;

return true;
}

Expand Down
37 changes: 36 additions & 1 deletion test/EAS.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,18 @@ contract ResolverTest is Test {
vm.startPrank(villager);
bytes32 eventUID = attest_event(uids[2], manager, titles[0], "This address changed my mind");

// Attest Responses, then revoke it
// Attest Response
vm.startPrank(manager);
bytes32 responseUID = attest_response(uids[3], villager, eventUID, true);
assert(resolver.cannotReply(eventUID));
// Should fail to attest response again
assert(!try_attest_response(uids[3], villager, eventUID, true));
// Should be able to revoke the response
attest_response_revoke(uids[3], responseUID);
assert(!resolver.cannotReply(eventUID));
// Should be able to re-attest response
attest_response(uids[3], villager, eventUID, false);
assert(resolver.cannotReply(eventUID));

// Check-Out Villager as Himself
vm.startPrank(villager);
Expand Down Expand Up @@ -284,6 +292,33 @@ contract ResolverTest is Test {
}
}

function try_attest_response(
bytes32 schemaUID,
address recipient,
bytes32 refUID,
bool status
) public returns (bool) {
try
eas.attest(
AttestationRequest({
schema: schemaUID,
data: AttestationRequestData({
recipient: recipient,
expirationTime: 0,
revocable: true,
refUID: refUID,
data: abi.encode(status),
value: 0
})
})
)
{
return true;
} catch {
return false;
}
}

function attest_response_revoke(bytes32 schemaUID, bytes32 attestationUID) public {
eas.revoke(
RevocationRequest({
Expand Down
2 changes: 2 additions & 0 deletions test/Resolver.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ contract ResolverTest is Test {
assert(!hasRole(ROOT_ROLE, deployer));
}

function test_return_all_badge_titles() public {}

function hasRole(bytes32 role, address account) public view returns (bool) {
return IAccessControl(address(resolver)).hasRole(role, account);
}
Expand Down

0 comments on commit c9bc183

Please sign in to comment.