diff --git a/example.env b/.env.example similarity index 69% rename from example.env rename to .env.example index 9d57c6d..b95e558 100644 --- a/example.env +++ b/.env.example @@ -1,7 +1,10 @@ PRIVATE_KEY_CHIADO='YOUR_PRIVATE_KEY' +PRIVATE_KEY_GNOSIS='YOUR_PRIVATE_KEY' RPC_URL_CHIADO='https://rpc.chiadochain.net' +RPC_URL_GNOSIS='https://rpc.gnosischain.com' BLOCKSCOUT_API_KEY='YOUR_API_KEY' BLOCKSCOUT_URL_CHIADO='https://gnosis-chiado.blockscout.com/api?' +BLOCKSCOUT_URL_GNOSIS='https://gnosis.blockscout.com/api?' BLOCKSCOUT_VERIFIER='blockscout' GNOSISSCAN_API_KEY='YOUR_API_KEY' GNOSISSCAN_URL='https://api.gnosisscan.io/api?' diff --git a/.gitmodules b/.gitmodules index 690924b..257ba39 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,4 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts + branch = v5.0.2 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index df01a4b..998ae44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v0.3.1 + +- [PR ] + - temporarilly rename "Circles" to "Rings" and "CRC" to "RING", so that pre-release deployments are easily recognizable from the later production deployment +- [PR 123] + - fix: `personalMint` should not revert if issuance is zero; + - add `calculateIssuanceWithCheck` to know issuance without minting (while possibly updating v1 mint status); + - add testing for simple migration and invitation flows + - improve test for Circles issuance, add test for consecutive periods in personal mint ## v0.3.0 diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/chiado-0.3.1-alpha-1fd0982-240401-133217.log b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/chiado-0.3.1-alpha-1fd0982-240401-133217.log new file mode 100644 index 0000000..db02382 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/chiado-0.3.1-alpha-1fd0982-240401-133217.log @@ -0,0 +1,18 @@ +Chiado deployment +================= +Deployment Date: 2024-04-01 13:32:17 +Version: 0.3.1-alpha +Git Commit: 1fd0982c4c86533de2845a975113bcd763df8a56 +Deployer Address: 0x7619F26728Ced663E50E578EB6ff42430931564c, Intitial nonce: 81 +Compiler Version: v0.8.23+commit.f704f362 + +Deployed Contracts: +Hub: 0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD +Migration: 0x421ae522F756412808Ff62F74C20e5ebDA8C4208 +NameRegistry: 0x48F6B0aa3Ca905C9DbE41717c7664639107257da +ERC20Lift: 0xBF2F902d56d7ad2F2B1d674DC8B4d8C58354e7e5 +StandardTreasury: 0xD3529a99A7881DF7D11829135759AD0e4A8Ab587 +BaseGroupMintPolicy: 0x6604C8eBDD57F3771e4D0Cb3A174AfaEAf38C463 +MastercopyDemurrageERC20: 0x55241b3B4705f4624b282C6bFb162132a8B93B5C +MastercopyInflationaryERC20: 0x3F89b0154e1252cDcf7b86152EA10a21aDF07bFd +MastercopyStandardVault: 0xe2f033e8912dE80A363e3eFE9D840F346229bCCC diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/chiado-artefacts-0.3.1-alpha-1fd0982-240401-133217.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/chiado-artefacts-0.3.1-alpha-1fd0982-240401-133217.txt new file mode 100644 index 0000000..9a40ca8 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/chiado-artefacts-0.3.1-alpha-1fd0982-240401-133217.txt @@ -0,0 +1,9 @@ +{"contractName":"Hub","deployedAddress":"0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD","sourcePath":"src/hub/Hub.sol:Hub","constructor-args":"0xdbF22D4e8962Db3b2F1d9Ff55be728A887e47710 0x48F6B0aa3Ca905C9DbE41717c7664639107257da 0x421ae522F756412808Ff62F74C20e5ebDA8C4208 0xBF2F902d56d7ad2F2B1d674DC8B4d8C58354e7e5 0xD3529a99A7881DF7D11829135759AD0e4A8Ab587 1675209600 31540000 https://fallback.aboutcircles.com/v1/circles/{id}.json","argumentsFile":"constructorArgs_Hub.txt"} +{"contractName":"Migration","deployedAddress":"0x421ae522F756412808Ff62F74C20e5ebDA8C4208","sourcePath":"src/migration/Migration.sol:Migration","constructor-args":"0xdbF22D4e8962Db3b2F1d9Ff55be728A887e47710 0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD","argumentsFile":"constructorArgs_Migration.txt"} +{"contractName":"NameRegistry","deployedAddress":"0x48F6B0aa3Ca905C9DbE41717c7664639107257da","sourcePath":"src/names/NameRegistry.sol:NameRegistry","constructor-args":"0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD","argumentsFile":"constructorArgs_NameRegistry.txt"} +{"contractName":"ERC20Lift","deployedAddress":"0xBF2F902d56d7ad2F2B1d674DC8B4d8C58354e7e5","sourcePath":"src/lift/ERC20Lift.sol:ERC20Lift","constructor-args":"0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD 0x48F6B0aa3Ca905C9DbE41717c7664639107257da 0x55241b3B4705f4624b282C6bFb162132a8B93B5C 0x3F89b0154e1252cDcf7b86152EA10a21aDF07bFd","argumentsFile":"constructorArgs_ERC20Lift.txt"} +{"contractName":"StandardTreasury","deployedAddress":"0xD3529a99A7881DF7D11829135759AD0e4A8Ab587","sourcePath":"src/treasury/StandardTreasury.sol:StandardTreasury","constructor-args":"0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD 0xe2f033e8912dE80A363e3eFE9D840F346229bCCC","argumentsFile":"constructorArgs_StandardTreasury.txt"} +{"contractName":"BaseGroupMintPolicy","deployedAddress":"0x6604C8eBDD57F3771e4D0Cb3A174AfaEAf38C463","sourcePath":"src/groups/BaseMintPolicy.sol:MintPolicy","constructor-args":"","argumentsFile":"constructorArgs_BaseGroupMintPolicy.txt"} +{"contractName":"MastercopyDemurrageERC20","deployedAddress":"0x55241b3B4705f4624b282C6bFb162132a8B93B5C","sourcePath":"src/lift/DemurrageCircles.sol:DemurrageCircles","constructor-args":"","argumentsFile":"constructorArgs_MastercopyDemurrageERC20.txt"} +{"contractName":"MastercopyInflationaryERC20","deployedAddress":"0x3F89b0154e1252cDcf7b86152EA10a21aDF07bFd","sourcePath":"src/lift/InflationaryCircles.sol:InflationaryCircles","constructor-args":"","argumentsFile":"constructorArgs_MastercopyInflationaryERC20.txt"} +{"contractName":"MastercopyStandardVault","deployedAddress":"0xe2f033e8912dE80A363e3eFE9D840F346229bCCC","sourcePath":"src/treasury/StandardVault.sol:StandardVault","constructor-args":"","argumentsFile":"constructorArgs_MastercopyStandardVault.txt"} diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_BaseGroupMintPolicy.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_BaseGroupMintPolicy.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_BaseGroupMintPolicy.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_ERC20Lift.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_ERC20Lift.txt new file mode 100644 index 0000000..e92bca9 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_ERC20Lift.txt @@ -0,0 +1 @@ +0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD 0x48F6B0aa3Ca905C9DbE41717c7664639107257da 0x55241b3B4705f4624b282C6bFb162132a8B93B5C 0x3F89b0154e1252cDcf7b86152EA10a21aDF07bFd diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_Hub.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_Hub.txt new file mode 100644 index 0000000..8d0dad4 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_Hub.txt @@ -0,0 +1 @@ +0xdbF22D4e8962Db3b2F1d9Ff55be728A887e47710 0x48F6B0aa3Ca905C9DbE41717c7664639107257da 0x421ae522F756412808Ff62F74C20e5ebDA8C4208 0xBF2F902d56d7ad2F2B1d674DC8B4d8C58354e7e5 0xD3529a99A7881DF7D11829135759AD0e4A8Ab587 1675209600 31540000 https://fallback.aboutcircles.com/v1/circles/{id}.json diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyDemurrageERC20.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyDemurrageERC20.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyDemurrageERC20.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyInflationaryERC20.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyInflationaryERC20.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyInflationaryERC20.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyStandardVault.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyStandardVault.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_MastercopyStandardVault.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_Migration.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_Migration.txt new file mode 100644 index 0000000..86fadc6 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_Migration.txt @@ -0,0 +1 @@ +0xdbF22D4e8962Db3b2F1d9Ff55be728A887e47710 0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_NameRegistry.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_NameRegistry.txt new file mode 100644 index 0000000..65c3f89 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_NameRegistry.txt @@ -0,0 +1 @@ +0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD diff --git a/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_StandardTreasury.txt b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_StandardTreasury.txt new file mode 100644 index 0000000..704cfd7 --- /dev/null +++ b/script/deployments/chiado-0.3.1-alpha-1fd0982-240401-133217/constructorArgs_StandardTreasury.txt @@ -0,0 +1 @@ +0x1CAc5fE351EFFa130223aC0f84EB6B7Efc7a66AD 0xe2f033e8912dE80A363e3eFE9D840F346229bCCC diff --git a/script/deployments/chiadoDeploy.sh b/script/deployments/chiadoDeploy.sh index 8fe7761..208b2db 100755 --- a/script/deployments/chiadoDeploy.sh +++ b/script/deployments/chiadoDeploy.sh @@ -119,11 +119,11 @@ echo "MastercopyDemurrageERC20: ${MASTERCOPY_DEMURRAGE_ERC20_ADDRESS_07}" echo "MastercopyInflationaryERC20: ${MASTERCOPY_INFLATIONARY_ERC20_ADDRESS_08}" echo "MastercopyStandardVault: ${MASTERCOPY_STANDARD_VAULT_09}" -Deploy the contracts +# Deploy the contracts export deployment_details_file="${OUT_DIR}/chiado-artefacts-${identifier}.txt" echo "Deployment details will be stored in $deployment_details_file" -ยง + echo "" echo "Starting deployment..." echo "======================" diff --git a/script/deployments/extra-chiado-0.3.0-30195f2-240326-132818/chiado-0.3.0-30195f2-240326-132818.log b/script/deployments/extra-chiado-0.3.0-30195f2-240326-132818/chiado-0.3.0-30195f2-240326-132818.log deleted file mode 100644 index 9416673..0000000 --- a/script/deployments/extra-chiado-0.3.0-30195f2-240326-132818/chiado-0.3.0-30195f2-240326-132818.log +++ /dev/null @@ -1,18 +0,0 @@ -Chiado deployment -================= -Deployment Date: 2024-03-26 13:28:18 -Version: 0.3.0 -Git Commit: 30195f294f6a8c0adcc5ba46bde50a4885c07cf3 -Deployer Address: 0x7619F26728Ced663E50E578EB6ff42430931564c, Intitial nonce: 72 -Compiler Version: v0.8.23+commit.f704f362 - -Deployed Contracts: -Hub: 0x26fA0d8A877E6A6170E4613fA7Cb0359efdb985d -Migration: 0x38A8d2A38A788A388D20210ff18847EF7e13eda5 -NameRegistry: 0x570E9D5472994543C5032c83538B41983cF4F90E -ERC20Lift: 0x117077270aFd0Ede1d20DC5F9f14e884e2dbAb2a -StandardTreasury: 0x41152Bc23442A17877550bE0E133C501180f0338 -BaseGroupMintPolicy: 0xDa424b55C9977205F650Ef5A791A9C3E0E089068 -MastercopyDemurrageERC20: 0x3a0F7848071f067c25b0747eC5bEdc77cb3778eb -MastercopyInflationaryERC20: 0x3483cE5904413bc4Fb83DA2E43540eD769752C88 -MastercopyStandardVault: 0x6192069E85afBD09D03f7e85eB6c35982A847e16 diff --git a/script/deployments/gnosisChainDeploy.sh b/script/deployments/gnosisChainDeploy.sh new file mode 100755 index 0000000..08e4239 --- /dev/null +++ b/script/deployments/gnosisChainDeploy.sh @@ -0,0 +1,236 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status, +# specifically useful for the deploy_and_store_details function +set -e + +# Function to deploy contract and store deployment details +deploy_and_store_details() { + local contract_name=$1 + local precalculated_address=$2 + local source_path=$3 + local nonce_to_use=$4 + local deployment_output + local deployed_address + + echo "" >&2 + echo "Deploying ${contract_name}..." >&2 + + # Formulate the command + forge_command="forge create \ +--rpc-url ${RPC_URL} \ +--private-key ${PRIVATE_KEY} \ +--optimizer-runs 200 \ +--chain-id 100 \ +--priority-gas-price 2200000000 \ +--nonce ${nonce_to_use} \ +${source_path} \ +${@:5}" + + # Print the command for debugging + echo "Executing command: $forge_command" >&2 + + # if DRY_RUN is not true, execute the command + if [[ "$DRY_RUN" == "true" ]]; then + echo "Dry run mode enabled. Skipping deployment steps." >&2 + else + # Execute the command + deployment_output=$(eval "${forge_command}") + + deployed_address=$(echo "$deployment_output" | grep "Deployed to:" | awk '{print $3}') + echo "${contract_name} deployed at ${deployed_address}" >&2 + + # Verify that the deployed address matches the precalculated address + if [ "$deployed_address" = "$precalculated_address" ]; then + echo "Verification Successful: Deployed address matches the precalculated address for ${contract_name}." >&2 + else + echo "Verification Failed: Deployed address does not match the precalculated address for ${contract_name}." >&2 + echo "Precalculated Address: $precalculated_address" >&2 + echo "Deployed Address: $deployed_address" >&2 + # exit the script if the addresses don't match + exit 1 + fi + + # sleep for 20 seconds to allow the api to not rush, and display sleep countdown + for i in {20..1}; do + echo -ne "Sleeping for $i seconds... \r" >&2 + sleep 1 + done + fi + + # Define the filename for constructor arguments based on the contract name + arguments_file="constructorArgs_${contract_name}.txt" + arguments_path="${OUT_DIR}/${arguments_file}" + # Save constructor arguments to the file, skip "--constructor-args" + echo "${@:6}" > "${arguments_path}" + + # Store deployment details in a file + echo "{\"contractName\":\"${contract_name}\",\"deployedAddress\":\"${deployed_address}\",\"sourcePath\":\"$3\",\"constructor-args\":\"${@:6}\",\"argumentsFile\":\"${arguments_file}\"}" >> "${deployment_details_file}" + + # return the deployed address + echo "$deployed_address" +} + +# Function to generate a compact and short identifier +generate_identifier() { + # Fetch the current Git commit hash and take the first 7 characters + local git_commit_short=$(git rev-parse HEAD | cut -c1-7) + + # Get the current date and time in a compact format (YYMMDD-HMS) + local deployment_date=$1 + + # Fetch version from package.json + local version=$(node -p "require('./package.json').version") + + # Define the summary file name with version, short git commit, and compact date + echo "${version}-${git_commit_short}-${deployment_date}" +} + +# Set the environment variables, also for use in node script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +source "$DIR/../../.env" + +# declare Gnosis Chain constants +V1_HUB_ADDRESS='0x29b9a7fBb8995b2423a71cC17cf9810798F6C543' +# gnosis chain v1 deployment time is 1602786330 unix time, +# but we want to offset this to midnight to start day zero +# on midnight 15 October 2020 +INFLATION_DAY_ZERO=1602720000 +# put a long bootstrap time for testing bootstrap to one year +BOOTSTRAP_ONE_YEAR=31540000 +# fallback URI +URI='https://fallback.aboutcircles.com/v1/circles/{id}.json' + +# re-export the variables for use here and in the general calculation JS script +export PRIVATE_KEY=$PRIVATE_KEY_GNOSIS +export RPC_URL=$RPC_URL_GNOSIS +VERIFIER_URL=$BLOCKSCOUT_URL_GNOSIS +VERIFIER_API_KEY=$BLOCKSCOUT_API_KEY +VERIFIER=$BLOCKSCOUT_VERIFIER + +# Get the current date and time in a compact format (YYMMDD-HMS) outside the functions +deployment_date=$(date "+%y%m%d-%H%M%S") +deployment_date_long=$(date "+%Y-%m-%d %H:%M:%S") +identifier=$(generate_identifier $deployment_date) + +# Run the Node.js script to predict contract addresses +# Assuming predictAddresses.js is in the current directory +read DEPLOYER_ADDRESS NONCE_USED HUB_ADDRESS_01 MIGRATION_ADDRESS_02 NAMEREGISTRY_ADDRESS_03 \ +ERC20LIFT_ADDRESS_04 STANDARD_TREASURY_ADDRESS_05 BASE_GROUPMINTPOLICY_ADDRESS_06 \ +MASTERCOPY_DEMURRAGE_ERC20_ADDRESS_07 MASTERCOPY_INFLATIONARY_ERC20_ADDRESS_08 \ +MASTERCOPY_STANDARD_VAULT_09 \ +<<< $(node predictDeploymentAddresses.js) + +# Check if DRY_RUN is set to "true" and adjust the directory name accordingly +if [[ "$DRY_RUN" == "true" ]]; then + OUT_DIR="$DIR/DRYRUN-gnosischain-${identifier}-DRY_RUN" +else + OUT_DIR="$DIR/gnosischain-$identifier" +fi +# Create a directory for the deployment and go there after calling node script +mkdir -p "$OUT_DIR" + +# Use DEPLOYER_ADDRESS and NONCE_USED as needed +echo "Deployer Address: $DEPLOYER_ADDRESS, Nonce Used: $NONCE_USED" + +# Log the predicted deployment addresses +echo "Predicted deployment addresses:" +echo "===============================" +echo "Hub: ${HUB_ADDRESS_01}" +echo "Migration: ${MIGRATION_ADDRESS_02}" +echo "NameRegistry: ${NAMEREGISTRY_ADDRESS_03}" +echo "ERC20Lift: ${ERC20LIFT_ADDRESS_04}" +echo "StandardTreasury: ${STANDARD_TREASURY_ADDRESS_05}" +echo "BaseGroupMintPolicy: ${BASE_GROUPMINTPOLICY_ADDRESS_06}" +echo "MastercopyDemurrageERC20: ${MASTERCOPY_DEMURRAGE_ERC20_ADDRESS_07}" +echo "MastercopyInflationaryERC20: ${MASTERCOPY_INFLATIONARY_ERC20_ADDRESS_08}" +echo "MastercopyStandardVault: ${MASTERCOPY_STANDARD_VAULT_09}" + +# Deploy the contracts + +export deployment_details_file="${OUT_DIR}/gnosischain-artefacts-${identifier}.txt" +echo "Deployment details will be stored in $deployment_details_file" + +echo "" +echo "Starting deployment..." +echo "======================" + +HUB=$(deploy_and_store_details "Hub" $HUB_ADDRESS_01 \ + src/hub/Hub.sol:Hub $((NONCE_USED)) \ + --constructor-args $V1_HUB_ADDRESS \ + $NAMEREGISTRY_ADDRESS_03 $MIGRATION_ADDRESS_02 $ERC20LIFT_ADDRESS_04 \ + $STANDARD_TREASURY_ADDRESS_05 $INFLATION_DAY_ZERO \ + $BOOTSTRAP_ONE_YEAR $URI) + +NONCE_USED=$((NONCE_USED + 1)) + +MIGRATION=$(deploy_and_store_details "Migration" $MIGRATION_ADDRESS_02 \ + src/migration/Migration.sol:Migration $((NONCE_USED)) \ + --constructor-args $V1_HUB_ADDRESS $HUB_ADDRESS_01) + +NONCE_USED=$((NONCE_USED + 1)) + +NAME_REGISTRY=$(deploy_and_store_details "NameRegistry" $NAMEREGISTRY_ADDRESS_03 \ + src/names/NameRegistry.sol:NameRegistry $((NONCE_USED)) \ + --constructor-args $HUB_ADDRESS_01) + +NONCE_USED=$((NONCE_USED + 1)) + +ERC20LIFT=$(deploy_and_store_details "ERC20Lift" $ERC20LIFT_ADDRESS_04 \ + src/lift/ERC20Lift.sol:ERC20Lift $((NONCE_USED)) \ + --constructor-args $HUB_ADDRESS_01 \ + $NAMEREGISTRY_ADDRESS_03 $MASTERCOPY_DEMURRAGE_ERC20_ADDRESS_07 \ + $MASTERCOPY_INFLATIONARY_ERC20_ADDRESS_08) + +NONCE_USED=$((NONCE_USED + 1)) + +STANDARD_TREASURY=$(deploy_and_store_details "StandardTreasury" $STANDARD_TREASURY_ADDRESS_05 \ + src/treasury/StandardTreasury.sol:StandardTreasury $((NONCE_USED)) \ + --constructor-args $HUB_ADDRESS_01 $MASTERCOPY_STANDARD_VAULT_09) + +NONCE_USED=$((NONCE_USED + 1)) + +BASE_MINTPOLICY=$(deploy_and_store_details "BaseGroupMintPolicy" $BASE_GROUPMINTPOLICY_ADDRESS_06 \ + src/groups/BaseMintPolicy.sol:MintPolicy $((NONCE_USED))) + +NONCE_USED=$((NONCE_USED + 1)) + +MC_ERC20_DEMURRAGE=$(deploy_and_store_details "MastercopyDemurrageERC20" $MASTERCOPY_DEMURRAGE_ERC20_ADDRESS_07 \ + src/lift/DemurrageCircles.sol:DemurrageCircles $((NONCE_USED))) + +NONCE_USED=$((NONCE_USED + 1)) + +MC_ERC20_INFLATION=$(deploy_and_store_details "MastercopyInflationaryERC20" $MASTERCOPY_INFLATIONARY_ERC20_ADDRESS_08 \ + src/lift/InflationaryCircles.sol:InflationaryCircles $((NONCE_USED))) + +NONCE_USED=$((NONCE_USED + 1)) + +MC_STANDARD_VAULT=$(deploy_and_store_details "MastercopyStandardVault" $MASTERCOPY_STANDARD_VAULT_09 \ + src/treasury/StandardVault.sol:StandardVault $((NONCE_USED))) + +# log to file + +# Use the function to generate the file name +summary_file="${OUT_DIR}/gnosischain-${identifier}.log" + +# Now you can use $summary_file for logging +{ + echo "Gnosis Chain deployment" + echo "=================" + echo "Deployment Date: $deployment_date_long" + echo "Version: $(node -p "require('./package.json').version")" + echo "Git Commit: $(git rev-parse HEAD)" + echo "Deployer Address: $DEPLOYER_ADDRESS, Initial nonce: $NONCE_USED" + echo "Compiler Version: v0.8.23+commit.f704f362" # todo: figure out where to extract this from + echo "" + echo "Deployed Contracts:" + echo "Hub: ${HUB}" + echo "Migration: ${MIGRATION}" + echo "NameRegistry: ${NAME_REGISTRY}" + echo "ERC20Lift: ${ERC20LIFT}" + echo "StandardTreasury: ${STANDARD_TREASURY}" + echo "BaseGroupMintPolicy: ${BASE_MINTPOLICY}" + echo "MastercopyDemurrageERC20: ${MC_ERC20_DEMURRAGE}" + echo "MastercopyInflationaryERC20: ${MC_ERC20_INFLATION}" + echo "MastercopyStandardVault: ${MC_STANDARD_VAULT}" +} >> "$summary_file" diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_BaseGroupMintPolicy.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_BaseGroupMintPolicy.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_BaseGroupMintPolicy.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_ERC20Lift.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_ERC20Lift.txt new file mode 100644 index 0000000..e89428b --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_ERC20Lift.txt @@ -0,0 +1 @@ +0x00315615f899F90d6Bc46EaaC911E37871d62DD8 0xa28f05bd1C054E2AA9680497e03Df2cc17f1b3d8 0x16920293865aCe1c5e97e672eaECFD3254224b6f 0x3467E9977856A422CfE7960D0a34af2FFB91B47f diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_Hub.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_Hub.txt new file mode 100644 index 0000000..e2a8e2b --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_Hub.txt @@ -0,0 +1 @@ +0x29b9a7fBb8995b2423a71cC17cf9810798F6C543 0xa28f05bd1C054E2AA9680497e03Df2cc17f1b3d8 0xE22bFb93aDFa2b446226Ffb6FDa8dC641C819f23 0xE2CD16612EaF37432a8384c5dA8a66e1CAf0E68F 0x8e9b584764C467d95C23c27c0Ed0eE96A95b427d 1602720000 31540000 https://fallback.aboutcircles.com/v1/circles/{id}.json diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyDemurrageERC20.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyDemurrageERC20.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyDemurrageERC20.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyInflationaryERC20.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyInflationaryERC20.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyInflationaryERC20.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyStandardVault.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyStandardVault.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_MastercopyStandardVault.txt @@ -0,0 +1 @@ + diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_Migration.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_Migration.txt new file mode 100644 index 0000000..ad2ebcb --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_Migration.txt @@ -0,0 +1 @@ +0x29b9a7fBb8995b2423a71cC17cf9810798F6C543 0x00315615f899F90d6Bc46EaaC911E37871d62DD8 diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_NameRegistry.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_NameRegistry.txt new file mode 100644 index 0000000..e1c4858 --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_NameRegistry.txt @@ -0,0 +1 @@ +0x00315615f899F90d6Bc46EaaC911E37871d62DD8 diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_StandardTreasury.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_StandardTreasury.txt new file mode 100644 index 0000000..20d80a1 --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/constructorArgs_StandardTreasury.txt @@ -0,0 +1 @@ +0x00315615f899F90d6Bc46EaaC911E37871d62DD8 0xc70FAfea358d371477BbAd1c3adE0936AC4eEe24 diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/gnosischain-0.3.1-alpha-1fd0982-240401-175101.log b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/gnosischain-0.3.1-alpha-1fd0982-240401-175101.log new file mode 100644 index 0000000..c97bb0b --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/gnosischain-0.3.1-alpha-1fd0982-240401-175101.log @@ -0,0 +1,18 @@ +Gnosis Chain deployment +================= +Deployment Date: 2024-04-01 17:51:01 +Version: 0.3.1-alpha +Git Commit: 1fd0982c4c86533de2845a975113bcd763df8a56 +Deployer Address: 0x7619F26728Ced663E50E578EB6ff42430931564c, Initial nonce: 8 +Compiler Version: v0.8.23+commit.f704f362 + +Deployed Contracts: +Hub: 0x00315615f899F90d6Bc46EaaC911E37871d62DD8 +Migration: 0xE22bFb93aDFa2b446226Ffb6FDa8dC641C819f23 +NameRegistry: 0xa28f05bd1C054E2AA9680497e03Df2cc17f1b3d8 +ERC20Lift: 0xE2CD16612EaF37432a8384c5dA8a66e1CAf0E68F +StandardTreasury: 0x8e9b584764C467d95C23c27c0Ed0eE96A95b427d +BaseGroupMintPolicy: 0x6ecbCB0166A8143aa4AC19a43Ea31f915D3D36dB +MastercopyDemurrageERC20: 0x16920293865aCe1c5e97e672eaECFD3254224b6f +MastercopyInflationaryERC20: 0x3467E9977856A422CfE7960D0a34af2FFB91B47f +MastercopyStandardVault: 0xc70FAfea358d371477BbAd1c3adE0936AC4eEe24 diff --git a/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/gnosischain-artefacts-0.3.1-alpha-1fd0982-240401-175101.txt b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/gnosischain-artefacts-0.3.1-alpha-1fd0982-240401-175101.txt new file mode 100644 index 0000000..c46a3d7 --- /dev/null +++ b/script/deployments/gnosischain-0.3.1-alpha-1fd0982-240401-175101/gnosischain-artefacts-0.3.1-alpha-1fd0982-240401-175101.txt @@ -0,0 +1,9 @@ +{"contractName":"Hub","deployedAddress":"0x00315615f899F90d6Bc46EaaC911E37871d62DD8","sourcePath":"src/hub/Hub.sol:Hub","constructor-args":"0x29b9a7fBb8995b2423a71cC17cf9810798F6C543 0xa28f05bd1C054E2AA9680497e03Df2cc17f1b3d8 0xE22bFb93aDFa2b446226Ffb6FDa8dC641C819f23 0xE2CD16612EaF37432a8384c5dA8a66e1CAf0E68F 0x8e9b584764C467d95C23c27c0Ed0eE96A95b427d 1602720000 31540000 https://fallback.aboutcircles.com/v1/circles/{id}.json","argumentsFile":"constructorArgs_Hub.txt"} +{"contractName":"Migration","deployedAddress":"0xE22bFb93aDFa2b446226Ffb6FDa8dC641C819f23","sourcePath":"src/migration/Migration.sol:Migration","constructor-args":"0x29b9a7fBb8995b2423a71cC17cf9810798F6C543 0x00315615f899F90d6Bc46EaaC911E37871d62DD8","argumentsFile":"constructorArgs_Migration.txt"} +{"contractName":"NameRegistry","deployedAddress":"0xa28f05bd1C054E2AA9680497e03Df2cc17f1b3d8","sourcePath":"src/names/NameRegistry.sol:NameRegistry","constructor-args":"0x00315615f899F90d6Bc46EaaC911E37871d62DD8","argumentsFile":"constructorArgs_NameRegistry.txt"} +{"contractName":"ERC20Lift","deployedAddress":"0xE2CD16612EaF37432a8384c5dA8a66e1CAf0E68F","sourcePath":"src/lift/ERC20Lift.sol:ERC20Lift","constructor-args":"0x00315615f899F90d6Bc46EaaC911E37871d62DD8 0xa28f05bd1C054E2AA9680497e03Df2cc17f1b3d8 0x16920293865aCe1c5e97e672eaECFD3254224b6f 0x3467E9977856A422CfE7960D0a34af2FFB91B47f","argumentsFile":"constructorArgs_ERC20Lift.txt"} +{"contractName":"StandardTreasury","deployedAddress":"0x8e9b584764C467d95C23c27c0Ed0eE96A95b427d","sourcePath":"src/treasury/StandardTreasury.sol:StandardTreasury","constructor-args":"0x00315615f899F90d6Bc46EaaC911E37871d62DD8 0xc70FAfea358d371477BbAd1c3adE0936AC4eEe24","argumentsFile":"constructorArgs_StandardTreasury.txt"} +{"contractName":"BaseGroupMintPolicy","deployedAddress":"0x6ecbCB0166A8143aa4AC19a43Ea31f915D3D36dB","sourcePath":"src/groups/BaseMintPolicy.sol:MintPolicy","constructor-args":"","argumentsFile":"constructorArgs_BaseGroupMintPolicy.txt"} +{"contractName":"MastercopyDemurrageERC20","deployedAddress":"0x16920293865aCe1c5e97e672eaECFD3254224b6f","sourcePath":"src/lift/DemurrageCircles.sol:DemurrageCircles","constructor-args":"","argumentsFile":"constructorArgs_MastercopyDemurrageERC20.txt"} +{"contractName":"MastercopyInflationaryERC20","deployedAddress":"0x3467E9977856A422CfE7960D0a34af2FFB91B47f","sourcePath":"src/lift/InflationaryCircles.sol:InflationaryCircles","constructor-args":"","argumentsFile":"constructorArgs_MastercopyInflationaryERC20.txt"} +{"contractName":"MastercopyStandardVault","deployedAddress":"0xc70FAfea358d371477BbAd1c3adE0936AC4eEe24","sourcePath":"src/treasury/StandardVault.sol:StandardVault","constructor-args":"","argumentsFile":"constructorArgs_MastercopyStandardVault.txt"} diff --git a/script/deployments/package.json b/script/deployments/package.json index 402615d..5ff09a4 100644 --- a/script/deployments/package.json +++ b/script/deployments/package.json @@ -1,6 +1,6 @@ { "name": "deploy-circles", - "version": "0.3.0", + "version": "0.3.1-alpha", "type": "module", "dependencies": { "dotenv": "^16.4.5", diff --git a/src/circles/Circles.sol b/src/circles/Circles.sol index 811fb45..a1a826e 100644 --- a/src/circles/Circles.sol +++ b/src/circles/Circles.sol @@ -60,6 +60,8 @@ contract Circles is ERC1155 { // Events + event PersonalMint(address indexed human, uint256 amount, uint256 startPeriod, uint256 endPeriod); + // Constructor /** @@ -79,8 +81,11 @@ contract Circles is ERC1155 { /** * @notice Calculate the demurraged issuance for a human's avatar. * @param _human Address of the human's avatar to calculate the issuance for. + * @return issuance The issuance in attoCircles. + * @return startPeriod The start of the claimable period. + * @return endPeriod The end of the claimable period. */ - function calculateIssuance(address _human) public view returns (uint256) { + function calculateIssuance(address _human) public view returns (uint256, uint256, uint256) { MintTime storage mintTime = mintTimes[_human]; if (mintTime.mintV1Status != address(0) && mintTime.mintV1Status != CIRCLES_STOPPED_V1) { // Circles v1 contract cannot be active. @@ -90,7 +95,7 @@ contract Circles is ERC1155 { if (uint256(mintTime.lastMintTime) + 1 hours > block.timestamp) { // Mint time is set to indefinite future for stopped mints in v2 // and only complete hours get minted, so shortcut the calculation - return 0; + return (0, 0, 0); } // calculate the start of the claimable period @@ -115,7 +120,13 @@ contract Circles is ERC1155 { int128 overcount = Math64x64.add(Math64x64.mul(R[n], k), l); // subtract the overcount from the total issuance, and convert to attoCircles - return Math64x64.mulu(Math64x64.sub(T[n], overcount), EXA); + return ( + Math64x64.mulu(Math64x64.sub(T[n], overcount), EXA), + // start of the claimable period + inflationDayZero + dA * 1 days + Math64x64.mulu(k, 1 hours), + // end of the claimable period + inflationDayZero + dB * 1 days + 1 days - Math64x64.mulu(l, 1 hours) + ); } // Internal functions @@ -125,15 +136,17 @@ contract Circles is ERC1155 { * @param _human Address of the human's avatar to claim the issuance for. */ function _claimIssuance(address _human) internal { - uint256 issuance = calculateIssuance(_human); + (uint256 issuance, uint256 startPeriod, uint256 endPeriod) = calculateIssuance(_human); if (issuance == 0) { - // No issuance to claim. - revert CirclesERC1155NoMintToIssue(_human, mintTimes[_human].lastMintTime); + // No issuance to claim, simply return without reverting + return; } // mint personal Circles to the human _mint(_human, toTokenId(_human), issuance, ""); // update the last mint time mintTimes[_human].lastMintTime = uint96(block.timestamp); + + emit PersonalMint(_human, issuance, startPeriod, endPeriod); } // Private functions diff --git a/src/errors/Errors.sol b/src/errors/Errors.sol index c9afdb0..650b2c3 100644 --- a/src/errors/Errors.sol +++ b/src/errors/Errors.sol @@ -38,8 +38,6 @@ interface IHubErrors { interface ICirclesERC1155Errors { error CirclesERC1155MintBlocked(address human, address mintV1Status); - error CirclesERC1155NoMintToIssue(address human, uint96 lastMintTime); - error CirclesERC1155AmountExceedsMaxUint190(address account, uint256 circlesId, uint256 amount, uint8 code); } diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index dd0cd85..a9e0dbb 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -73,14 +73,20 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { */ IHubV1 public immutable hubV1; - INameRegistry public immutable nameRegistry; + /** + * @notice The name registry contract address. + */ + INameRegistry public nameRegistry; /** * @notice The address of the migration contract for v1 Circles. */ - address public immutable migration; + address public migration; - IERC20Lift public immutable liftERC20; + /** + * @notice The address of the Lift ERC20 contract. + */ + IERC20Lift public liftERC20; /** * @notice The timestamp of the start of the invitation-only period. @@ -95,7 +101,7 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { * @notice The standard treasury contract address used when * registering a (non-custom) group. */ - address public immutable standardTreasury; + address public standardTreasury; /** * @notice The mapping of registered avatar addresses to the next avatar address, @@ -104,24 +110,22 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { */ mapping(address => address) public avatars; - // Mint policy registered by avatar. + /** + * @notice The mapping of group avatar addresses to the mint policy contract address. + */ mapping(address => address) public mintPolicies; + /** + * @notice The mapping of group avatar addresses to the treasury contract address. + */ mapping(address => address) public treasuries; - // mapping(uint256 => WrappedERC20) public tokenIDToInfERC20; - /** * @notice The iterable mapping of directional trust relations between avatars and * their expiry times. */ mapping(address => mapping(address => TrustMarker)) public trustMarkers; - // /** - // * @notice tokenIDToCidV0Digest is a mapping of token IDs to the IPFS CIDv0 digest. - // */ - // mapping(uint256 => bytes32) public tokenIdToCidV0Digest; - // Events event RegisterHuman(address indexed avatar); @@ -360,18 +364,27 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { // Only avatars registered as human can call personal mint. revert CirclesHubMustBeHuman(msg.sender, 1); } - // check if v1 Circles is known to be stopped - if (mintTimes[msg.sender].mintV1Status != CIRCLES_STOPPED_V1) { - // if v1 Circles is not known to be stopped, check the status - address v1MintStatus = _avatarV1CirclesStatus(msg.sender); - _updateMintV1Status(msg.sender, v1MintStatus); - } + // check if v1 Circles is known to be stopped and update status + _checkHumanV1CirclesStatus(msg.sender); // claim issuance if any is available _claimIssuance(msg.sender); } - // graph transfers SHOULD allow personal -> group conversion en route + /** + * @notice Calculate issuance allows to calculate the issuance for a human avatar with a check + * to update the v1 mint status if updated. + * @param _human address of the human avatar to calculate the issuance for + * @return issuance amount of Circles that can be minted + * @return startPeriod start of the claimable period + * @return endPeriod end of the claimable period + */ + function calculateIssuanceWithCheck(address _human) external returns (uint256, uint256, uint256) { + // check if v1 Circles is known to be stopped and update status + _checkHumanV1CirclesStatus(_human); + // calculate issuance for the human avatar, but don't mint + return calculateIssuance(_human); + } /** * @notice Group mint allows to mint group Circles by providing the required collateral. @@ -1048,6 +1061,20 @@ contract Hub is Circles, MetadataDefinitions, IHubErrors, ICirclesErrors { return registrationCount; } + /** + * Check the status of an avatar's Circles in the Hub v1 contract, + * and update the mint status of the avatar. + * @param _human Address of the human avatar to check the v1 mint status of. + */ + function _checkHumanV1CirclesStatus(address _human) internal { + // check if v1 Circles is known to be stopped + if (mintTimes[_human].mintV1Status != CIRCLES_STOPPED_V1) { + // if v1 Circles is not known to be stopped, check the status + address v1MintStatus = _avatarV1CirclesStatus(_human); + _updateMintV1Status(_human, v1MintStatus); + } + } + /** * Checks the status of an avatar's Circles in the Hub v1 contract, * and returns the address of the Circles if it exists and is not stopped. diff --git a/src/migration/IHub.sol b/src/migration/IHub.sol index 4f244c2..08abb9a 100644 --- a/src/migration/IHub.sol +++ b/src/migration/IHub.sol @@ -8,7 +8,10 @@ pragma solidity >=0.8.13; */ interface IHubV1 { function signup() external; + function signupBonus() external view returns (uint256); function organizationSignup() external; + function symbol() external view returns (string memory); + function name() external view returns (string memory); function tokenToUser(address token) external view returns (address); function userToToken(address user) external view returns (address); @@ -18,11 +21,12 @@ interface IHubV1 { function deployedAt() external view returns (uint256); function initialIssuance() external view returns (uint256); - // function issuance() external view returns (uint256); - // function issuanceByStep(uint256 periods) external view returns (uint256); + function issuance() external view returns (uint256); + function issuanceByStep(uint256 periods) external view returns (uint256); function inflate(uint256 initial, uint256 periods) external view returns (uint256); function inflation() external view returns (uint256); function divisor() external view returns (uint256); function period() external view returns (uint256); function periods() external view returns (uint256); + function timeout() external view returns (uint256); } diff --git a/src/migration/IToken.sol b/src/migration/IToken.sol index 5efacf3..909fdf5 100644 --- a/src/migration/IToken.sol +++ b/src/migration/IToken.sol @@ -11,5 +11,8 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface ITokenV1 is IERC20 { function owner() external view returns (address); + function stop() external; function stopped() external view returns (bool); + + function update() external; } diff --git a/src/migration/Migration.sol b/src/migration/Migration.sol index 65d05a5..a590c88 100644 --- a/src/migration/Migration.sol +++ b/src/migration/Migration.sol @@ -18,7 +18,7 @@ contract Migration is ICirclesErrors { */ IHubV1 public immutable hubV1; - IHubV2 public immutable hubV2; + IHubV2 public hubV2; /** * @dev Deployment timestamp of Hub v1 contract diff --git a/src/names/NameRegistry.sol b/src/names/NameRegistry.sol index 562a134..cb159dc 100644 --- a/src/names/NameRegistry.sol +++ b/src/names/NameRegistry.sol @@ -14,16 +14,24 @@ contract NameRegistry is Base58Converter, INameRegistryErrors, ICirclesErrors { */ uint72 public constant MAX_SHORT_NAME = uint72(1449225352009601191935); - string public constant DEFAULT_CIRCLES_NAME_PREFIX = "Circles-"; + /** + * @notice The default name prefix for Circles + * @dev to test pre-release codes, we use a toy name prefix + * so that we can easily identify the test Circles + */ + string public constant DEFAULT_CIRCLES_NAME_PREFIX = "Rings-"; - string public constant DEFAULT_CIRCLES_SYMBOL = "CRC"; + /** + * @notice The default symbol for Circles + */ + string public constant DEFAULT_CIRCLES_SYMBOL = "RING"; // State variables /** * @notice The address of the hub contract where the address must have registered first */ - IHubV2 public immutable hub; + IHubV2 public hub; /** * @notice a mapping from the avatar address to the assigned name @@ -158,10 +166,10 @@ contract NameRegistry is Base58Converter, INameRegistryErrors, ICirclesErrors { uint72 shortName = shortNames[_avatar]; if (shortName == uint72(0)) { string memory base58FullAddress = toBase58(uint256(uint160(_avatar))); - return string(abi.encodePacked("Circles-", base58FullAddress)); + return string(abi.encodePacked("DEFAULT_CIRCLES_NAME_PREFIX", base58FullAddress)); } string memory base58ShortName = toBase58(uint256(shortName)); - return string(abi.encodePacked("Circles-", base58ShortName)); + return string(abi.encodePacked("DEFAULT_CIRCLES_NAME_PREFIX", base58ShortName)); } function symbol(address _avatar) external view mustBeRegistered(_avatar, 2) returns (string memory) { diff --git a/test/circles/Circles.t.sol b/test/circles/Circles.t.sol index e6f7a05..993b386 100644 --- a/test/circles/Circles.t.sol +++ b/test/circles/Circles.t.sol @@ -5,19 +5,16 @@ import {Test} from "forge-std/Test.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import "forge-std/console.sol"; import "./MockCircles.sol"; -import "../setup/TimeSetup.sol"; +import "../setup/TimeCirclesSetup.sol"; import "../utils/Approximation.sol"; -contract CirclesTest is Test, TimeSetup, Approximation { +contract CirclesTest is Test, TimeCirclesSetup, Approximation { // Constants uint256 public constant N = 4; - uint256 public constant DAY0 = (3 * 365 days + 100 days) / 1 days; - uint256 public constant EPS = 10 ** (18 - 2); - - uint256 public constant CRC = 10 ** 18; + uint256 public constant FEMTO_EPS = 10 ** (18 - 15); // State variables @@ -31,55 +28,70 @@ contract CirclesTest is Test, TimeSetup, Approximation { function setUp() public { // set time to 15th October 2020 - _setUpTime(INFLATION_DAY_ZERO + 1); - - // 23 january 2024 12:01 am UTC - _forwardTime(DAY0 * 1 days); + startTime(); circles = new MockCircles(INFLATION_DAY_ZERO); for (uint256 i = 0; i < N; i++) { addresses[i] = makeAddr(avatars[i]); - circlesIdentifiers[i] = circles.toTokenId(addresses[i]); + circlesIdentifiers[i] = uint256(uint160(addresses[i])); circles.registerHuman(addresses[i]); } } function testCalculateIssuance() public { - uint256 day = circles.day(block.timestamp); - assertEq(day, DAY0); - uint256 issuance = circles.calculateIssuance(addresses[0]); - assertEq(issuance, 0); + for (uint256 i = 0; i < 100; i++) { + // Generate a pseudo-random number of seconds between 0 and 16 days (14days is max claimable period) + uint256 secondsSkip = uint256(keccak256(abi.encodePacked(block.timestamp, i, uint256(2)))) % 16 days; - _forwardTime(30 minutes); - // still the same day - assertEq(circles.day(block.timestamp), DAY0); - issuance = circles.calculateIssuance(addresses[0]); - assertEq(issuance, 0); + _skipAndMint(secondsSkip, addresses[0]); + } + } - _forwardTime(31 minutes); - issuance = circles.calculateIssuance(addresses[0]); - assertEq(issuance, 1 * CRC); + function testConsecutiveClaimablePeriods() public { + // skipTime(5 hours); // todo: investigate why startTime is zero is this commented out? - _forwardTime(1 days + 2 hours + 15 minutes); - issuance = circles.calculateIssuance(addresses[0]); - assertTrue(approximatelyEqual(issuance, 27 * CRC, EPS)); + uint256 previousEndPeriod = 0; - _forwardTime(1 hours + 40 minutes); - issuance = circles.calculateIssuance(addresses[0]); - assertTrue(approximatelyEqual(issuance, 28 * CRC, EPS)); + for (uint256 i = 0; i < 10; i++) { + // Calculate issuance to get the current start and end periods + (, uint256 startPeriod, uint256 endPeriod) = circles.calculateIssuance(addresses[0]); - vm.prank(addresses[0]); + // For iterations after the first, check if the previous endPeriod matches the current startPeriod + if (i > 0) { + assertEq(previousEndPeriod, startPeriod, "EndPeriod does not match next StartPeriod"); + } + + // Update previousEndPeriod with the current endPeriod for the next iteration + previousEndPeriod = endPeriod; + + // Generate a pseudo-random number between 1 and 4 + uint256 hoursSkip = uint256(keccak256(abi.encodePacked(block.timestamp, i, uint256(0)))) % 4 + 1; + uint256 secondsSkip = uint256(keccak256(abi.encodePacked(block.timestamp, i, uint256(1)))) % 3600; + + // Simulate passing of time variable windows of time (1-5 hours) + skipTime(hoursSkip * 1 hours + secondsSkip); + + // Perform the mint operation as Alice + vm.prank(addresses[0]); + circles.claimIssuance(); + } + + uint256 balanceOfAlice = circles.balanceOf(addresses[0], circlesIdentifiers[0]); + + // now mint for Bob in one go and test that Alice and Bob have the same balance + vm.prank(addresses[1]); circles.claimIssuance(); - uint256 balance = circles.balanceOf(addresses[0], circlesIdentifiers[0]); - assertTrue(approximatelyEqual(balance, 28 * CRC, EPS)); + uint256 balanceOfBob = circles.balanceOf(addresses[1], circlesIdentifiers[1]); + // the difference between Alice and Bob is less than dust + assertTrue(approximatelyEqual(balanceOfAlice, balanceOfBob, FEMTO_EPS)); } function testDemurragedTransfer() public { - _forwardTime(12 * 24 hours + 1 minutes); + skipTime(12 * 24 hours + 1 minutes); for (uint256 i = 0; i < 2; i++) { - uint256 expectedIssuance = circles.calculateIssuance(addresses[i]); + (uint256 expectedIssuance,,) = circles.calculateIssuance(addresses[i]); vm.prank(addresses[i]); circles.claimIssuance(); uint256 balance = circles.balanceOf(addresses[i], circlesIdentifiers[i]); @@ -107,11 +119,23 @@ contract CirclesTest is Test, TimeSetup, Approximation { // Private functions - function _setUpTime(uint256 _time) internal { - vm.warp(_time); - } + function _skipAndMint(uint256 _seconds, address _avatar) private { + // ensure the avatar has no issuance already to start with + (uint256 issuance, uint256 startPeriod, uint256 endPeriod) = circles.calculateIssuance(_avatar); + assertEq(issuance, 0, "Ensure avatar has no issuance"); - function _forwardTime(uint256 _duration) internal { - vm.warp(block.timestamp + _duration); + // skip time + skipTime(_seconds); + + uint256 balanceBefore = circles.balanceOf(_avatar, uint256(uint160(_avatar))); + (issuance, startPeriod, endPeriod) = circles.calculateIssuance(_avatar); + uint256 hoursCount = (endPeriod - startPeriod) / 1 hours; + // console.log("hoursCount", hoursCount, "days:", hoursCount / 24); + vm.prank(_avatar); + circles.claimIssuance(); + uint256 balanceAfter = circles.balanceOf(_avatar, uint256(uint160(_avatar))); + assertEq(balanceAfter - balanceBefore, issuance, "Ensure issuance is minted"); + assertTrue(issuance <= hoursCount * CRC, "Ensure issuance is not more than expected"); + assertTrue(relativeApproximatelyEqual(issuance, hoursCount * CRC, ONE_PERCENT), "Ensure issuance is correct"); } } diff --git a/test/hub/MockMigrationHub.sol b/test/hub/MockMigrationHub.sol new file mode 100644 index 0000000..90ad2b9 --- /dev/null +++ b/test/hub/MockMigrationHub.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "../../src/names/INameRegistry.sol"; +import "../../src/hub/Hub.sol"; +import "../migration/MockHub.sol"; + +contract MockMigrationHub is Hub { + // Constructor + + constructor(IHubV1 _hubV1, address _migration, uint256 _inflationDayZero, uint256 _bootstrapTime) + Hub( + _hubV1, + INameRegistry(address(0)), + _migration, + IERC20Lift(address(0)), + address(1), + _inflationDayZero, + _bootstrapTime, + "" + ) + {} + + // External functions + + function setSiblings(address _migration, address _nameRegistry) external { + migration = _migration; + nameRegistry = INameRegistry(_nameRegistry); + } +} diff --git a/test/hub/PathTransferHub.t.sol b/test/hub/PathTransferHub.t.sol index ce2fbb4..757adbd 100644 --- a/test/hub/PathTransferHub.t.sol +++ b/test/hub/PathTransferHub.t.sol @@ -4,16 +4,13 @@ pragma solidity >=0.8.13; import {Test} from "forge-std/Test.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import "forge-std/console.sol"; -import "../setup/TimeSetup.sol"; +import "../../src/hub/Hub.sol"; +import "../setup/TimeCirclesSetup.sol"; import "../setup/HumanRegistration.sol"; +import "../utils/Approximation.sol"; import "./MockPathTransferHub.sol"; -import "../../src/hub/Hub.sol"; - -contract HubPathTransferTest is Test, TimeSetup, HumanRegistration { - // Constants - - uint256 public constant CRC = uint256(10 ** 18); +contract HubPathTransferTest is Test, TimeCirclesSetup, HumanRegistration, Approximation { // State variables MockPathTransferHub public mockHub; @@ -41,7 +38,8 @@ contract HubPathTransferTest is Test, TimeSetup, HumanRegistration { for (uint256 i = 0; i < N; i++) { vm.prank(addresses[i]); mockHub.personalMintWithoutV1Check(); - assertEq(mockHub.balanceOf(addresses[i], mockHub.toTokenId(addresses[i])), 47985696851874424310); + uint256 balance = mockHub.balanceOf(addresses[i], mockHub.toTokenId(addresses[i])); + assertTrue(relativeApproximatelyEqual(balance, 48 * CRC, ONE_PERCENT)); } // get this value first to avoid using `startPrank` over inline calls diff --git a/test/hub/V1MintStatusUpdate.t.sol b/test/hub/V1MintStatusUpdate.t.sol new file mode 100644 index 0000000..f5eab7d --- /dev/null +++ b/test/hub/V1MintStatusUpdate.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; +import "forge-std/console.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../../src/migration/IToken.sol"; +import "../migration/MockMigration.sol"; +import "../names/MockNameRegistry.sol"; +import "../setup/TimeCirclesSetup.sol"; +import "../setup/HumanRegistration.sol"; +import "../migration/MockHub.sol"; +import "./MockMigrationHub.sol"; + +contract V1MintStatusUpdateTest is Test, TimeCirclesSetup, HumanRegistration { + // Constants + + bytes32 private constant SALT = keccak256("CirclesV2:V1MintStatusUpdateTest"); + + // State variables + + MockMigrationHub public mockHub; + MockHubV1 public mockHubV1; + + MockNameRegistry public nameRegistry; + MockMigration public migration; + + // Constructor + + constructor() HumanRegistration(2) {} + + // Setup + + function setUp() public { + // Set time in 2021 + startTime(); + + mockHubV1 = new MockHubV1(); + // First deploy the contracts to know the addresses + migration = new MockMigration(mockHubV1, IHubV2(address(1))); + nameRegistry = new MockNameRegistry(IHubV2(address(1))); + mockHub = new MockMigrationHub(mockHubV1, address(2), INFLATION_DAY_ZERO, 365 days); + // then set the addresses in the respective contracts + migration.setHubV2(IHubV2(address(mockHub))); + nameRegistry.setHubV2(IHubV2(address(mockHub))); + mockHub.setSiblings(address(migration), address(nameRegistry)); + } + + // Tests + + function testMigrationFromV1DuringBootstrap() public { + // Alice and Bob register in V1 + ITokenV1 tokenAlice = signupInV1(addresses[0]); + ITokenV1 tokenBob = signupInV1(addresses[1]); + + // move time + skipTime(5 days + 1 hours + 31 minutes); + + // Alice and Bob mint in V1 + mintV1Tokens(tokenAlice); + mintV1Tokens(tokenBob); + + // Alice stops her V1 token and registers in V2 + vm.startPrank(addresses[0]); + tokenAlice.stop(); + assertTrue(tokenAlice.stopped(), "Token not stopped"); + mockHub.registerHuman(bytes32(0)); + vm.stopPrank(); + assertTrue(mockHub.isHuman(addresses[0]), "Alice not registered"); + + // Alice invites Bob, while he is still active in V1 + vm.prank(addresses[0]); + mockHub.inviteHuman(addresses[1]); + assertTrue(mockHub.isHuman(addresses[1]), "Bob not registered"); + + // move time + skipTime(5 days - 31 minutes); + + // Alice can mint in V2 + vm.prank(addresses[0]); + mockHub.personalMint(); + + // Bob cannot mint in V2, but can in V1 + vm.expectRevert(); + vm.prank(addresses[1]); + mockHub.personalMint(); + mintV1Tokens(tokenBob); + + // Bob stops his V1 token + vm.prank(addresses[1]); + tokenBob.stop(); + + // Bob can mint in V2, but calculateIssuance will still revert because V1 status not updated + vm.expectRevert(); + mockHub.calculateIssuance(addresses[1]); + // however, sending a transaction to update the V1 status does calculate the issuance + (uint256 issuance, uint256 startPeriod, uint256 endPeriod) = mockHub.calculateIssuanceWithCheck(addresses[1]); + console.log("Bob can mint in V2:", issuance); + console.log("from", startPeriod, "to", endPeriod); + + // move time + skipTime(5 days + 31 minutes); + (issuance, startPeriod, endPeriod) = mockHub.calculateIssuance(addresses[1]); + console.log("Bob can mint in V2:", issuance); + console.log("from", startPeriod, "to", endPeriod); + + // Bob can mint in V2 + vm.prank(addresses[1]); + mockHub.personalMint(); + } + + // Private functions + + function signupInV1(address _user) private returns (ITokenV1) { + vm.prank(_user); + mockHubV1.signup(); + ITokenV1 token = ITokenV1(mockHubV1.userToToken(_user)); + require(address(token) != address(0), "Token not minted"); + require(token.owner() == _user, "Token not owned by user"); + return token; + } + + function mintV1Tokens(ITokenV1 _token) private returns (uint256) { + address owner = _token.owner(); + uint256 balanceBefore = _token.balanceOf(owner); + ITokenV1(_token).update(); + uint256 balanceAfter = _token.balanceOf(owner); + return balanceAfter - balanceBefore; + } +} diff --git a/test/migration/Migration.t.sol b/test/migration/Migration.t.sol index efe843a..7e9a53e 100644 --- a/test/migration/Migration.t.sol +++ b/test/migration/Migration.t.sol @@ -4,7 +4,6 @@ pragma solidity >=0.8.13; import {Test} from "forge-std/Test.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import "../../src/migration/Migration.sol"; -import "../setup/TimeSetup.sol"; import "./MockHub.sol"; contract MigrationTest is Test { diff --git a/test/migration/MockHub.sol b/test/migration/MockHub.sol index 7244bfa..433fe3d 100644 --- a/test/migration/MockHub.sol +++ b/test/migration/MockHub.sol @@ -2,25 +2,46 @@ pragma solidity >=0.8.13; import "../../src/migration/IHub.sol"; +import "./MockToken.sol"; contract MockHubV1 is IHubV1 { - function signup() external pure { - notMocked(); - } + // Constants - function organizationSignup() external pure { - notMocked(); + // parameters taken from: + // https://gnosisscan.io/address/0x29b9a7fbb8995b2423a71cc17cf9810798f6c543/advanced#readContract + uint256 public constant deployedAt = uint256(1602786330); + uint256 public constant initialIssuance = uint256(92592592592592); + uint256 public constant timeout = uint256(7776000); + uint256 public constant inflation = uint256(107); + uint256 public constant divisor = uint256(100); + uint256 public constant period = uint256(31556952); + uint256 public constant signupBonus = uint256(50000000000000000000); + string public constant name = "CirclesV1"; + string public constant symbol = "CRC"; + + // State variables + + // simplify to boolean trust + mapping(address => mapping(address => bool)) public trusts; + + mapping(address => address) public userToToken; + mapping(address => address) public tokenToUser; + + // External functions + + function signup() external { + require(address(userToToken[msg.sender]) == address(0), "You can't sign up twice"); + + Token token = new Token(msg.sender); + userToToken[msg.sender] = address(token); + tokenToUser[address(token)] = msg.sender; + // every user must trust themselves with a weight of 100 + // this is so that all users accept their own token at all times + trust(msg.sender, 100); } - function tokenToUser(address /*token*/ ) external pure returns (address) { + function organizationSignup() external pure { notMocked(); - return address(0); - } - - function userToToken(address /*user*/ ) external pure returns (address) { - // notMocked(); - // return always zero addres, ie. "not signed up in v1" - return address(0); } function limits(address, /*truster*/ address /*trustee*/ ) external pure returns (uint256) { @@ -28,50 +49,35 @@ contract MockHubV1 is IHubV1 { return uint256(0); } - function trust(address, /*trustee*/ uint256 /*limit*/ ) external pure { - notMocked(); + function trust(address _trustee, uint256 _limit) public { + trusts[msg.sender][_trustee] = _limit > 0 ? true : false; } - // parameters taken from: - // https://gnosisscan.io/address/0x29b9a7fbb8995b2423a71cc17cf9810798f6c543/advanced#readContract - function deployedAt() public pure returns (uint256) { - return uint256(1602786330); - } - - function initialIssuance() public pure returns (uint256) { - return uint256(92592592592592); + function issuance() external view returns (uint256) { + return inflate(initialIssuance, periods()); } function inflate(uint256 _initial, uint256 _periods) public pure returns (uint256) { // copy of the implementation from circles contracts v1 // to mirror the same numerical errors as hub v1 has. // https://github.com/CirclesUBI/circles-contracts/blob/master/contracts/Hub.sol#L96-L103 - uint256 q = pow(inflation(), _periods); - uint256 d = pow(divisor(), _periods); + uint256 q = pow(inflation, _periods); + uint256 d = pow(divisor, _periods); return (_initial * q) / d; } - function inflation() public pure returns (uint256) { - return uint256(107); - } - - function divisor() public pure returns (uint256) { - return uint256(100); - } - - function period() public pure returns (uint256) { - return uint256(31556952); + /// @notice finds the inflation rate at a given inflation period + /// @param _periods the step to calculate the issuance rate at + /// @return inflation rate as of the given period + function issuanceByStep(uint256 _periods) public pure returns (uint256) { + return inflate(initialIssuance, _periods); } function periods() public view returns (uint256) { - return (block.timestamp - deployedAt()) / period(); + return (block.timestamp - deployedAt) / period; } - // Private functions - - function notMocked() private pure { - assert(false); - } + // Public functions /// @dev this is an implementation of exponentiation by squares /// @param base the base to be used in the calculation @@ -100,4 +106,10 @@ contract MockHubV1 is IHubV1 { } return base * y; } + + // Private functions + + function notMocked() private pure { + assert(false); + } } diff --git a/test/migration/MockMigration.sol b/test/migration/MockMigration.sol new file mode 100644 index 0000000..7de3cd8 --- /dev/null +++ b/test/migration/MockMigration.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "../../src/migration/Migration.sol"; + +contract MockMigration is Migration { + // Constructor + + constructor(IHubV1 _hubV1, IHubV2 _hubV2) Migration(_hubV1, _hubV2) {} + + // External functions + + function setHubV2(IHubV2 _hubV2) external { + hubV2 = _hubV2; + } +} diff --git a/test/migration/MockToken.sol b/test/migration/MockToken.sol new file mode 100644 index 0000000..8cb4738 --- /dev/null +++ b/test/migration/MockToken.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../../src/migration/IHub.sol"; + +contract Token is ERC20 { + uint256 public lastTouched; // the timestamp of the last ubi payout + address public hub; // the address of the hub this token was deployed through + address public immutable owner; // the safe that deployed this token + uint256 public inflationOffset; // the amount of seconds until the next inflation step + uint256 public currentIssuance; // issanceRate at the time this token was deployed + bool private manuallyStopped; // true if this token has been stopped by it's owner + + /// @dev modifier allowing function to be only called through the hub + modifier onlyHub() { + require(msg.sender == hub); + _; + } + + /// @dev modifier allowing function to be only called by the token owner + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + constructor(address _owner) ERC20("CirclesV1", "CRC") { + require(_owner != address(0)); + owner = _owner; + hub = msg.sender; + lastTouched = time(); + inflationOffset = findInflationOffset(); + currentIssuance = IHubV1(hub).issuance(); + _mint(_owner, IHubV1(hub).signupBonus()); + } + + /// @notice helper function for block timestamp + /// @return the block timestamp + function time() public view returns (uint256) { + return block.timestamp; + } + + /// @notice helper function for the token symbol + /// @dev all circles tokens should have the same symbol + /// @return the token symbol function name() external view returns (string memory); + + function symbol() public view override returns (string memory) { + return IHubV1(hub).symbol(); + } + + /// @notice helper function for the token name + /// @dev all circles tokens should have the same name + /// @return the token name + function name() public view override returns (string memory) { + return IHubV1(hub).name(); + } + + /// @notice helper function for fetching the period length from the hub + /// @return period length in seconds + function period() public view returns (uint256) { + return IHubV1(hub).period(); + } + + /// @notice helper function for fetching the number of periods from the hub + /// @return the number of periods since the hub was deployed + function periods() public view returns (uint256) { + return IHubV1(hub).periods(); + } + + /// @notice helper function for fetching the timeout from the hub + /// @return the number of seconds the token can go without being updated before it's deactivated + function timeout() public view returns (uint256) { + return IHubV1(hub).timeout(); + } + + /// @notice find the inflation step when ubi was last payed out + /// @dev ie. if ubi was last payed out during the second inflation step, returns two + /// @return the inflation step by count + function periodsWhenLastTouched() public view returns (uint256) { + return (lastTouched - hubDeployedAt()) / period(); + } + + /// @notice helper functio for getting the hub deployment time + /// @return the timestamp the hub was deployed at + function hubDeployedAt() public view returns (uint256) { + return IHubV1(hub).deployedAt(); + } + + /// @notice Caution! manually deactivates or stops this token, no ubi will be payed out after this is called + /// @dev intended for use in case of key loss, system failure, or migration to new contracts + function stop() public onlyOwner { + manuallyStopped = true; + } + + /// @notice checks whether this token has been either stopped manually, or whether it has timed out + /// @dev combines the manual stop variable with a dead man's switch + /// @return false is the token is still paying out ubi, otherwise true + function stopped() public view returns (bool) { + if (manuallyStopped) return true; + uint256 secondsSinceLastTouched = time() - lastTouched; + if (secondsSinceLastTouched > timeout()) return true; + return false; + } + + /// @notice the amount of seconds until the ubi payout is next inflated + /// @dev ubi is payed out continuously between inflation steps + /// @return the amount of seconds until the next inflation step + function findInflationOffset() public view returns (uint256) { + // finds the timestamp of the next inflation step, and subtracts the current timestamp + uint256 nextInflation = (period() * (periods() + 1)) + hubDeployedAt(); + return nextInflation - time(); + } + + /// @notice checks how much ubi this token holder is owed, but doesn't update their balance + /// @dev is called in the update method to write the new balance to state, but also useful in wallets + /// @return how much ubi this token holder is owed + function look() public view returns (uint256) { + // don't payout ubi if the token has been deactivated/stopped + if (stopped()) return 0; + uint256 payout = 0; + uint256 clock = lastTouched; + uint256 offset = inflationOffset; + uint256 rate = currentIssuance; + uint256 p = periodsWhenLastTouched(); + // this while loop gets executed only when we're rolling over an inflation step + // in the course of a ubi payout aka while we have to pay out ubi for more time + // than lastTouched + inflationOffset + while (clock + offset <= time()) { + // add the remaining offset time to the payout total at the current rate + payout = payout + (offset * rate); + // adjust clock to the timestamp of the next inflation step + clock = clock + offset; + // the offset is now the length of 1 period + offset = period(); + // increment the period we are paying out for + p = p + 1; + // find the issuance rate as of the next period + rate = IHubV1(hub).issuanceByStep(p); + } + // at this point, time() - clock should always be less than 1 period + uint256 timeSinceLastPayout = time() - clock; + payout = payout + (timeSinceLastPayout * rate); + return payout; + } + + /// @notice receive a ubi payout + /// @dev this is the method to actually update storage with new token balance + function update() public { + uint256 gift = look(); + // does nothing if there's no ubi to be payed out + if (gift > 0) { + // update the state variables used to calculate ubi, then mint + inflationOffset = findInflationOffset(); + lastTouched = time(); + currentIssuance = IHubV1(hub).issuance(); + _mint(owner, gift); + } + } + + /// @notice special method called by the hub to execute a transitive transaction + /// @param from the address the tokens are being transfered from + /// @param to the address the tokens are being transferred to + /// @param amount the amount of tokens to transfer + function hubTransfer(address from, address to, uint256 amount) public onlyHub returns (bool) { + _transfer(from, to, amount); + return true; + } + + function transfer(address dst, uint256 wad) public override returns (bool) { + // this code shouldn't be necessary, but when it's removed the gas estimation methods + // in the gnosis safe no longer work, still true as of solidity 7.1 + return super.transfer(dst, wad); + } +} diff --git a/test/names/MockNameRegistry.sol b/test/names/MockNameRegistry.sol new file mode 100644 index 0000000..fc074f0 --- /dev/null +++ b/test/names/MockNameRegistry.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.13; + +import "../../src/hub/Hub.sol"; +import "../../src/names/NameRegistry.sol"; + +contract MockNameRegistry is NameRegistry { + // Constructor + + constructor(IHubV2 _hubV2) NameRegistry(_hubV2) {} + + // External functions + + function setHubV2(IHubV2 _hubV2) external { + hub = _hubV2; + } +} diff --git a/test/setup/TimeCircleSetup.sol b/test/setup/TimeCircleSetup.sol index fc10d78..dc5ad8e 100644 --- a/test/setup/TimeCircleSetup.sol +++ b/test/setup/TimeCircleSetup.sol @@ -8,9 +8,9 @@ import "../../src/graph/Graph.sol"; import "../../src/circles/TimeCircle.sol"; import "../../src/circles/GroupCircle.sol"; import "../migration/MockHub.sol"; -import "./TimeSetup.sol"; +import "./TimeCirclesSetup.sol"; -contract TimeCircleSetup is TimeSetup { +contract TimeCircleSetup is TimeCirclesSetup { // Constants // number of avatars in the graph uint256 public constant N = 4; diff --git a/test/setup/TimeSetup.sol b/test/setup/TimeCirclesSetup.sol similarity index 89% rename from test/setup/TimeSetup.sol rename to test/setup/TimeCirclesSetup.sol index b603e40..9865078 100644 --- a/test/setup/TimeSetup.sol +++ b/test/setup/TimeCirclesSetup.sol @@ -4,7 +4,11 @@ pragma solidity >=0.8.13; import {Test} from "forge-std/Test.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; -contract TimeSetup is Test { +contract TimeCirclesSetup is Test { + // Constants + + uint256 internal constant CRC = uint256(10 ** 18); + /** * Arbitrary origin for counting time since 10 December 2021 * "Hope" is the thing with feathers - diff --git a/test/utils/Approximation.sol b/test/utils/Approximation.sol index 1d98b4d..d80a091 100644 --- a/test/utils/Approximation.sol +++ b/test/utils/Approximation.sol @@ -1,8 +1,39 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.13; +import "../../src/lib/Math64x64.sol"; + contract Approximation { - function approximatelyEqual(uint256 a, uint256 b, uint256 epsilon) public pure returns (bool) { - return a > b ? a - b <= epsilon : b - a <= epsilon; + // Constants + + int128 private constant ONE = int128(2 ** 64); + + // 1% in 64x64 fixed point: integer approximation of 2**64 / 100 + int128 internal constant ONE_PERCENT = int128(184467440737095516); + + function approximatelyEqual(uint256 _a, uint256 _b, uint256 _epsilon) public pure returns (bool) { + return _a > _b ? _a - _b <= _epsilon : _b - _a <= _epsilon; + } + + function relativeApproximatelyEqual(uint256 _a, uint256 _b, int128 _epsilon) public pure returns (bool) { + require(_epsilon >= 0, "Approximation: negative epsilon"); + require(_epsilon <= ONE, "Approximation: epsilon too large"); + if (_a == _b) { + return true; + } + if (_a == 0 || _b == 0) { + return _epsilon == ONE; + } + + // calculate the absolute difference + uint256 diff = _a > _b ? _a - _b : _b - _a; + + // use the larger of the two values as denominator + uint256 max = _a > _b ? _a : _b; + + // calculate the relative difference + int128 relDiff = Math64x64.divu(diff, max); + + return relDiff <= _epsilon; } }