Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[submodule "nibble/lib/forge-std"]
path = OneByTwo/lib/forge-std
url = https://github.com/foundry-rs/forge-std

[submodule "level/lib/forge-std"]
path = level/lib/forge-std
url = https://github.com/foundry-rs/forge-std
Expand Down
16 changes: 16 additions & 0 deletions Nibble/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env

node_modules/
3 changes: 3 additions & 0 deletions Nibble/.gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "packages/contracts/lib/forge-std"]
path = packages/contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
4 changes: 4 additions & 0 deletions Nibble/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"tabWidth": 4,
"useTabs": false
}
14 changes: 14 additions & 0 deletions Nibble/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Nibble - Restaurant Revenue Sharing

### Overview

Nibble allows restaurants to mint tokens that entitle frequent customers to a portion of their revenue.

**Problem:**
Small businesses rely on strong local communities, but in cities with countless dining options, it's hard to stand out, attract an initial customer base, and maintain long-term engagement. Combined with thin margins emblematic of restaurants and price pressures from corporate competitors, enfranchsing our local businesses requires more than just consumer choice.

**Insight:**
By letting customers become partial owners, small businesses can foster a deeper sense of loyalty. When customers are invested, they spend more and are more likely to promote the restaurant through word-of-mouth and social media, increasing brand visibility. Additionally, they provide more valuable and constructive feedback, as they want the business to succeed, ultimately leading to better service, menu improvements, and a stronger community around the restaurant.

**Solution:**
Offer a platform where customer loyalty translates into a share of the restaurant's on-chain, encrypted revenue.
4 changes: 4 additions & 0 deletions Nibble/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
1 change: 1 addition & 0 deletions Nibble/lib/forge-std
Submodule forge-std added at bf909b
39 changes: 39 additions & 0 deletions Nibble/src/ISRC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.27;

/*
* Assumption:
* The custom types `saddress` and `suint256` are defined elsewhere.
* They are identical in behavior to address and uint256 respectively,
* but signal that the underlying data is stored privately.
*/

/*//////////////////////////////////////////////////////////////
// ISRC20 Interface
//////////////////////////////////////////////////////////////*/

interface ISRC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
// event Transfer(address indexed from, address indexed to, uint256 amount);
// event Approval(address indexed owner, address indexed spender, uint256 amount);

/*//////////////////////////////////////////////////////////////
METADATA FUNCTIONS
//////////////////////////////////////////////////////////////*/
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function totalSupply() external view returns (uint256);
function decimals() external view returns (uint8);

/*//////////////////////////////////////////////////////////////
ERC20 FUNCTIONS
//////////////////////////////////////////////////////////////*/
function balanceOf() external view returns (uint256);
function transfer(saddress to, suint256 amount) external returns (bool);
// owner passed in as msg.sender via signedRead
function allowance(saddress spender) external view returns (uint256);
function approve(saddress spender, suint256 amount) external returns (bool);
function transferFrom(saddress from, saddress to, suint256 amount) external returns (bool);
}
155 changes: 155 additions & 0 deletions Nibble/src/Nibble.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: MIT License
pragma solidity ^0.8.13;

/**
* @title Restaurant Revenue Sharing and Token System
* @dev This contract allows restaurants to register, mint tokens, track customer spending,
* and facilitate revenue sharing through token redemption.
*/
import {Rewards20} from "./Rewards20.sol";


/* The current rewards distribution for customers: token rewards are calculated across ETH spend and pre-existing holdings.
Specifically, through the Rewards20 mint function, when rewards are minted user recieve a boost based on their existing balance
This rewards customers for spending early and often, as repeated spends will grow
faster than infrequent, larger spends. It also encourages holding of tokens rather than immediately cashing out.
*/

contract Nibble {
/// @notice The total number of registered restaurants.
uint256 public restaurantCount;

/// @notice Maps a restaurant (owner) address to its respective Rewards20 token contract address.
// mapping(restaurant (owner) address => restaurant's Rewards20 token)
mapping(address => address) public restaurantsTokens;

/// @dev Maps a restaurant address to its total accumulated revenue.
// mapping(restauraunt address => total revenue)
mapping(address => suint256) internal restaurantTotalRevenue;

/// @dev Tracks how much each customer has spent at a specific restaurant.
// mapping(restaurant address => mapping(customer address => spend amount))
mapping(address => mapping(address => suint256)) internal customerSpend;

/// @notice Emitted when a new restaurant registers and its token is created.
/// @param Restaurant_ The address of the restaurant owner.
/// @param tokenAddress The address of the newly created Rewards20 token contract.
event Register(address Restaurant_, address tokenAddress);

/// @notice Emitted when a consumer spends at a restaurant.
/// @param Restaurant_ The address of the restaurant where the transaction occurred.
/// @param Consumer_ The address of the consumer who spent money.
event SpentAtRestaurant(address Restaurant_, address Consumer_); //Event of a user spending at a restaurant

/// @dev Ensures the caller is a registered restaurant.
/// @param _restaurantAddress The address to check.
modifier reqIsRestaurant(address _restaurantAddress) {
if (restaurantsTokens[_restaurantAddress] == address(0)) {
revert("restaurant is not registered");
}
_;
}

constructor() {}

/**
* @notice Registers a new restaurant and mints an associated token.
* @dev Assigns a unique Rewards20 token to the restaurant and updates the count.
* @param name_ The name of the restaurant token.
* @param symbol_ The symbol of the restaurant token.
*/
function registerRestaurant(string calldata name_, string calldata symbol_) public {
//This is a sample - token distribution should ideally be automated around user spend
//events to give larger portions of the tokens to early/regular spenders, while maintaining
//a token pool for the restaurant. Currently, the restaurant has to manually handle distribution.

if (restaurantsTokens[msg.sender] != address(0)) {
revert("restaurant already registered");
}

Rewards20 token = new Rewards20(name_, symbol_, 18, saddress(msg.sender), suint(1e24));
restaurantsTokens[msg.sender] = address(token);

restaurantCount++;

emit Register(msg.sender, address(token));
}

/**
* @notice Allows a customer to make a payment at a restaurant.
* @dev Updates revenue tracking and mints corresponding tokens to the consumer.
* @param restaurant_ The address of the restaurant where payment is made.
*/
function spendAtRestaurant(address restaurant_) public payable reqIsRestaurant(restaurant_) {
restaurantTotalRevenue[restaurant_] = restaurantTotalRevenue[restaurant_] + suint256(msg.value);
customerSpend[restaurant_][msg.sender] = customerSpend[restaurant_][msg.sender] + suint256(msg.value);

// Calculate the number of tokens to mint.
// Here we assume a 1:1 ratio between wei paid and tokens minted.
// You can adjust the conversion factor as needed.
uint256 tokenAmount = msg.value;

// Mint tokens directly to msg.sender.
// We assume that restaurantTokens[restaurant_] returns the Rewards20 token contract
// associated with this restaurant.

Rewards20 token = Rewards20(restaurantsTokens[restaurant_]);
token.mint(saddress(msg.sender), suint256(tokenAmount));

emit SpentAtRestaurant(restaurant_, msg.sender);
}

/**
* @notice Retrieves the total revenue accumulated by the restaurant.
* @dev Only callable by the restaurant itself.
* @return The total revenue in suint256.
*/
function checkTotalSpendRestaurant() public view reqIsRestaurant(msg.sender) returns (uint256) {
return uint256(restaurantTotalRevenue[msg.sender]);
}

/**
* @notice Retrieves the total spending of a specific customer at the caller's restaurant.
* @dev Only callable by the restaurant.
* @param user_ The address of the customer.
* @return The amount spent in suint256.
*/
function checkCustomerSpendRestaurant(address user_) public view reqIsRestaurant(msg.sender) returns (uint256) {
return uint256(customerSpend[msg.sender][user_]);
}

/**
* @notice Retrieves the caller's total spend at a specific restaurant.
* @dev Only callable by a customer for a restaurant where they have spent.
* @param restaurant_ The address of the restaurant.
* @return The amount spent in suint256.
*/
function checkSpendCustomer(address restaurant_) public view reqIsRestaurant(restaurant_) returns (uint256) {
return uint256(customerSpend[restaurant_][msg.sender]);
}

/**
* @notice Allows a user to exchange restaurant tokens for a portion of the restaurant's revenue.
* @dev Transfers tokens back to the restaurant and distributes a proportional revenue share.
* @param restaurant_ The address of the restaurant where tokens are redeemed.
* @param amount The amount of tokens to redeem, in suint256.
*/
function checkOut(address restaurant_, suint256 amount) public reqIsRestaurant(restaurant_) {
address tokenAddress = restaurantsTokens[restaurant_]; // get the address of the restaurant's token
Rewards20 token = Rewards20(tokenAddress);

// decrease msg.sender's allowance by amount so they cannot double checkOut
// note: reverts if amount is more than the user's allowance
token.transferFrom(saddress(msg.sender), saddress(restaurant_), amount);

// calculate the entitlement
suint256 totalRev = restaurantTotalRevenue[restaurant_];
uint256 entitlement = uint256(amount * totalRev) / token.totalSupply();

// send the entitlement to the customer
bool success = payable(msg.sender).send(uint256(entitlement));
if (!success) {
revert("Payment Failed");
}
}
}
142 changes: 142 additions & 0 deletions Nibble/src/Rewards20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.27;

import {ISRC20} from "./ISRC20.sol";


/**
* @title Rewards20 Token
* @dev A customer rewards token implementing the ISRC20 standard.
*
* @notice The contract allows the owner to mint tokens at their discretion.
* @dev When tokens are minted, the owner receives a boost based on their existing balance to reward loyalty.
* @dev Rewards20 tokens are non-transferable, meaning loyalty rewards cannot be shared or pooled.
*/

/*//////////////////////////////////////////////////////////////
// Rewards20 Contract
//////////////////////////////////////////////////////////////*/
contract Rewards20 is ISRC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
// Leaks information to public, will replace with encrypted events
// event Transfer(address indexed from, address indexed to, uint256 amount);
// event Approval(
// address indexed owner,
// address indexed spender,
// uint256 amount
// );

/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
saddress internal mintTo;
address public owner;

/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
// All storage variables that will be mutated must be confidential to
// preserve functional privacy.
uint256 public totalSupply;
mapping(saddress => suint256) internal balance;
mapping(saddress => mapping(saddress => suint256)) internal _allowance;

/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(string memory _name, string memory _symbol, uint8 _decimals, saddress mintTo_, suint256 supply_) {
name = _name;
symbol = _symbol;
decimals = _decimals;
_mint(mintTo_, supply_);
mintTo = mintTo_;
owner = msg.sender;
}

/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function balanceOf() public view virtual returns (uint256) {
return uint256(balance[saddress(msg.sender)]);
}

function approve(saddress spender, suint256 amount) public virtual returns (bool) {
_allowance[saddress(msg.sender)][spender] = amount;
// emit Approval(msg.sender, address(spender), uint256(amount));
return true;
}

function transfer(saddress to, suint256 amount) public virtual returns (bool) {
//Prevents token transfer outside of OG restaurant
if (to != mintTo && saddress(msg.sender) != mintTo) {
revert("Non-transferable outside of Original Restaurant");
}

// msg.sender is public information, casting to saddress below doesn't change this
balance[saddress(msg.sender)] -= amount;
unchecked {
balance[to] += amount;
}
// emit Transfer(msg.sender, address(to), uint256(amount));
return true;
}

function transferFrom(saddress from, saddress to, suint256 amount) public virtual returns (bool) {
//same as in above
if (to != mintTo && from != mintTo) {
revert("Non-transferable outside of Original Restaurant");
}

if (msg.sender != owner) {
suint256 allowed = _allowance[from][saddress(msg.sender)]; // Saves gas for limited approvals.
if (allowed != suint256(type(uint256).max)) {
if (amount > allowed) {
revert("not enough allowance");
}
_allowance[from][saddress(msg.sender)] = allowed - amount;
}
}

balance[from] -= amount;
unchecked {
balance[to] += amount;
}
// emit Transfer(msg.sender, address(to), uint256(amount));
return true;
}

function mint(saddress to, suint256 amount) external {
// For example, restrict minting so that only the owner can mint.
require(msg.sender == owner, "Only owner can mint tokens");
suint256 customerBalance = balance[to];
if (uint256(customerBalance) == 0) {
_mint(to, amount);
}
else{
_mint(to, amount * customerBalance);
}
}

/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(saddress to, suint256 amount) internal virtual {
totalSupply += uint256(amount);
unchecked {
balance[to] += amount;
}
// emit Transfer(address(0), address(to), uint256(amount));
}

function allowance(saddress spender) external view returns (uint256) {
return uint256(_allowance[saddress(msg.sender)][spender]);
}



}
Loading
Loading