# 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({ ... }) ```