diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fdeed48 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +docs +*.md \ No newline at end of file diff --git a/contracts/Mocks/README.md b/contracts/Mocks/README.md new file mode 100644 index 0000000..b1d2109 --- /dev/null +++ b/contracts/Mocks/README.md @@ -0,0 +1,3 @@ +# Mocks + +This directory contains mocks of contracts used for testing purposes. \ No newline at end of file diff --git a/contracts/Mocks/Upgrades/README.md b/contracts/Mocks/Upgrades/README.md new file mode 100644 index 0000000..6ec7d66 --- /dev/null +++ b/contracts/Mocks/Upgrades/README.md @@ -0,0 +1,13 @@ +# Upgrades + +This directory contrains **upgrade test** mock contracts. + +The contracts placed here **must** follow a pattern (in order to be properly picked up by the upgrade test harness): + +- Test contract names **must** match an existing upgradeable contract **exactly** +- Test contract names **must** end in `V2` + - eg. `Foo` -> `FooV2` +- Test contracts **must** imlement a `checkUpgrade()` function that **must** return at least a confitrmation string (default expected value being `"OK"`) +- Test contractss **should** reference all state in the contract, in the order is declared in the contract (including base contracts) + +If properly implemented, the test harness will be able to check if the upgraded contract retains state integrity after the upgrade. \ No newline at end of file diff --git a/contracts/TokenDistro/README.md b/contracts/TokenDistro/README.md new file mode 100644 index 0000000..b1fa592 --- /dev/null +++ b/contracts/TokenDistro/README.md @@ -0,0 +1,123 @@ +# TokenDistro Contract + +This directory contains the `TokenDistro` contract. The `TokenDistro` contract handles the allocation of rewards by releasing them over time in the **GIVStream**. + +The contract is based on the original `TokenDistro` contracts initially provided by Dappnode. + +## Initialization + +During intialization the TokenDistro defines the follwing: +- The token that is being distributed +- Start time, cliff period and duration of the distribution +- Initial percentage available to claim +- If the admin can cancel an allocation + +The `msg.sender` assumes the inital `DEFAULT_ADMIN_ROLE` role used in the contract. + +## Acess Control + +The Token Distro contract utilizes Openzeppelin `AccessControl` roles. Two roles are recognized: + +1. `DEFAULT_ADMIN_ROLE` is essentally the owner of the TokenDistro. It is assigned to the initializer address. `DEFAULT_ADMIN_ROLE` can add **distributors**, set the start time and cancel allocations. +2. `DISTRIBUTOR_ROLE` are the only addresses allowed to allocate tokens for distribution through claims. This should usually be an address of a liquidity mining contract such as eg. `Unipool` or similar. + +## Assigning Distributors + +TokenDistro assumes that there exist distributors (contracts or EOAs) that will later be used to distribute tokens. There are for example: +- The UniV3 staking incentive program +- The `UnipoolDistributor` used for liquidity mining +- The `GardenUnipoolDistirbutor` used for GIVGarden staking + +The `assign(address account, uint256 amount)` function will assign the given amount of tokens to an address with the `DISTRIBUTOR` role to distribute the tokens later. It must be called by the `DEFAULT_ADMIN_ROLE`. + +NOTE: Distributors are granted to `DISTRIBUTOR` role by the admin (`DEFAULT_ADMIN_ROLE`) using `grantRole` function. + +## Allocating/claiming GIV + +When a recipient wants to claim her tokens, she is expected to interact with a deployed distributor contract. The logic of the distributor governs how much the recipient is owed and at what time. + +In an example, a deployed `MerkleDistributor` checks if an address has a GIV merkle drop available. + +**IMPORTANT:** A distributor cannot allocate GIV to another `DISTRIBUTOR_ROLE`. This is to avoid tokens being trapped in a distribution contract. + +### Allocating GIV + +Before (or in the process of) claiming rewards, a registered distributor should call `allocate(address account, uint256 amount, bool claim)` with the address and the amount of GIV the recipient is entitled to. This will allocate the amount to the recipient where +- One part can be claimed immediately as GIV +- The rest is allocated to the GIVStream and is released over time + +The proportion of GIV that can be claimed is determined by the +- Initial percentage of GIV that is set as claimable +- Current block time (percentage of duration that is elapsed) +- Amount of tokens already claimed + +The globally claimable amount of tokens is: +``` +/** + * Function to get the total claimable tokens at some moment + * @param timestamp Unix time to check the number of tokens claimable + * @return Number of tokens claimable at that timestamp + */ +function globallyClaimableAt(uint256 timestamp) + public + view + override + returns (uint256) +{ + if (timestamp < startTime) return 0; + if (timestamp < cliffTime) return initialAmount; + if (timestamp > startTime + duration) return totalTokens; + + uint256 deltaTime = timestamp - startTime; + return initialAmount + (deltaTime * lockedAmount) / duration; +} +``` + +This determines the current unlocked amount of allocated tokens (at a given timestamp) as: +``` +uint256 unlockedAmount = (globallyClaimableAt(timestamp) * + balances[recipient].allocatedTokens) / totalTokens; + +return unlockedAmount - balances[recipient].claimed; +``` + +Only a registered distributor can allocate tokens to be claimed to an address. + +NOTE: If the claim flag is set to `true` the distributor will also perform a claim (as seen in the *Claiming GIV* section). + +### SendGIVbacks wrapper + +The `sendGIVbacks(address[] memory recipients, uint256[] memory amounts)` is a wrapper function that will allocate amounts of GIV tokens to corresponding recipients. It uses the underlying `_allocateMany` call. It will emit a `GivBacksPaid(address)` event that logs the function caller. + +This is useful for tracking GIVBack payouts that are tracked off-chain and triggered on a regular basis. + +### Claiming GIV + +Anyone can initiate a claim by calling `claim` or `claimTo` functions. This will transfer any liberated amount of tokens that are available to be claimed to the recipient. + +This way the recipient can access her GIVStream tokens regardless of the distributor. + +## Changing adress allocations + +The TokenDistro contract supports modifying the recipient of an allocation. This can be very useful if an address is compromised (invalid, lost keys etc.) This can be done by the recipient or (if enabled) by `DEFAULT_ADMIN_ROLE`. + +### Regular mode + +Any recipient can change the address to which she receives GIV allocations by calling `changeAddress(address newAddress)`. The new address **must not** be **in use**, that is it must not have any allocated (or claimed) tokens. + +This call emits a `ChangeAddress(address prevRecipient, address newRecipient)` event. + +**NOTE:** The `DISTRIBUTOR_ROLE` **cannot** change it's recipient address. + +When a recipient changes her address, she will be able to claim the remainder of her tokens from existing allocations. + +### Admin mode + +The `DEFAULT_ADMIN_ROLE` has an option to call the `cancelAllocation(address prevRecipient, address newRecipient)` function to change a recipient by force, buy only if the `cancellable` parameter was set to `true` on TokenDistro initialization. + +This call emits a `ChangeAddress(address prevRecipient, address newRecipient)` event. + +**NOTE:** The `DISTRIBUTOR_ROLE` recipient address **cannot** be changed. + + + diff --git a/contracts/Tokens/README.md b/contracts/Tokens/README.md new file mode 100644 index 0000000..0e36b57 --- /dev/null +++ b/contracts/Tokens/README.md @@ -0,0 +1,85 @@ +# Token Contracts + +This directory contains different **token contracts** used by the GIVEconomy. The contracts here are mainly used for reference and testing purposes, with the execption of the `UniswapV3RewardToken`. + +## UniswapV3 Reward Token + +This token is used as a bridge between `UniswapV3Staker` contract which handles the liquidity mining on Uniswap V3 and the `TokenDistro`. + +We must use this bridging token because the UniV3 incentive has no way to call `TokenDistro.allocate` when rewards are claimed. If the rewards in GIV are claimed directly from UniV3, there would be no way to enforce that the correct proportion of the claim is placed in the GIVStream. + +To accomplish this, the reward token contract maintains a fake balance that represents GIV that should be assigned and claimed from the TokenDistro. When a recipient withdraws rewards from the UniV3 incentive contract, the exact amount of reward tokens is "burned" and real GIV is allocated to the recipient via `TokenDistro.allocate`. + +The token implements the ERC-20 standard in such a way to **only** allow interactions between the TokenDistro and the UniV3 staker contract. + +The contract implements `ownable` and is owned by the deployer address. + +### The `approve` and `allowance` functions + +The UniV3 incentive contract is the only account that is allowed to call transfer from, and it has a constant `MAX_UINT256` allowance. +Approve is unused, but for interface compatibility always returns `true`. + +### The `transferFrom` function + +This function is called by the `UniV3Staker` contract when creating a new incentive: +``` +function createIncentive(IncentiveKey memory key, uint256 reward) external override { + ... + + TransferHelper.safeTransferFrom( + address(key.rewardToken), + msg.sender, + address(this), + reward + ); + + ... +} +``` + +The `transferFrom` call will only succeed if: +- Is called by the UniV3 staker contract +- Is `from` the contract `owner` +- Is `to` the the UniV3 staker contract + +For the `createIncentive` to succeed, the caller **must** be the `owner` of the contract. + +The `transferFrom` will "mint" the reward tokens that can later be burned and instead real GIV allocated in the TokenDistro by calling `transfer`. + +### The `transfer` function + +This function is called by the `UniV3Staker` when a recipient claims rewards: +``` +function claimReward( + IERC20Minimal rewardToken, + address to, + uint256 amountRequested +) external override returns (uint256 reward) { + ... + + TransferHelper.safeTransfer(address(rewardToken), to, reward); + + ... +} +``` +To succeed, `transfer` **must** be called by the UniV3 staker contract. + +When called, this function will "burn" the claimed amount of reward tokens and then call `TokenDistro.allocate` with the same amount. The `allocate` call will allocate actual GIV tokens as stream and perform a claim to transfer the released part of the stream. + +This function emits a `RewardPaid(uint256 amount, address to)` event. + +## GIV Token + +This is the GIV token contract that is deployed to both Ethereum mainnet and Gnosis chain. + +GIV is a lightweight ERC-20 token modeled after the [uniswap implementation](https://github.com/Uniswap/v2-core/blob/v1.0.1/contracts/UniswapV2ERC20.sol) with some modifications: + +- It has exposed `mint` and `burn` functions + - With associated `minter` role + - Total supply is uncapped +- [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) `transferWithAuthorization` +- `DOMAIN_SEPARATOR` is computed inside `_validateSignedData` to avoid reply-attacks due to Hardforks +- Forbids transfers to the contract and address(0) + - This is to avoid losing tokens in the contract and to enforce burn events + +**NOTE:** The actual GIV token is not meant to be deployed from the GIVEconomy repo, and was deployed separately before mainnet launch. diff --git a/contracts/UniswapV3Staker/README.md b/contracts/UniswapV3Staker/README.md new file mode 100644 index 0000000..a509879 --- /dev/null +++ b/contracts/UniswapV3Staker/README.md @@ -0,0 +1,9 @@ +# Uniswap V3 Staker Contracts + +This is the cannonical Uniswap V3 Staker staker contract, sourced directly from the [uniswap repo](https://github.com/Uniswap/v3-staker/tree/main/contracts). + +The contract source is not modified in any way, and is only included +- To provide bytecode and ABI to reference during deployment +- To use in testing + +Uniswap V3Staker is deployed to Ethereum **mainnet**. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1c90556 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Documentation + +This directory will contain **documentation** for the GivEconomy contracts. + +See `NOTES.md` for a general overview of contrats. + +TODO: add diagrams diff --git a/docs/assets/.gitkeep b/docs/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/assets/Merkle Distro claim.png b/docs/assets/Merkle Distro claim.png new file mode 100644 index 0000000..c29f641 Binary files /dev/null and b/docs/assets/Merkle Distro claim.png differ diff --git a/docs/src/MerkleDistroClaimSequene.puml b/docs/src/MerkleDistroClaimSequene.puml new file mode 100644 index 0000000..f46c716 --- /dev/null +++ b/docs/src/MerkleDistroClaimSequene.puml @@ -0,0 +1,47 @@ +@startuml "Merkle Distro claim" + +actor Recipient as caller +participant MerkleDistro as merkle +participant TokenDistro as distro +participant GIVToken as token + +caller -> merkle : isClaimed(index) +activate merkle + +merkle --> caller : bool +deactivate merkle + +caller -> merkle : claim(index, amount, proof) +activate merkle + +alt proof is invalid + merkle --> caller : revert +destroy merkle +else + merkle -> distro: allocate() + + activate distro + note right: set allocation + + alt claim + distro -> distro : claim() + activate distro + + distro -> token: safeTransfer() + activate token + + token --> caller: transfer tokens + + token --> distro: OK + deactivate token + deactivate distro + end + + distro --> merkle: OK + deactivate distro + + merkle -->caller : OK + deactivate merkle +end + +@enduml \ No newline at end of file