Skip to content

feat: invite struct, event, errors and redeem function implemented#59

Merged
exo404 merged 15 commits intodevfrom
42-refactor-enable-breadfund-creation-without-all-users
Oct 19, 2025
Merged

feat: invite struct, event, errors and redeem function implemented#59
exo404 merged 15 commits intodevfrom
42-refactor-enable-breadfund-creation-without-all-users

Conversation

@exo404
Copy link
Contributor

@exo404 exo404 commented Oct 9, 2025


Technical Spec – Safety Net Invite


1. Background

Problem Statement:
Today, when creating a Safety Net (SafetyNet), all members must be declared upfront. This makes progressive onboarding difficult and slows down organizers. The first design with on-chain stored invites exposed them publicly. A later proposal with counters and Merkle roots was too complex for the core use case.

Context / History:

  • SafetyNets repository
  • Discussion of on-chain invites → discoverability problem.
  • Discussion of counters / Merkle roots → overengineered for MVP.

Stakeholders:

  • Safety Net creators / owners.
  • Prospective members invited to join.
  • Bread maintainers.
  • Frontend / UI developers.

2. Motivation

Goals & Success Stories:

  • Safety Net owner generates an off-chain signed invite.
  • Invite is shared as a link or QR code.
  • Anyone holding the invite can redeem it once to join.
  • Each invite is single-use via a nonce.
  • Nothing is exposed on-chain until redemption.

3. Scope and Approaches

Non-Goals:

Technical Functionality Reasoning for being off scope Tradeoffs
Multi-use invites Simpler MVP: one signature = one invite Cannot share one link with many users
Allowlist / Merkle root Overkill for current goals Useful for enterprise, not needed now
Global counter revocation Not required for MVP Cannot invalidate all invites at once

Value Proposition:

Technical Functionality Value Tradeoffs
EIP-712 signed invites Private, non-discoverable on-chain Requires off-chain signing
Nonce (one-time use) Prevents replay and double use Storage required per invite

Alternative Approaches:

Approach Pros Cons
On-chain stored invites Easy to track, transparent state Publicly visible, like open join

Relevant Metrics:

  • Invite redemption success rate.
  • Number of invites consumed vs. issued.
  • Common failure cases (expired, already used, bad signature).

4. Step-by-Step Flow

4.1 Main (“Happy”) Path

  • Actor: Owner generates a invite using fundId, nonce and signs with EIP-712.
  • Pre-condition: Safety Net exists and has a defined owner.
  • System validates:
    • Nonce not used.
    • Signature matches fund owner.
  • System persists / computes:
    • Marks nonce as used.
    • Adds msg.sender as member.
    • Emits InviteRedeemed.
  • Post-condition: Caller becomes a member of the Safety Net.

4.2 Alternate / Error Paths

# Condition System Action
A1 Invite already used revert Invite used
A2 Invalid signer revert Invalid signer
A3 User already member revert Already member

5. UML Diagrams

Class Diagram

classDiagram
    class SafetyNet {
        +redeemInvite(Invite inv, bytes sig)
        -usedNonces[fundId][nonce]: bool
        -isMember[fundId][addr]: bool
        -ownerOf[fundId]: address
    }
    class Invite {
        +fundId: uint256
        +nonce: uint256
    }
    SafetyNet o--> Invite : verifies
Loading

Sequence Diagram

sequenceDiagram
    participant Owner
    participant User
    participant Safety Net Contract

    Owner->>Owner: Sign Invite {fundId, nonce}
    Owner->>User: Share link (Invite+sig)

    User->>Safety Net Contract: redeemInvite(inv, sig)
    Safety Net Contract->>Safety Net Contract: validate signature, nonce, and that the caller isn't already a member 
    Safety Net Contract->>Safety Net Contract: mark nonce used, add member
    Safety Net Contract-->>User: InviteRedeemed event
Loading

State Diagram

stateDiagram
    [*] --> Valid
    Valid --> Used : nonce consumed
Loading

5. Edge cases and concessions

  • Single-use only: each invite works once, then becomes invalid.
  • No global invalidation: leaked invites remain valid until used or expired.

6. Open Questions

  • Should fund owners be able to invalidate unused invites (e.g. via counter like @bagelface said) in the MVP?

7. Glossary / References

  • Invite: EIP-712 signature over (fundId, nonce).
  • Nonce: Unique value ensuring invite is one-time use.
  • Deadline: Expiry timestamp for the invite.
  • Safety Net: Mutual aid group with periodic contributions.

Links:


@exo404 exo404 self-assigned this Oct 9, 2025
@exo404 exo404 added the enhancement New feature or request label Oct 9, 2025
@exo404 exo404 linked an issue Oct 9, 2025 that may be closed by this pull request
@exo404
Copy link
Contributor Author

exo404 commented Oct 9, 2025

coverage and linting workflows are outdated

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements an invite system for Safety Net contracts, allowing owners to create off-chain signed invites for progressive member onboarding. The implementation uses EIP-712 signatures to maintain privacy while preventing replay attacks through nonces.

Key changes:

  • Added EIP-712 based invite struct with signature verification
  • Implemented single-use nonce system to prevent invite reuse
  • Added comprehensive error handling for invite redemption edge cases

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/interfaces/ISafetyNet.sol Defines Invite struct, InviteRedeemed event, invite-related errors, and redeemInvite function interface
src/contracts/SafetyNet.sol Implements EIP-712 signature verification, nonce tracking, and invite redemption logic with proper validations

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

if (usedNonces[_invite.safetyNetId][_invite.nonce]) revert InviteAlreadyUsed();
if (isMember[_invite.safetyNetId][msg.sender]) revert AlreadyMember();
if (_safetyNet.members.length >= _safetyNet.maximumMembers) revert SafetyNetFull();
if (_invite.redeemer != address(0) && _invite.redeemer != msg.sender) revert NotRedeemer();
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic assumes that address(0) means 'anyone can redeem', but this behavior is not documented in the struct definition or function comments. Consider adding documentation to clarify that redeemer = address(0) creates an open invite.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)
);

return keccak256(abi.encodePacked('\x19\x01', _domainSeparator, _structHash));
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using abi.encode instead of abi.encodePacked for the final hash construction. While this follows EIP-712 specification correctly, abi.encode is generally safer and the gas difference is negligible for this use case.

Suggested change
return keccak256(abi.encodePacked('\x19\x01', _domainSeparator, _structHash));
return keccak256(abi.encode('\x19\x01', _domainSeparator, _structHash));

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonTuretzky
Copy link
Contributor

@exo404 i think we should take the redeemer off scope completely. it was not in the spec, not sure why it was implemented. the whole point of an invite link is to not specify an address, this is the entire pain point we are trying to solve with this feature.

@exo404
Copy link
Contributor Author

exo404 commented Oct 10, 2025

@exo404 i think we should take the redeemer off scope completely. it was not in the spec, not sure why it was implemented. the whole point of an invite link is to not specify an address, this is the entire pain point we are trying to solve with this feature.

Acrually if you write 0x0 address the invite is open. I may read wrong but you and bagel said you wanted option so this is in the spec too. I can change it tho that's not a problem @RonTuretzky @bagelface

@RonTuretzky
Copy link
Contributor

@exo404 i think we should take the redeemer off scope completely. it was not in the spec, not sure why it was implemented. the whole point of an invite link is to not specify an address, this is the entire pain point we are trying to solve with this feature.

Acrually if you write 0x0 address the invite is open. I may read wrong but you and bagel said you wanted option so this is in the spec too. I can change it tho that's not a problem

It's not in the UML. I don't think we need it at all. Would prefer to not have it.

@exo404
Copy link
Contributor Author

exo404 commented Oct 10, 2025

@exo404 i think we should take the redeemer off scope completely. it was not in the spec, not sure why it was implemented. the whole point of an invite link is to not specify an address, this is the entire pain point we are trying to solve with this feature.

Acrually if you write 0x0 address the invite is open. I may read wrong but you and bagel said you wanted option so this is in the spec too. I can change it tho that's not a problem

It's not in the UML. I don't think we need it at all. Would prefer to not have it.

I forgot to specify the 0x0 thing you right. I'll change the code and the spec

@exo404
Copy link
Contributor Author

exo404 commented Oct 10, 2025

@exo404 i think we should take the redeemer off scope completely. it was not in the spec, not sure why it was implemented. the whole point of an invite link is to not specify an address, this is the entire pain point we are trying to solve with this feature.

Acrually if you write 0x0 address the invite is open. I may read wrong but you and bagel said you wanted option so this is in the spec too. I can change it tho that's not a problem

It's not in the UML. I don't think we need it at all. Would prefer to not have it.
@RonTuretzky
1716aae

@RonTuretzky
Copy link
Contributor

please add something like this and this to this PR. eventually we will need to do a typescript version as well but we can open a seperate issue for this. cc @crystal503

@exo404
Copy link
Contributor Author

exo404 commented Oct 13, 2025

@RonTuretzky working on this now

@exo404
Copy link
Contributor Author

exo404 commented Oct 13, 2025

please add something like this and this to this PR. eventually we will need to do a typescript version as well but we can open a seperate issue for this. cc @crystal503

@RonTuretzky b41de51

Copy link
Contributor

@RonTuretzky RonTuretzky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add test for the invite validation

/// @notice Utility contract that produces Safety Net invite signatures matching the on-chain verification logic
contract InviteGenerator is Script {
/// @notice Invite signing domain name used for EIP-712 signatures
string private constant INVITE_SIGNING_DOMAIN = 'SafetyNetInvite';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getting warnings on the next 3 variables they should start with _

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add to the README instead of introducing another file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please have some discretion, this file seems very long

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@exo404
Copy link
Contributor Author

exo404 commented Oct 15, 2025

please add test for the invite validation
@RonTuretzky
4335d72

@exo404
Copy link
Contributor Author

exo404 commented Oct 15, 2025

@RonTuretzky done with testing, both for SafetyNet redeemInvite function and InviteGenerator contract

@RonTuretzky RonTuretzky requested a review from Copilot October 16, 2025 08:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

}

/// @notice Computes the full EIP-712 digest for a struct invite
function inviteDigest(
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use abi.encodePacked instead of abi.encode on line 70 for the EIP-712 digest. According to EIP-712 specification, the final digest should use encodePacked for the \x19\x01 prefix concatenation.

Copilot uses AI. Check for mistakes.
function test_shouldReturnsInviteDigest() public {
bytes32 structHash = inviteGenerator.hashInvite(STRUCT_ID, NONCE);
bytes32 domainSeparator = inviteGenerator.domainSeparator(CHAIN_ID, VERIFYING_CONTRACT);
bytes32 expectedDigest = keccak256(abi.encode('\x19\x01', domainSeparator, structHash));
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use abi.encodePacked instead of abi.encode for the EIP-712 digest in the test expectation to match the EIP-712 specification.

Suggested change
bytes32 expectedDigest = keccak256(abi.encode('\x19\x01', domainSeparator, structHash));
bytes32 expectedDigest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash));

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@RonTuretzky RonTuretzky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is a refrerene for a cfrontend compoenent that is compatible with a less rigorus implementation , please adjust to work with this implementation , test locally , it is enough just to document a correct typescript/javacript snippet that works with this solidity code

/// @notice Utility contract that produces EIP712 invite signatures matching the on-chain verification logic
contract InviteGenerator is Script {
/// @notice Invite signing domain name used for EIP-712 signatures
string private _inviteSigningDomain;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop in favor of a comment on the hasehd version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string private _inviteSigningDomain;

/// @notice Invite signing version used for EIP-712 signatures
string private _inviteSignatureVersion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop string in favor of comment next to hashed version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@exo404
Copy link
Contributor Author

exo404 commented Oct 19, 2025

@RonTuretzky all test pass, I'm merging this to start to work on the frontend component locally

@exo404 exo404 merged commit 914f714 into dev Oct 19, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Enable Breadfund Creation Without Specifying All Users

3 participants