# Allogram
*An allogram is the smallest meaningful unit of a writing system*
Built on Scaffold-ETH2, inspired by the design-principles of OpenZeppelin.
### Goals
- Fun and easy to set up and start to play around with. Scaffold Eth is great at this because they have mastered it and heard feedback for years
- Shows what's possible and inspires experimentation (must be a quick feedback loop of making changes in contract and editing the app)
- Documentation that inspires: "wow this looks cool and I want to try some idea out" it should hook the action of forking the repo.
### Getting Started
```sh
npx create-eth@latest -e allocapital/allo-app
# Starter Contract in /packages/hardhat/contracts/Strategy.sol
# Allo App in /packages/app
# Contract Debugger in /packages/nextjs
# Indexer in /packages/allo-indexer
```
## Architecture
Exploring four different builds to find the common patterns of the function calls which will help us discover the correct abstractions.
- **SimpleGrants** - add projects to a cart and donate tokens to them
- **AlloIRL** - quadratic vote for projects, calculate matching funds, projects claim matching funds
- **AlloStaker** - stake ALLO on projects
- **AlloFunder** - crowdfunding
The more we can discover and re-use common patterns, interfaces, and events, the more flexible and powerful the architecture becomes.
```solidity
contract SimpleGrants {
// Register project (with application details in metadata)
function register(address project, string calldata metadataURI) public {}
// Approve project (with review details in metadata)
function approve(address project, string calldata metadataURI, address token) public onlyOwner {}
// Fund project (we want to allow to fund many project in one tx)
function allocate(address[] calldata projects, uint256[] calldata amounts) public {}
}
contract AlloIRL {
// Register project
function register(address project, string calldata metadataURI) public {}
// Approve project
function approve(address project, string calldata metadataURI) public onlyOwner {}
// Fund matching funds + Vote for projects
function allocate(address[] calldata projects, uint256[] calldata amounts, address token) public {}
// Transfer matching funds to projects
function distribute(address[] calldata projects, uint256[] calldata amounts, address token) public {}
}
contract AlloStaker {
// Register project
function register(address project, string calldata metadataURI) public {}
// Approve project (can also be auto-approved in register())
function approve(address project, string calldata metadataURI) public onlyOwner {}
// Stake to project
function allocate(address[] calldata projects, uint256[] calldata amounts) public {}
// Withdraw stake
function withdraw(address project, uint256 amount) public {}
// Stakers can claim accumulated rewards
function claimRewards() public {}
}
event Register(address indexed project, string metadataURI);
event Approve(address indexed project, string metadataURI);
event Allocate(address indexed from, address indexed recipient, uint256 amount, address token);
event Distribute(address indexed from, address indexed recipient, uint256 amount, address token);
```
- Register - register project (can we handle both project registrations and applications / campaigns etc with this?)
- `projects[projectAddress][applicationId]`
- When applicationId is 0 we interpret it as the project registration. This way we have a simple and flexible way to handle project registration, applications, creation of campaigns etc.
- Allocate - transfer tokens from sender to recipient (could also be contract address)
- Distribute - withdraw tokens from contract to recipient
---
> The main functions in these three are:
> - register + approve (of projects)
> - allocate (vote, fund, stake)
>
> If these emit events with the same signatures we can index it from one indexer.
>
> This means we can create hooks that work for all strategy contracts following these function and event signatures.
>
> - useRegister + useApprove - useRegistrations + useRegistrationByAddress
> - useAllocate + useAllocations
>
>
> This allows us to have a toolkit that:
> - easily create and approve projects
> - query registered projects and reviews
> - vote, fund, stake to projects
> - query votes, funding, stakings
### Indexer
The indexer picks up the events emitted by these contract extensions.
It can be queried like:
#### Applications
```graphql
{
registrations(
orderBy: "createdAt"
orderDirection: "desc"
limit: 100
where: { isApproved: true }
) {
items {
address
metadata # Project or Application metadata
review # Review metadata
createdAt
updatedAt
isApproved
strategyAddress
allocations { # allocations for project
items {
amount
}
}
}
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
```
#### Allocations
```graphql
{
allocations(
after: $after
before: $before
orderBy: "amount"
orderDirection: "desc"
limit: 100
where: { amount_gte: 100, token: "0xUSDCtoken" }
) {
items {
amount
recipient
from
token
createdAt
strategyAddress
}
}
}
```
### Composing Contracts
A Strategy contract can easily be created by composing extensions and gates.
### Extensions
These are inspired by the design choices in OpenZeppelin ERC20 with a public and an internal function.
#### Registry
A simple registry to handle eligibility for funding (or keeping track of project addresses for other purposes).
Metadata can be sent for both registration (Application) and approving (Review).
```solidity
event Register(address indexed project, string metadataURI);
event Approve(address indexed project, string metadataURI);
enum Status { Registered, Approved }
mapping(address => Status) public projects;
function _register(address recipient, string calldata metadataURI, bytes calldata data) internal virtual
function _approve(address recipient, string calldata metadataURI, bytes calldata data) internal virtual
```
#### Allocator
The Allocator comes with functions to allocate tokens to a collection of recipients. We use arrays for the public function to simplify token transfers to multiple recipients.
```solidity
event Allocate(address indexed from, address indexed recipient, uint256 amount, address token);
event Withdraw(address indexed from, address indexed recipient, uint256 amount, address token);
function allocate(address[] calldata recipients, uint256[] calldata amounts, address token, bytes[] calldata data) public virtual
function _allocate(address recipient) internal virtual
function withdraw(address[] calldata recipients, uint256[] calldata amounts, address token, bytes[] calldata data) public virtual
function _withdraw(address recipient) internal virtual
```
The reason the allocate function contains an array of recipients and amounts is because we want to be able to send to many in one transaction without using Multicall.
> Never approve Multicall3 to spend your tokens.
https://github.com/mds1/multicall?tab=readme-ov-file#security
#### MerkleClaim
MerkleVerify provides two internal functions that can be called from parent contract and a public claim function to transfer tokens.
```solidity
event MerkleSet(bytes32 root);
event Claim(address recipient, uint256 amount, address token);
function _setMerkle(bytes32 root) internal virtual
function _verify(bytes32 memory proof, address token, address recipient, uint256 amount) internal virtual
function claim(bytes32 memory proof, address token, address recipient, uint256 amount) public virtual
```
### Gates
Gates are small contracts with simple checks that can be used on contracts. Similar to OpenZeppelin's `Ownable` with the modifier `onlyOwner`.
#### TokenGate
TokenGate lets us check the balance of ERC20 or ERC721 tokens.
```solidity
modifier onlyTokens(account, token, amount)
```
##### Example
```solidity
contract GatedRegistryWithMerkleClaim is Allocator, Registry, TokenGate, MerkleClaim {
address private alloToken;
address private guildNFT;
constructor(address _alloToken, address _guildNFT) {
alloToken = _alloToken;
guildNFT = _guildNFT;
}
// Only holders of 1 Guild NFT can register projects
function register(
address project,
string memory metadataURI, // Metadata contain Application Details
bytes memory data
) public override onlyToken(_msgSender(), guildNFT, 1) {
_register(project, metadataURI, data);
}
// Only holders of 10 ALLO tokens can approve projects
function approve(
address project,
string memory metadataURI, // Metadata contains Application Review
bytes memory data
) public override onlyToken(_msgSender(), alloToken, 10 ** 18) {
_approve(project, metadataURI, data);
}
// Only Owner can update Merkle
function setMerkle(bytes32 _root) public override onlyOwner {
_setMerkle(_root);
}
// This is the same function as in MerkleClaim added here for clarity
function claim(bytes32[] memory proof, address token, address recipient, uint256 amount) public override {
if (_verify(proof, token, recipient, amount)) {
IERC20(token).transfer(recipient, amount);
emit Claim(recipient, amount, token);
}
}
}
```
#### EASGate
Check if an account has an attestation for a specific schemaUID and attester.
Can be used for example for OP Badgeholders or only approved projects can register projects.
```solidity
modifier onlyEAS(account, schemaUID, attester)
```
```solidity
contract OptimismRetroFunding is Allocator, EASGate, Registry {
// Only OP Badgeholders can call
function allocate() public overide onlyEAS(msg.sender, badgeholderSchemaUID, approvedAttester) {
_allocate(recipient, token, amount);
}
function register(address project) onlyEAS(project, approvedSchemaUID, approvedAttester) {
_register(recipient, metadataURI);
}
}
```
#### SignatureGate (TBD)
SignatureGate lets us restrict function calls to offchain generated signatures.
```solidity
modifier onlySignature(account, data)
// data contains signature + hash and is decoded in function
```
```solidity
contract OffchainSignatureCheck is Allocator, SignatureGate {
// Signature has been generated offchain by a trusted signer
function register(address project, string calldata metadataURI bytes calldata) onlySignature(msg.sender, data) {
_register(project, metadataURI);
}
}
```
### React Components
Ready-made components for most common use-cases.
Categories:
- registrations
- register
- registrations
- approvals
- allocations
- allocate
- allocations
- distribute
- distributions
- deposit
- deposits
- cart (maps registrations to allocation amounts)
- erc20
- token (name, symbol and decimals)
- approve
- allowance
- balance
- merkle
- generate merkle tree
- set merkle root
- generate proof
- claim
- quadratic
- calculate matching amounts from votes
#### RegistrationForm
Use-cases:
- Register Project
- Register Application / Campaign
Here's what it handles:
- Form + validation
- Uploading of metadata (+ avatars, cover images etc)
- Call strategy register via `useRegister` hook.
- Waiting for event to be published and redirect to RegistrationDetails
Should be easy to add and remove different form fields.
#### ProjectsList and ApplicationsList
#### RegistrationApproval
#### AllocationForm
Use-cases:
- Transfer tokens to addresses (projects, etc)
### React Hooks
```ts
function useRegister(strategyAddress) {}
function useApprove(strategyAddress) {}
function useRegistrations(query: IndexerQuery) {} // Query projects (and applications?)
function useAllocate(strategy: Address) {}
function useAllocations(query: IndexerQuery) {}
// Allocations into Strategy (wraps useAllocations)
function useDeposits(strategy: Address, query: IndexerQuery) {}
// Allocations from Strategy (wraps useAllocations)
function useWithdraws(strategy: Address, query: IndexerQuery) {}
```
```tsx
const register = useRegister("0xStrategyAddress")
register.mutate({ ... })
```