diff --git a/.github/workflows/contract-verify.yml b/.github/workflows/contract-verify.yml new file mode 100644 index 000000000..d73cea5dd --- /dev/null +++ b/.github/workflows/contract-verify.yml @@ -0,0 +1,44 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: ContractVerify + +on: + push: + branches: [ "main", "develop" ] + paths: + - 'src/contracts/*.h' + - '!src/contracts/math_lib.h' + - '!src/contracts/qpi.h' + - '!src/contracts/TestExample*.h' + - '.github/workflows/contract-verify.yml' + pull_request: + branches: [ "main", "develop" ] + paths: + - 'src/contracts/*.h' + - '!src/contracts/math_lib.h' + - '!src/contracts/qpi.h' + - '!src/contracts/TestExample*.h' + - '.github/workflows/contract-verify.yml' + +jobs: + contract_verify_job: + runs-on: ubuntu-latest + timeout-minutes: 15 # Sometimes the parser can get stuck + name: Verify smart contract files + steps: + # Checkout repo to use files of the repo as input for container action + - name: Checkout + uses: actions/checkout@v4 + - name: Find all contract files to verify + id: filepaths + run: | + files=$(find src/contracts/ -maxdepth 1 -type f -name "*.h" ! -name "*TestExample*" ! -name "*math_lib*" ! -name "*qpi*" -printf "%p\n" | paste -sd, -) + echo "contract-filepaths=$files" >> "$GITHUB_OUTPUT" + - name: Contract verify action step + id: verify + uses: Franziska-Mueller/qubic-contract-verify@v1.0.3 + with: + filepaths: '${{ steps.filepaths.outputs.contract-filepaths }}' diff --git a/.github/workflows/efi-build-develop.yml b/.github/workflows/efi-build-develop.yml index f91e345bb..70a7a1730 100644 --- a/.github/workflows/efi-build-develop.yml +++ b/.github/workflows/efi-build-develop.yml @@ -8,8 +8,12 @@ name: EFIBuild on: push: branches: [ "main", "develop" ] + paths-ignore: + - 'doc/**' pull_request: branches: [ "main", "develop" ] + paths-ignore: + - 'doc/**' env: # Path to the solution file relative to the root of the project. diff --git a/.gitignore b/.gitignore index bf4f57667..08ddaaaad 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,22 @@ x64/ .DS_Store .clang-format tmp + +# Build directories and temporary files +out/build/ +**/Testing/Temporary/ +**/_deps/googletest-src +test/CMakeLists.txt +test/CMakeLists.txt +comp.md +proposal.md +src/Qubic.vcxproj +.claude/settings.local.json +src/Qubic.vcxproj +test/CMakeLists.txt +ANALISIS_STATUS_3.md +.gitignore +node.md +report.md +claude.md +RESUMEN_REVISION.md diff --git a/README.md b/README.md index 168fd9060..8d3a3bcaa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # qubic - node -Qubic Node Source Code - this repository contains the source code of a full qubic node. +Qubic Core Node Source Code - this repository contains the source code of a full qubic node. > MAIN (current version running qubic)
> [![EFIBuild](https://github.com/qubic/core/actions/workflows/efi-build-develop.yml/badge.svg?branch=main)](https://github.com/qubic/core/actions/workflows/efi-build-develop.yml) @@ -9,13 +9,14 @@ Qubic Node Source Code - this repository contains the source code of a full qubi ## Prerequisites To run a qubic node, you need the following spec: -- Bare Metal Server/Computer with at least 8 Cores (high CPU frequency with AVX2 support). AVX-512 support is recommended; check supported CPUs [here](https://www.epey.co.uk/cpu/e/YTozOntpOjUwOTc7YToxOntpOjA7czo2OiI0Mjg1NzUiO31pOjUwOTk7YToyOntpOjA7czoxOiI4IjtpOjE7czoyOiIzMiI7fWk6NTA4ODthOjY6e2k6MDtzOjY6IjQ1NjE1MCI7aToxO3M6NzoiMjM4Nzg2MSI7aToyO3M6NzoiMTkzOTE5OSI7aTozO3M6NzoiMTUwMjg4MyI7aTo0O3M6NzoiMjA2Nzk5MyI7aTo1O3M6NzoiMjE5OTc1OSI7fX1fYjowOw==/) +- Bare Metal Server/Computer with at least 8 Cores (high CPU frequency with AVX2 support). AVX-512 support is recommended - by the end of 2026 only AVX512 will be supported +- Recommended CPU: AMD Epyc 9274F or better - At least 2TB of RAM - 1Gb/s synchronous internet connection - A NVME disk to store data (via NVMe M.2) - UEFI Bios -> You will need the current `spectrum, universe, and contract` files to be able to start Qubic. The latest files can be found in our #computor-operator channel on the Qubic Discord server: https://discord.gg/qubic (inquire there for the files). +> You will need the current `spectrum, universe, and contract` files to be able to start Qubic. The latest files can be downloaded from [https://storage.qubic.li/network](https://storage.qubic.li/network) or ask in our #computor-operator channel on the Qubic Discord server: https://discord.gg/qubic. ### Prepare your Disk 1. Your Qubic Boot device should be formatted as FAT32 with the label QUBIC. @@ -40,16 +41,7 @@ echo -e "o\nY\nd\nn\n\n\n+200G\n\nt\n\nef00\nw\nY" | gdisk /dev/sda ``` /contract0000.XXX /contract0001.XXX -/contract0002.XXX -/contract0003.XXX -/contract0004.XXX -/contract0005.XXX -/contract0006.XXX -/contract0007.XXX -/contract0008.XXX -/contract0009.XXX -/contract0010.XXX -/contract0011.XXX +/contractYYYY.XXX /spectrum.XXX /system /universe.XXX @@ -60,17 +52,7 @@ echo -e "o\nY\nd\nn\n\n\n+200G\n\nt\n\nef00\nw\nY" | gdisk /dev/sda ``` - contract0000.XXX => must be the current contract #0 file. XXX must be replaced with the current epoch. (e.g. `contract0000.114`) - contract0001.XXX => must be the current contract #1 file. XXX must be replaced with the current epoch. (e.g. `contract0001.114`). Data from Qx. -- contract0002.XXX => must be the current contract #2 file. XXX must be replaced with the current epoch. (e.g. `contract0002.114`). Data from Quottery. -- contract0003.XXX => must be the current contract #3 file. XXX must be replaced with the current epoch. (e.g. `contract0003.114`). Data from Random. -- contract0004.XXX => must be the current contract #4 file. XXX must be replaced with the current epoch. (e.g. `contract0004.114`). Data from QUtil. -- contract0005.XXX => must be the current contract #5 file. XXX must be replaced with the current epoch. (e.g. `contract0005.114`). Data from MyLastMatch. -- contract0006.XXX => must be the current contract #6 file. XXX must be replaced with the current epoch. (e.g. `contract0006.114`). Data from GQMPROPO. -- contract0007.XXX => must be the current contract #7 file. XXX must be replaced with the current epoch. (e.g. `contract0007.114`). Data from Swatch. -- contract0008.XXX => must be the current contract #8 file. XXX must be replaced with the current epoch. (e.g. `contract0008.114`). Data from CCF. -- contract0009.XXX => must be the current contract #9 file. XXX must be replaced with the current epoch. (e.g. `contract0009.114`). Data from QEarn. -- contract0010.XXX => must be the current contract #10 file. XXX must be replaced with the current epoch. (e.g. `contract0010.114`). Data from QVault. -- contract0011.XXX => must be the current contract #10 file. XXX must be replaced with the current epoch. (e.g. `contract0011.114`). Data from MSVault. -- Other contract files with the same format as above. For now, we have 6 contracts. +- contractYYYY.XXX => must be the current contract #YYYY file. XXX must be replaced with the current epoch. (e.g. `contract0002.114`). State data from all contracts. - universe.XXX => must be the current universe file. XXX must be replaced with the current epoch. (e.g `universe.114`) - spectrum.XXX => must be the current spectrum file. XXX must be replaced with the current epoch. (e.g `spectrum.114`) - system => to start from scratch, use an empty file. (e.g. `touch system`) @@ -97,19 +79,21 @@ Qubic.efi > To make it easier, you can copy & paste our prepared initial disk from https://github.com/qubic/core/blob/main/doc/qubic-initial-disk.zip -> If you have multiple network interfaces, you may disconnect these before starting qubic. +> If you have multiple network interfaces, you may disconnect these before starting qubic. [Here you see how](https://github.com/qubic/integration/blob/main/Computor-Setup/Disconnect-Unneeded-Devices.md). ### Prepare your Server To run Qubic on your server you need the following: - UEFI Bios - Enabled Network Stack in Bios - Your USB Stick/SSD should be the boot device +- We advice to not disable any CPU virtualization or multi threading ## General Process of deploying a node 1. Find knownPublicPeers public peers (e.g. from: https://app.qubic.li/network/live) -2. Set the needed parameters inside src/private_settings.h (https://github.com/qubic/core/blob/main/src/private_settings.h) -3. Compile Source to EFI -4. Start EFI Application on your Computer +2. Set the needed parameters inside [src/private_settings.h](https://github.com/qubic/core/blob/main/src/private_settings.h) +3. Compile Source to EFI (Release build) +4. Copy the binary to your server +5. Start your server with the EFI Application ## How to run a Listening Node @@ -128,12 +112,12 @@ static unsigned char computorSeeds[][55 + 1] = { }; ``` 2. Add your Operator Identity. -The Operator Identity is used to identify the Operator. The Operator can send Commands to your Node. +The Operator Identity is used to identify the Operator. Many remote commands are only allowed when they are signed by the Operator seed. Use the [CLI](https://github.com/qubic/qubic-cli) to send remote commands. ```c++ #define OPERATOR "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ``` 3. Add static IPs of known public peers (can be obtained from https://app.qubic.li/network/live). -Ideally, add at least 4 including your own IP. +Ideally, add at least 4. Include also the public IP of your server. This IP Address will be propagated to other Qubic nodes. ```c++ static const unsigned char knownPublicPeers[][4] = { {12,13,14,12} @@ -162,7 +146,9 @@ We cannot support you in any case. You are welcome to provide updates, bug fixes ## More Documentation - [How to contribute](doc/contributing.md) -- [Developing a smart contract ](doc/contracts.md) +- [Developing a smart contract](doc/contracts.md) - [Qubic protocol](doc/protocol.md) - [Custom mining](doc/custom_mining.md) - [Seamless epoch transition](SEAMLESS.md) +- [Proposals and voting](doc/contracts_proposals.md) + diff --git a/cli.md b/cli.md new file mode 100644 index 000000000..7d1de0fea --- /dev/null +++ b/cli.md @@ -0,0 +1,440 @@ +./qubic-cli [basic config] [command] [command extra parameters] +-help print this message + +Basic config: + -conf + Specify configuration file. Relative paths will be prefixed by datadir location. See qubic.conf.example. + Notice: variables in qubic.conf will be overrided by values on parameters. + -seed + 55-char seed for private key + -nodeip + IP address of the target node for querying blockchain information (default: 127.0.0.1) + -nodeport + Port of the target node for querying blockchain information (default: 21841) + -scheduletick + Offset number of scheduled tick that will perform a transaction (default: 20) + -force + Do action although an error has been detected. Currently only implemented for proposals. + -enabletestcontracts + Enable test contract indices and names for commands using a contract index parameter. This flag has to be passed before the contract index/name. The node to connect to needs to have test contracts running. + -print-only + Print the raw transaction data without sending it to the network. Useful for offline signing or broadcasting later. +Commands: + +[WALLET COMMANDS] + -showkeys + Generate identity, public key and private key from seed. Seed must be passed either from params or configuration file. + -getbalance + Balance of an identity (amount of qubic, number of in/out txs) + -getasset + Print a list of assets of an identity + -queryassets + Query and print assets information. Skip arguments to get detailed documentation. + -gettotalnumberofassetshares + Get total number of shares currently existing of a specific asset. + -sendtoaddress + Perform a standard transaction to sendData qubic to . A valid seed and node ip/port are required. + -sendtoaddressintick + Perform a standard transaction to sendData qubic to in a specific . A valid seed and node ip/port are required. + +[QUTIL COMMANDS] + -qutilsendtomanyv1 + Performs multiple transaction within in one tick. must contain one ID and amount (space seperated) per line. Max 25 transaction. Fees apply! Valid seed and node ip/port are required. + -qutilburnqubic + Performs burning qubic, valid seed and node ip/port are required. + -qutilburnqubicforcontract + Burns qubic for the specified contract index, valid seed and node ip/port are required. + -qutilqueryfeereserve + Queries the amount of qubic in the fee reserve of the specified contract, valid node ip/port are required. + -qutildistributequbictoshareholders + Distribute QU among shareholders, transferring the same amount of QU for each share. The fee is proportional to the number of shareholders. The remainder that cannot be distributed equally is reimbursed. + -qutilsendtomanybenchmark + Sends transfers of 1 qu to addresses in the spectrum. Max 16.7M transfers total. Valid seed and node ip/port are required. + -qutilcreatepoll + Create a new poll. is the poll's name (32 bytes), is 1 for Qubic or 2 for Asset, is the minimum vote amount, is a 256-byte GitHub link. For Asset polls (type 2), provide a semicolon-separated list of assets in the format 'asset_name,issuer;asset_name,issuer'. Valid seed and node ip/port are required. + -qutilvote + Vote in a poll. is the poll's ID, is the vote amount, and is the selected option (0-63). Valid seed and node ip/port are required. + -qutilgetcurrentresult + Get the current results of a poll. is the poll's ID. Valid node ip/port are required. + -qutilgetpollsbycreator + Get polls created by a specific user. is the creator's identity. Valid node ip/port are required. + -qutilgetcurrentpollid + Get the current poll ID and list of active polls. + -qutilgetpollinfo + Get information about a specific poll by its ID. + -qutilcancelpoll + Cancel a poll by its ID. Only the poll creator can cancel it. Requires seed and node ip/port. + -qutilgetfee + Show current QUTIL fees. + +[BLOCKCHAIN/PROTOCOL COMMANDS] + -gettickdata + Get tick data and write it to a file. Use -readtickdata to examine the file. valid node ip/port are required. + -getquorumtick + Get quorum tick data, the summary of quorum tick will be printed, is fetched by command -getcomputorlist. valid node ip/port are required. + -getcomputorlist + Get computor list of the current epoch. Feed this data to -readtickdata to verify tick data. valid node ip/port are required. + -getnodeiplist + Print a list of node ip from a seed node ip. Valid node ip/port are required. + -gettxinfo + Get tx infomation, will print empty if there is no tx or invalid tx. valid node ip/port are required. + -checktxontick + Check if a transaction is included in a tick. valid node ip/port are required. + -checktxonfile + Check if a transaction is included in a tick (tick data from a file). valid node ip/port are required. + -readtickdata + Read tick data from a file, print the output on screen, COMPUTOR_LIST is required if you need to verify block data + -sendcustomtransaction + Perform a custom transaction (IPO, querying smart contract), valid seed and node ip/port are required. + -dumpspectrumfile + Dump spectrum file into csv. + -dumpuniversefile + Dump universe file into csv. + -dumpcontractfile + Dump contract file into csv. Current supported CONTRACT_ID: 1-QX + -makeipobid + Participating IPO (dutch auction). valid seed and node ip/port, CONTRACT_INDEX are required. + -getipostatus + View IPO status. valid node ip/port, CONTRACT_INDEX are required. + -getactiveipos + View list of active IPOs in this epoch. valid node ip/port are required. + -getsysteminfo + View Current System Status. Includes initial tick, random mining seed, epoch info. + +[NODE COMMANDS] + -getcurrenttick + Show current tick information of a node + -sendspecialcommand + Perform a special command to node, valid seed and node ip/port are required. + -togglemainaux + Remotely toggle Main/Aux mode on node, valid seed and node ip/port are required. + and value are: MAIN or AUX + -setsolutionthreshold + Remotely set solution threshold for future epoch, valid seed and node ip/port are required. + -refreshpeerlist + (equivalent to F4) Remotely refresh the peer list of node, all current connections will be closed after this command is sent, valid seed and node ip/port are required. + -forcenexttick + (equivalent to F5) Remotely force next tick on node to be empty, valid seed and node ip/port are required. + -reissuevote + (equivalent to F9) Remotely re-issue (re-send) vote on node, valid seed and node ip/port are required. + -sendrawpacket + Send a raw packet to nodeip. Valid node ip/port are required. + -synctime + Sync node time with local time, valid seed and node ip/port are required. Make sure that your local time is synced (with NTP)! + -getminingscoreranking + Get current mining score ranking. Valid seed and node ip/port are required. + -getvotecountertx + Get vote counter transaction of a tick: showing how many votes per ID that this tick leader saw from (-675-3) to (-3) + -setloggingmode + Set console logging mode: 0 disabled, 1 low computational cost, 2 full logging. Valid seed and node ip/port are required. + -savesnapshot + Remotely trigger saving snapshot, valid seed and node ip/port are required. + -setexecutionfeemultiplier + Set the multiplier for the conversion of raw execution time to contract execution fees to ( NUMERATOR / DENOMINATOR ), valid seed and node ip/port are required. + -getexecutionfeemultiplier + Get the current multiplier for the conversion of raw execution time to contract execution fees, valid seed and node ip/port are required. + +[SMART CONTRACT COMMANDS] + -callcontractfunction + Call a contract function of contract index and print the output. Valid node ip/port are required. + -invokecontractprocedure + Invoke a procedure of contract index. Valid seed and node ip/port are required. + -setshareholderproposal + Set shareholder proposal in a contract. May overwrite existing proposal, because each seed can have only one proposal at a time. Costs a fee. You need to be shareholder of the contract. + is explained if there is a parsing error. Most contracts only support "Variable|2" (yes/no proposals to change state variable). + -clearshareholderproposal + Clear own shareholder proposal in a contract. Costs a fee. + -getshareholderproposals + Get shareholder proposal info from a contract. + Either pass "active" to get proposals that are open for voting in the current epoch, or "finished" to get proposals of previous epochs not overwritten or cleared yet, or a proposal index. + -shareholdervote + Cast vote(s) for a shareholder proposal in the contract. You need to be shareholder of the contract. + may be a single value to set all your votes (one per share) to the same value. + In this case, is the option in range 0 ... N-1 or "none" (in usual case of option voting), or an arbitrary integer or "none" (if proposal is for scalar voting). + also may be a comma-separated list of pairs of count and value (for example: "3,0,10,1" meaning 3 votes for option 0 and 10 votes for option 1). + If the total count is less than the number of shares you own, the remaining votes will be set to "none". + -getshareholdervotes [VOTER_IDENTITY] + Get shareholder proposal votes of the contract. If VOTER_IDENTITY is skipped, identity of seed is used. + -getshareholderresults + Get the current result of a shareholder proposal. + +[QX COMMANDS] + -qxgetfee + Show current Qx fee. + -qxissueasset + Create an asset via Qx contract. + -qxtransferasset + Transfer an asset via Qx contract. + -qxorder add/remove bid/ask [ISSUER (in qubic format)] [ASSET_NAME] [PRICE] [NUMBER_OF_SHARE] + Set order on Qx. + -qxgetorder entity/asset bid/ask [ISSUER/ENTITY (in qubic format)] [ASSET_NAME (NULL for requesting entity)] [OFFSET] + Get orders on Qx + -qxtransferrights + Transfer asset management rights of shares from QX to another contract. + can be given as name or index. + You need to own/possess the shares to do this (seed required). + +[QTRY COMMANDS] + -qtrygetbasicinfo + Show qtry basic info from a node. + -qtryissuebet + Issue a bet (prompt mode) + -qtrygetactivebet + Show all active bet id. + -qtrygetactivebetbycreator + Show all active bet id of an ID. + -qtrygetbetinfo + Get meta information of a bet + -qtrygetbetdetail + Get a list of IDs that bet on of the bet + -qtryjoinbet + Join a bet + -qtrypublishresult + (Oracle providers only) publish a result for a bet + -qtrycancelbet + (Game operator only) cancel a bet + +[GENERAL QUORUM PROPOSAL COMMANDS] + -gqmpropsetproposal + Set proposal in general quorum proposals contract. May overwrite existing proposal, because each computor can have only one proposal at a time. For success, computor status is needed. + is explained if there is a parsing error. + -gqmpropclearproposal + Clear own proposal in general quorum proposals contract. For success, computor status is needed. + -gqmpropgetproposals + Get proposal info from general quorum proposals contract. + Either pass "active" to get proposals that are open for voting in the current epoch, or "finished" to get proposals of previous epochs not overwritten or cleared yet, or a proposal index. + -gqmpropvote + Vote for proposal in general quorum proposals contract. + is the option in range 0 ... N-1 or "none". + -gqmpropgetvote [VOTER_IDENTITY] + Get vote from general quorum proposals contract. If VOTER_IDENTITY is skipped, identity of seed is used. + -gqmpropgetresults + Get the current result of a proposal (general quorum proposals contract). + -gqmpropgetrevdonation + Get and print table of revenue donations applied after each epoch. + +[CCF COMMANDS] + -ccfsetproposal + Set proposal in computor controlled fund (CCF) contract. May overwrite existing proposal, because each seed can have only one proposal at a time. Costs a fee. + is explained if there is a parsing error. Only "Transfer|2" (yes/no transfer proposals) are allowed in CCF. + For subscription proposals, append subscription parameters to PROPOSAL_STRING: |||. The AMOUNT in the transfer proposal is used as AMOUNT_PER_PERIOD. + To cancel the active subscription, or or should be zero. + -ccfclearproposal + Clear own proposal in CCF contract. Costs a fee. + -ccfgetproposals + Get proposal info from CCF contract. + Either pass "active" to get proposals that are open for voting in the current epoch, or "finished" to get proposals of previous epochs not overwritten or cleared yet, or a proposal index. + -ccfgetsubscription + Get active subscription info for a specific destination from CCF contract. + -ccfvote + Cast vote for a proposal in the CCF contract. + is the option in range 0 ... N-1 or "none". + -ccfgetvote [VOTER_IDENTITY] + Get vote from CCF contract. If VOTER_IDENTITY is skipped, identity of seed is used. + -ccfgetresults + Get the current result of a CCF proposal. + -ccflatesttransfers + Get and print latest transfers of CCF granted by quorum. + -ccfgetregularpayments + Get and print regular payments (subscription payments) made by CCF contract. + +[QEARN COMMANDS] + -qearnlock + lock the qu to Qearn SC. + -qearnunlock + unlock the qu from Qearn SC, unlock the amount of that locked in the epoch . + -qearngetlockinfoperepoch + Get the info(Total locked amount, Total bonus amount) locked in . + -qearngetuserlockedinfo + Get the locked amount that the user locked in the epoch . + -qearngetstateofround + Get the status(not started, running, ended) of the epoch . + -qearngetuserlockstatus + Get the status(binary number) that the user locked for 52 weeks. + -qearngetunlockingstatus + Get the unlocking history of the user. + -qearngetstatsperepoch + Get the Stats(early unlocked amount, early unlocked percent) of the epoch and Stats(total locked amount, average APY) of QEarn SC + -qearngetburnedandboostedstats + Get the Stats(burned amount and average percent, boosted amount and average percent, rewarded amount and average percent in QEarn SC) of QEarn SC + -qearngetburnedandboostedstatsperepoch + Get the Stats(burned amount and percent, boosted amount and percent, rewarded amount and percent in epoch ) of QEarn SC + +[QVAULT COMMANDS] + -qvaultsubmitauthaddress + Submit the new authaddress using multisig address. + -qvaultchangeauthaddress + Change the authaddress using multisig address. is the one of (1, 2, 3). + -qvaultsubmitfees + Submit the new permilles for QcapHolders, Reinvesting, Development using multisig address. the sum of 3 permilles should be 970 because the permille of shareHolder is 30. + -qvaultchangefees + Change the permilles for QcapHolders, Reinvesting, Development using multisig address. the sum of 3 permilles should be 970 because the permille of shareHolder is 30. Get the locked amount that the user locked in the epoch . + -qvaultsubmitreinvestingaddress + Submit the new reinvesting address using multisig address. + -qvaultchangereinvestingaddress + Change the address using multisig address. should be already submitted by -qvaultsubmitreinvestingaddress command. + -qvaultsubmitadminaddress + Submit the admin address using multisig address. + -qvaultchangeadminaddress + Change the admin address using multisig address. should be already submitted by -qvaultsubmitadminaddress command. + -qvaultgetdata + Get the state data of smart contract. anyone can check the changes after using the any command. + -qvaultsubmitbannedaddress + Submit the banned address using multisig address. + -qvaultsavebannedaddress + Save the banned address using multisig address. should be already submitted by -qvaultsubmitbannedaddress command. + -qvaultsubmitunbannedaddress + Submit the unbanned address using multisig address. + -qvaultsaveunbannedaddress + Unban the using the multisig address. should be already submitted by -qvaultsaveunbannedaddress command. + +[MSVAULT COMMANDS] + -msvaultregistervault + Register a vault. Vault's number of votes for proposal approval , vault name (max 32 chars), and a list of owners (separated by commas). Fee applies. + -msvaultdeposit + Deposit qubic into vault given vault ID. + -msvaultreleaseto + Request release qu to destination. Fee applies. + -msvaultresetrelease + Reset release requests. Fee applies. + -msvaultgetvaults + Get list of vaults owned by IDENTITY. + -msvaultgetreleasestatus + Get release status of a vault. + -msvaultgetbalanceof + Get balance of a vault. + -msvaultgetvaultname + Get vault name. + -msvaultgetrevenueinfo + Get MsVault revenue info. + -msvaultgetfees + Get MsVault fees. + -msvaultgetvaultowners + Get MsVault owners given vault ID. + +[QSWAP COMMANDS] + -qswapgetfee + Show current Qswap fees. + -qswapissueasset + Create an asset via Qswap contract. + -qswaptransferasset + Transfer an asset via Qswap contract. + -qswapcreatepool + Create an AMM pool via Qswap contract. + -qswapgetpoolbasicstate + Get the basic info of a pool, totol liquidity, qu reserved, asset reserved. + + -qswapaddliquidity + Add liquidity with restriction to an AMM pool via Qswap contract. + -qswapremoveliquidity + Remove liquidity with restriction from an AMM pool via Qswap contract. + -qswapswapexactquforasset + Swap qu for asset via Qswap contract, only execute if asset_amount_out >= ASSET_AMOUNT_OUT_MIN. + -qswapswapquforexactasset + Swap qu for asset via Qswap contract, only execute if qu_amount_in <= QU_AMOUNT_IN_MAX. + -qswapswapexactassetforqu + Swap asset for qu via Qswap contract, only execute if qu_amount_out >= QU_AMOUNT_OUT_MIN. + -qswapswapassetforexactqu + Swap asset for qu via Qswap contract, only execute if asset_amount_in <= ASSET_AMOUNT_IN_MAX. + -qswapgetliquidityof [LIQUIDITY_STAKER(in qublic format)] + Get the staker's liquidity in a pool. + -qswapquote exact_qu_input/exact_qu_output/exact_asset_input/exact_asset_output + Quote amount_out/amount_in with given amount_in/amount_out. + +[NOSTROMO COMMANDS] + -nostromoregisterintier + Register in tier. + -nostromologoutfromtier + Logout from tier. + -nostromocreateproject + Create a project with the specified token info and start and end date for voting. + -nostromovoteinproject + Vote in the project with in the -> if you want to vote with yes, it should be 1. otherwise it is 0. + -nostromocreatefundraising + + + + + + + + + + + Create a fundraising with the specified token and project infos. + -nostromoinvestinproject + Invest in the fundraising. + -nostromoclaimtoken + Claim your token from SC. + If you invest in the fundraising and also it is the time for claiming, you can receive the token from SC. + -nostromoupgradetierlevel + Upgrade your tierlevel to + -nostromotransfersharemanagementrights + Transfer the share management right to + -nostromogetstats + Get the infos of SC(like total pool weight, epoch revenue, number of registers, number of projects, ...). + -nostromogettierlevelbyuser + Get the tier_level for . + -nostromogetuservotestatus + Get the list of project index voted by . + -nostromochecktokencreatability + Check if the can be issued by SC. + If is already created by SC, it can not be issued anymore. + -nostromogetnumberofinvestedprojects + Get the number invested and project. you can check if the can invest. + The max number that can invest by one user at once in SC is 128 currently. + -nostromogetprojectbyindex + Get the infos of project. + -nostromogetfundraisingbyindex + Get the infos of fundraising. + -nostromogetprojectindexlistbycreator + Get the list of project that created. + -nostromogetinfouserinvested + Get the invseted infos(indexOfFundraising, InvestedAmount, ClaimedAmount). + -nostromogetmaxclaimamount + Get the max claim amount at the moment. + +[QBOND COMMANDS] + -qbondstake + Stake QU and get MBNDxxx token for every million of QU. + -qbondtransfer + Transfer of MBonds of specific to new owner + -qbondaddask + Add ask order of MBonds of at + -qbondremoveask + Remove MBonds of from ask order at + -qbondaddbid + Add bid order of MBonds of at + -qbondremovebid + Remove MBonds of from bid order at + -qbondburnqu + Burn of qu by QBOND sc. + -qbondupdatecfa + Only for admin! Update commission free addresses. must be 0 to remove or 1 to add. + -qbondgetfees + Get fees of QBond sc. + -qbondgetearnedfees + Get earned fees by QBond sc. + -qbondgetinfoperepoch + Get overall information about (stakers amount, total staked, APY) + -qbondgetorders + Get orders of MBonds. + -qbondgetuserorders + Get MBonds orders owner by . + -qbondtable + Get info about APY of each MBond. + -qbondgetusermbonds + Get MBonds owned by the . + -qbondgetcfa + Get list of commission free addresses. + +[TESTING COMMANDS] + -testqpifunctionsoutput + Test that output of qpi functions matches TickData and quorum tick votes for 15 ticks in the future (as specified by scheduletick offset). Requires the TESTEXA SC to be enabled. + -testqpifunctionsoutputpast + Test that output of qpi functions matches TickData and quorum tick votes for the last 15 ticks. Requires the TESTEXA SC to be enabled. + -testgetincomingtransferamounts + Get incoming transfer amounts from either TESTEXB ("B") or TESTEXC ("C"). Requires the TESTEXB and TESTEXC SCs to be +enabled. + -testbidinipothroughcontract + Bid in an IPO either as TESTEXB ("B") or as TESTEXC ("C"). Requires the TESTEXB and TESTEXC SCs to be enabled. diff --git a/doc/contracts.md b/doc/contracts.md index d738d2622..2b9b67f8a 100644 --- a/doc/contracts.md +++ b/doc/contracts.md @@ -68,7 +68,8 @@ In order to develop a contract, follow these steps: - Design and implement the interfaces of your contract (the user procedures and user functions along with its inputs and outputs). The QPI available for implementing the contract is defined in `src/contracts/qpi.h`. - Implement the system procedures needed and remove all the system procedures that are not needed by your contract. - - Add the short form contract name as a prefix to all global constants (if any). + - Follow the general [qubic style guidelines](https://github.com/qubic/core/blob/main/doc/contributing.md#style-guidelines) when writing your code. + - Add the short form contract name as a prefix to all global constants, structs and classes (if any). - Make sure your code is efficient. Execution time will cost fees in the future. Think about the data structures you use, for example if you can use a hash map instead of an array with linear search. Check if you can optimize code in loops and especially in nested loops. @@ -92,10 +93,13 @@ In order to develop a contract, follow these steps: ## Review and tests Each contract must be validated with the following steps: -1. The contract is verified with a special software tool, ensuring that it complies with the formal requirements mentioned above, such as no use of forbidden C++ features. - (Currently, this tool has not been implemented yet. Thus, this check needs to be done during the review in point 3.) +1. The contract is verified with the [Qubic Contract Verification Tool](https://github.com/Franziska-Mueller/qubic-contract-verify), ensuring that it complies with the formal requirements mentioned above, such as no use of [forbidden C++ features](#restrictions-of-c-language-features). + In the `qubic/core` repository, the tool is run automatically as GitHub workflow for PRs to the `develop` and `main` branches (as well as for commits in these branches). + However, since workflow runs on PRs require maintainer approval, we highly recommend to either build the tool from source or use the GitHub action provided in the tool's repository to analyze your contract header file before opening your PR. 2. The features of the contract have to be extensively tested with automated tests implemented within the Qubic Core's GoogleTest framework. 3. The contract and testing code must be reviewed by at least one of the Qubic Core devs, ensuring it meets high quality standards. + For this, open a pull request on GitHub, add a meaningful description about the new contract and request a review from one of the Core devs (preferred: assign via GitHub, otherwise ping on discord). + Disclaimer: The Core devs review for compliance with the restricted C++ language subset and the style guidelines. The contract devs are solely responsible for the correctness of the code, including the safety of the funds. 4. Before integrating the contract in the official Qubic Core release, the features of the contract must be tested in a test network with multiple nodes, showing that the contract works well in practice and that the nodes run stable with the contract. After going through this validation process, a contract can be integrated in official releases of the Qubic Core code. @@ -105,7 +109,7 @@ After going through this validation process, a contract can be integrated in off Steps for deploying a contract: -1. Finish development, review, and tests as written above. +1. Finish development, review, and tests as written above. This includes waiting for approval of your PR by the core dev team. If you need to make any significant changes to the code after the computors accepted your proposal, you will need to make a second proposal. 2. A proposal for including your contract into the Qubic Core needs to be prepared. We recommend to add your proposal description to https://github.com/qubic/proposal/tree/main/SmartContracts via a pull request (this directory also contains files from other contracts added before, which can be used as a template). The proposal description should include a detailed description of your contract (see point 1 of the [Development section](#development)) and the final source code of the contract. @@ -143,7 +147,7 @@ PUBLIC_PROCEDURE(ProcedureName) ### User functions User functions cannot modify the contract's state, but they are useful to query information from the state, either with the network message `RequestContractFunction`, or by a function or procedure of the same or another contract. -Functions can be called by procedures, but procedures cannot be called by functions. +Functions can be called by procedures, but procedures cannot be called by functions. Logging is not allowed inside functions. A user function has to be defined with one of the following the QPI macros, passing the name of the function: `PUBLIC_FUNCTION(NAME)`, `PUBLIC_FUNCTION_WITH_LOCALS(NAME)`, `PRIVATE_FUNCTION(NAME)`, or `PRIVATE_FUNCTION_WITH_LOCALS(NAME)`. @@ -241,9 +245,12 @@ They are defined with the following macros: 8. `POST_RELEASE_SHARES()`: Called after asset management rights were transferred from this contract to another contract that called `qpi.acquireShare()` 9. `POST_ACQUIRE_SHARES()`: Called after asset management rights were transferred to this contract from another contract that called `qpi.releaseShare()`. 10. `POST_INCOMING_TRANSFER()`: Called after QUs have been transferred to this contract. [More details...](#callback-post_incoming_transfer) +11. `SET_SHAREHOLDER_PROPOSAL()`: Called if another contracts tries to set a shareholder proposal in this contract by calling `qpi.setShareholderProposal()`. +12. `SET_SHAREHOLDER_VOTES()`: Called if another contracts tries to set a shareholder proposal in this contract by calling `qpi.setShareholderVotes()`. System procedures 1 to 5 have no input and output. The input and output of system procedures 6 to 9 are discussed in the section about [management rights transfer](#management-rights-transfer). +The system procedure 11 and 12 are discussed in the section about [contracts as shareholder of other contracts](contracts_proposals.md#contracts-as-shareholders-of-other-contracts) The contract state is passed to each of the procedures as a reference named `state`. And it can be modified (in contrast to contract functions). @@ -293,6 +300,10 @@ QX rejects all attempts (`qpi.acquireShares()`) of other contracts to acquire ri TODO +In the universe, NULL_ID is only used for owner / possessor for temporary entries during the IPO between issuing a contract asset and transferring the ownership/possession. +Further, NULL_ID is used for burning asset shares by transferring ownership / possession to NULL_ID. + + ### Management rights transfer There are two ways of transferring asset management rights: @@ -539,16 +550,28 @@ The type of transfer has one of the following values: - `TransferType::ipoBidRefund`: This transfer type is triggered if the contract has placed a bid in a contract IPO with `qpi.bidInIPO()` and QUs are refunded. This can happen in while executing `qpi.bidInIPO()`, when an IPO bid transaction is processed, and when the IPO is finished at the end of the epoch (after `END_EPOCH()` and before `BEGIN_EPOCH()`). -In the implementation of the callback procedure, you cannot run `qpi.transfer()`, `qpi.distributeDividends()`, and `qpi.bidInIPO()`. -That is, calls to these QPI procedures will fail to prevent nested callbacks. +Note that `qpi.invocator()` and `qpi.invocationReward()` will return `0` when called inside of `POST_INCOMING_TRANSFER`. Make sure to use `input.sourceId` and `input.amount` provided via the input struct instead. + +In the implementation of the callback procedure, you cannot run `qpi.distributeDividends()` and `qpi.bidInIPO()`. Calling `qpi.transfer()` is only allowed when transferring to a non-contract entity. +Calls to these QPI procedures will fail to prevent nested callbacks. If you invoke a user procedure from the callback, the fee / invocation reward cannot be transferred. In consequence, the procedure is executed but with `qpi.invocationReward() == 0`. +### Proposals and voting + +Proposals and voting are the on-chain way of decision-making, implemented in contracts. +The function, macros, and data structures provided by the QPI for implementing proposal voting in smart contracts are quite complex. +That is why they are described in a [separate document](contracts_proposals.md). + +CFB has an alternative idea for proposal-free voting on shareholder variables that is supposed to be used in the contracts QX, RANDOM, and MLM. +It has not been implemented yet. +https://github.com/qubic/core/issues/574 + ## Restrictions of C++ Language Features -It is prohibited to locally instantiating objects or variables on the function call stack. -Instead, use the function and procedure definition macros with the postfix `_WITH_LOCALS` (see above). +It is prohibited to locally instantiate objects or variables on the function call stack. This includes loop index variables `for (int i = 0; ...)`. +Instead, use the function and procedure definition macros with the postfix `_WITH_LOCALS` (see above). In procedures you alternatively may store temporary variables permanently as members of the state. Defining, casting, and dereferencing pointers is forbidden. @@ -566,22 +589,25 @@ The division operator `/` and the modulo operator `%` are prohibited to prevent Use `div()` and `mod()` instead, which return zero in case of division by zero. Strings `"` and chars `'` are forbidden, because they can be used to jump to random memory fragments. +If you want to use `static_assert` you can do so via the `STATIC_ASSERT` macro defined in `qpi.h` which does not require a string literal. -Variadic arguments are prohibited (character combination `...`). +Variadic arguments, template parameter packs, and function parameter packs are prohibited (character combination `...`). Double underscores `__` must not be used in a contract, because these are reserved for internal functions and compiler macros that are prohibited to be used directly. For similar reasons, `QpiContext` and `const_cast` are prohibited too. The scope resolution operator `::` is also prohibited, except for structs, enums, and namespaces defined in contracts and `qpi.h`. -The keywords `typedef` and `union` are prohibited to make the code easier to read and prevent tricking code audits. +The keyword `union` is prohibited to make the code easier to read and prevent tricking code audits. +Similarly, the keywords `typedef` and `using` are only allowed in local scope, e.g. inside structs or functions. +The only exception is `using namespace QPI` which can be used at global scope. Global variables are not permitted. -Global constants must begin with the name of the contract state struct. +Global constants, structs and classes must begin with the name of the contract state struct. There is a limit for recursion and depth of nested contract function / procedure calls (the limit is 10 at the moment). -The input and output structs of contract user procedures and functions may only use integer and boolean types (such as `uint64`, `sint8`, `bit`) as well as `id`, `Array`, and `BitArray`. +The input and output structs of contract user procedures and functions may only use integer and boolean types (such as `uint64`, `sint8`, `bit`) as well as `id`, `Array`, and `BitArray`, and struct types containing only allowed types. Complex types that may have an inconsistent internal state, such as `Collection`, are forbidden in the public interface of a contract. @@ -591,6 +617,10 @@ However there are situations where you want to change your SC. ### Bugfix A bugfix is possible at any time. It can be applied during the epoch (if no state is changed) or must be coordinated with an epoch update. +Such state changes are preferably done by extending the state with new data structures at the end while existing state variables remain unchanged. +This provides an easy way to extend the state files with 0 at the end (via command line during epoch transition) and initializing the new state variables in the `BEGIN_EPOCH` procedure. +If this is not possible, the state file can be adjusted with an external tool that computors apply during epoch transition. +This external tool can be written in C++, Python or Bash and the source code has to be public. ### New Features If you want to add new features, this needs to be approved by the computors again. Please refer to the [Deployment](#deployment) for the needed steps. The IPO is not anymore needed for an update of your SC. @@ -627,3 +657,10 @@ The file `proposal.cpp` has a lot of examples showing how to use both functions. For example, `getProposalIndices()` shows how to call a contract function requiring input and providing output with `runContractFunction()`. An example use case of `makeContractTransaction()` can be found in `gqmpropSetProposal()`. The function `castVote()` is a more complex example combining both, calling a contract function and invoking a contract procedure. + + + + + + + diff --git a/doc/contracts_proposals.md b/doc/contracts_proposals.md new file mode 100644 index 000000000..258c0002c --- /dev/null +++ b/doc/contracts_proposals.md @@ -0,0 +1,702 @@ +# Proposal voting + +Proposal voting is the the on-chain way of decision-making. +It is implemented in smart contracts with support of the QPI. + +There are some general characteristics of the proposal voting: + +- Proposal voting is implemented in smart contracts. +- A new proposal is open for voting until the end of the epoch. After the epoch transition, it changes its state from active to finished. +- Each proposal has a type and some types of proposals commonly trigger action (such as setting a contract state variable or transferring QUs to another entity) after the end of the epoch if the proposal is accepted by getting enough votes. +- The proposer entity can have at most one proposal at a time. Setting a new proposal with the same seed will overwrite the previous one. +- Number of simultaneous proposals per epoch is limited as configured by the contract. The data structures storing the proposal and voting state are stored as a part of the contract state. +- In this data storage, commonly named `state.proposals`, and the function/procedure interface, each proposal is identified by a proposal index. +- The types of proposals that are allowed are restricted as configured by the contract. +- The common types of proposals have a predefined set of options that the voters can vote for. Option 0 is always "no change". +- Each vote, which is connected to each voter entity, can have a value (most commonly an option index) or `NO_VOTE_VALUE` (which means abstaining). +- The entities that are allowed to create/change/cancel proposals are configured by the contract dev. The same applies to the entities that are allowed to vote. Checking eligibility is done by the proposal QPI functions provided. +- The common rule for accepting a proposal option (the one with most votes) is that at least 2/3 of the eligible votes have been casted and that at least the option gets more than 1/3 of the eligible votes. +- Depending on the required features, different data structures can be used. The most lightweight option (in terms of storage, compute, and network bandwidth) is supporting yes/no proposals only (2 options). + +In the following, we first address the application that is most relevant for contract devs, which is shareholder voting about state variables, such as fees. + +1. [Introduction to Shareholder Proposals](#introduction-to-shareholder-proposals) +2. [Understanding Shareholder Proposal Voting Implementation](#understanding-shareholder-proposal-voting-implementation) +3. [Voting by Computors](#voting-by-computors) +4. [Proposal Types](#proposal-types) + + +## Introduction to Shareholder Proposals + +The entities possessing the 676 shares of the contract (initially sold in the IPO) are the contract shareholders. +Typically, they get dividends from the contract revenues and decide about contract-related topics via voting. + +The most relevant use case of voting is changing state variables that control the behavior of the contract. For example: + +- how many QUs need to be paid as fees for using the contract procedures, +- how much of the revenue is paid as dividends to the shareholders, +- how much of the revenue is burned. + +Next to the general characteristics described in the section above, shareholder voting has the following features: + +- Only contract shareholders can propose and vote. +- There is one vote per share, so one shareholder can have multiple votes. +- A shareholder can distribute their votes to multiple vote values. +- Shares can be sold during the epoch. The right to vote in an active proposal isn't sold with the share, but the right to vote is fixed to the entities possessing the share in the moment of creating/changing the proposal. +- Contracts can be shareholders of other contracts. For that case, there are special requirements to facilitate voting and creating proposals discussed [here](#contracts-as-shareholders-of-other-contracts). +- Standardized interface to reuse existing tools for shareholder voting in new contracts. +- Macros for implementing the most common use cases as easily as possible. + + +### The easiest way to support shareholder voting + +The following shows how to easily implement the default shareholder proposal voting in your contract. + +Features: + +- Yes/no proposals for changing a single state variable per proposal, +- Usual checking that the invocator has the right to propose / vote and that the proposal data is valid, +- Check that proposed variable values are not negative (default use case: invocation fees), +- Check that index of variable in proposal is less than number of variables configured by contract dev, +- Allow charging fee for creating/changing/canceling proposal in order to avoid spam (fee is burned), +- Compatible with shareholder proposal voting interface already implemented in qubic-cli + +If you need more than these features, go through the following steps anyway and continue reading the section about understanding the shareholder voting implementation. + +#### 1. Setup proposal storage + +First, you need to add the proposal storage to your contract state. +You can easily do this using the QPI macro `DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(numProposalSlots, assetName)`. +With the yes/no shareholder proposals supported by this macro, each proposal slot occupies 22144 Bytes of state memory. +The number of proposal slots limits how many proposals can be open for voting simultaneously. +The `assetName` that you have to pass as the second argument is the `uint64` representation of your contract's 7-character asset name. + +You can get the `assetName` by running the following code in any contract test code (such as "test/contract_qutil.cpp"): + +```C++ +std::cout << assetNameFromString("QUTIL") << std::endl; +``` + +Replace "QUTIL" by your contract's asset name as given in `contractDescriptions` in `src/contact_core/contract_def.h`. +You will get an integer that we recommend to assign to a `constexpr uint64` with a name following the scheme `QUTIL_CONTRACT_ASSET_NAME`. + +When you have decided about the number of proposal slots and found out the the asset name, you can define the proposal storage similarly to this example taken from the contract QUTIL: + +```C++ +struct QUTIL +{ + // other state variables ... + + DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(8, QUTIL_CONTRACT_ASSET_NAME); + + // ... +}; +``` + +`DEFINE_SHAREHOLDER_PROPOSAL_STORAGE` defines a state object `state.proposals` and the types `ProposalDataT`, `ProposersAndVotersT`, and `ProposalVotingT`. +Make sure to have no name clashes with these. +Using other names isn't possible if you want to benefit from the QPI macros for simplifying the implementation. + + +#### 2. Implement code for updating state variables + +After voting in a proposal is closed, the results need to be checked and state variables may need to be updated. +This typically happens in the system procedure `END_EPOCH`. + +In order to simplify the implementation of the default case, we provide a QPI macro for implementing a procedure `FinalizeShareholderStateVarProposals()` that you can call from `END_EPOCH`. + +The macro `IMPLEMENT_FinalizeShareholderStateVarProposals()` can be used as follow: + +```C++ +struct QUTIL +{ + // ... + + IMPLEMENT_FinalizeShareholderStateVarProposals() + { + // When you call FinalizeShareholderStateVarProposals(), the following code is run for each + // proposal of the current epoch that has been accepted. + // + // Your code should set the state variable that the proposal is about to the accepted value. + // This can be done as in this example taken from QUTIL: + + switch (input.proposal.variableOptions.variable) + { + case 0: + state.smt1InvocationFee = input.acceptedValue; + break; + case 1: + state.pollCreationFee = input.acceptedValue; + break; + case 2: + state.pollVoteFee = input.acceptedValue; + break; + case 3: + state.distributeQuToShareholderFeePerShareholder = input.acceptedValue; + break; + case 4: + state.shareholderProposalFee = input.acceptedValue; + break; + } + } + + // ... +} +``` + +The next step is to call `FinalizeShareholderStateVarProposals()` in `END_EPOCH`, in order to make sure the state variables are changed after a proposal has been accepted. + +```C++ + END_EPOCH() + { + // ... + CALL(FinalizeShareholderStateVarProposals, input, output); + // ... + } +``` + +Note that `input` and `output` are instances of `NoData` passed to `END_EPOCH` (due to the requirements of the generalized contract procedure/function interface). +`FinalizeShareholderStateVarProposals` also has `NoData` as input and output, so the variables `input` and `output` available in `END_EPOCH` can be used instead of adding empty locals. + + +#### 3. Add default implementation of required procedures and functions + +The required procedures and functions of the standardized shareholder proposal voting interface can be implemented with the macro: +`IMPLEMENT_DEFAULT_SHAREHOLDER_PROPOSAL_VOTING(numFeeStateVariables, setProposalFeeVarOrValue)`. + +As `numFeeStateVariables`, pass the number of non-negative state variables (5 in the QUTIL example). + +`setProposalFeeVarOrValue` is the fee to be payed for adding/changing/canceling a shareholder proposal. Here you may pass as a state variable or a constant value. + +The macro can be used as follows (example from QUTIL): + +```C++ +struct QUTIL +{ + // ... + + IMPLEMENT_DEFAULT_SHAREHOLDER_PROPOSAL_VOTING(5, state.shareholderProposalFee) + + // ... +} +``` + +#### 4. Register required procedures and functions + +Finally, the required procedures and functions of the standardized shareholder proposal voting interface can be registered with the macro +`REGISTER_SHAREHOLDER_PROPOSAL_VOTING()` in `REGISTER_USER_FUNCTIONS_AND_PROCEDURES()`. Example: + +```C++ + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + // ... + + REGISTER_SHAREHOLDER_PROPOSAL_VOTING(); + } +``` + +Please note that `REGISTER_SHAREHOLDER_PROPOSAL_VOTING()` registers the following input types: + +- user functions 65531 - 65535 via `REGISTER_USER_FUNCTION` +- user procedures 65534 - 65535 via `REGISTER_USER_PROCEDURE` + +So make sure that your contract does not use these input types for other functions or procedures. + +#### 5. Test with qubic-cli + +After following the steps above, your contract is ready for changing fees and similar state variables via shareholder proposal voting. +When you run the node with your contract, you may test the voting with [qubic-cli](https://github.com/qubic/qubic-cli). + +In order to enable it, you need to add your contract to the array `shareholderProposalSupportPerContract` in `proposals.cpp` (with the type `DefaultYesNoSingleVarShareholderProposalSupported`). +Now you should be able to use `-setshareholderproposal` and the other commands with your contract. + +Optionally, if you want to use your contract name as ContractIndex when running qubic-cli, you may add it to `getContractIndex()` in `argparser.h`. + + +## Understanding Shareholder Proposal Voting Implementation + +In the section above, several macros have been used to easily implement a shareholder voting system that should suit the needs of many contracts. +If you require additional features, continue reading in order to learn how to get them. + +### Required procedures, functions, types, and data + +The following elements are required to support shareholder proposals and voting: + +- `END_EPOCH`: This system procedure is required in order to change your state variables if a proposal is accepted. The macro `IMPLEMENT_FinalizeShareholderStateVarProposals` provides a convenient implementation for simple use cases that can be called from `END_EPOCH`. +- `SetShareholderProposal`: This procedure is for creating, changing, and canceling shareholder proposals. It often requires a custom implementation, because it checks custom requirements defined by the contract developers, for example about which types of proposals are allowed and if the proposed values are valid for the contract. +- `GetShareholderProposal`: This function returns the proposal data (without votes or summarized results). This only requires a custom implementation if you want to support changing multiple variables in one proposal. +- `GetShareholderProposalIndices`: This function lists proposal indices of active or finished proposals. It usually doesn't require a custom implementation. +- `GetShareholderProposalFees`: This function returns the fee that is to be paid for invoking `SetShareholderProposal` and `SetShareholderVotes`. If you want to charge a fee `SetShareholderVotes` (default is no fee), you need a custom implementation of both `GetShareholderProposalFees` and `SetShareholderVotes`. +- `SetShareholderVotes`: Procedure for setting the votes. Only requires a custom implementation if your want to charge fees. +- `GetShareholderVotes`: Function for getting the votes of a shareholder. Usually shouldn't require a custom implementation. +- `GetShareholderVotingResults`: Function for getting the vote results summary. Usually doesn't require a custom implementation. +- `SET_SHAREHOLDER_PROPOSAL` and `SET_SHAREHOLDER_VOTES`: These are notification procedures required to handle voting of other contracts that are shareholder of your contract. They usually just invoke `SetShareholderProposal` or `SetShareholderVote`, respectively. +- Proposal data storage and types: The default implementations expect the object `state.proposals` and the types `ProposalDataT`, `ProposersAndVotersT`, and `ProposalVotingT`, which can be defined via `DEFINE_SHAREHOLDER_PROPOSAL_STORAGE` in some cases. + +QPI provides default implementations through several macros, as used in the [Introduction to Shareholder Proposals](#introduction-to-shareholder-proposals). +The following tables gives an overview about when the macros can be used. +In the columns there are four use cases, three with one variable per proposal suitable if the variables are independent of each other. +Yes/no is the default addressed with the simple macro implementation above. +N option is if you want to support proposals more than 2 options (yes/no), like: value1, value2, value3, or "no change"? +Scalar means that every vote can have a different scalar value. The current implementation computes the mean value of all votes as the final voting result. +Finally, multi-variable proposals change more than one variable if accepted. These make sense if the variables in the state cannot be changed independently of each other, but must be set together in order to ensure keeping a valid state. + + +Default implementation can be used? | 1-var yes/no | 1-var N option | 1-var scalar | multi-var +------------------------------------------------|--------------|----------------|--------------|----------- +`DEFINE_SHAREHOLDER_PROPOSAL_STORAGE` | X | | | X +`IMPLEMENT_FinalizeShareholderStateVarProposals`| X | | | +`IMPLEMENT_SetShareholderProposal` | X | | | +`IMPLEMENT_GetShareholderProposal` | X | X | X | +`IMPLEMENT_GetShareholderProposalIndices` | X | X | X | X +`IMPLEMENT_GetShareholderProposalFees` | X | X | X | X +`IMPLEMENT_SetShareholderVotes` | X | X | X | X +`IMPLEMENT_GetShareholderVotes` | X | X | X | X +`IMPLEMENT_GetShareholderVotingResults` | X | X | X | X +`IMPLEMENT_SET_SHAREHOLDER_PROPOSAL` | X | X | X | X +`IMPLEMENT_SET_SHAREHOLDER_VOTES` | X | X | X | X +tool `qubic-cli -shareholder*` | X | X | soon | +Example contracts | QUTIL | TESTEXB | TESTEXB | TESTEXA + + +If you need a custom implementation of one of the elements, I recommend to start with the default implementations given below. +You may also have a look into the example contracts given in the table. + + +#### Proposal types and storage + +The default implementation of `DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(assetNameInt64, numProposalSlots)` is defined as follows: + +```C++ + public: + typedef ProposalDataYesNo ProposalDataT; + typedef ProposalAndVotingByShareholders ProposersAndVotersT; + typedef ProposalVoting ProposalVotingT; + protected: + ProposalVotingT proposals; +``` + +With `ProposalDataT` your have the following options: +- `ProposalDataYesNo`: Proposals with two options, yes (change) and no (no change) +- `ProposalDataV1`: Support multi-option proposals with up to 5 options for the Variable and Transfer proposal types and up to 8 options for GeneralProposal and MultiVariables. No scalar voting support. +- `ProposalDataV1`: Support scalar voting and multi-option voting. This leads to the highest resource requirements, because 8 bytes of storage are required per proposal and voter. + +`ProposersAndVotersT` defines the class used to manage rights to propose and vote. It's important that you pass the correct asset name of your contract. Otherwise it won't find the right shareholders. +The number of proposal slots linearly scales the storage and digest compute requirements. So we recommend to use a quite low number here, similar to the number of variables that can be set in your state. + +`ProposalVotingT` combines `ProposersAndVotersT` and `ProposalDataT` into the class used for storing all proposal and voting data. +It is instantiated as `state.proposals`. + +In order to support MultiVariables proposals that allow to change multiple variables in a single proposal, the variable values need to be stored separately, for example in an array of `numProposalSlots` structs, one for each potential proposal. +See the contract TestExampleA to see how to support multi-variable proposals. + + +#### User Procedure FinalizeShareholderStateVarProposals for easy implementation of END_EPOCH + +This default implementation works for yes/no proposals. For a version that supports more options and scalar votes, see the contract "TestExampleB". + + +```C++ +IMPLEMENT_FinalizeShareholderStateVarProposals() +{ + // your code for setting state variable, which is called by FinalizeShareholderStateVarProposals() + // after a proposal has been accepted +} +``` + +The implementation above is expanded to the following code: + +```C++ +struct FinalizeShareholderProposalSetStateVar_input +{ + sint32 proposalIndex; + ProposalDataT proposal; + ProposalSummarizedVotingDataV1 results; + sint32 acceptedOption; + sint64 acceptedValue; +}; +typedef NoData FinalizeShareholderProposalSetStateVar_output; + +typedef NoData FinalizeShareholderStateVarProposals_input; +typedef NoData FinalizeShareholderStateVarProposals_output; +struct FinalizeShareholderStateVarProposals_locals +{ + FinalizeShareholderProposalSetStateVar_input p; + uint16 proposalClass; +}; + +PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeShareholderStateVarProposals) +{ + // Analyze proposal results and set variables: + // Iterate all proposals that were open for voting in this epoch ... + locals.p.proposalIndex = -1; + while ((locals.p.proposalIndex = qpi(state.proposals).nextProposalIndex(locals.p.proposalIndex, qpi.epoch())) >= 0) + { + if (!qpi(state.proposals).getProposal(locals.p.proposalIndex, locals.p.proposal)) + continue; + + locals.proposalClass = ProposalTypes::cls(locals.p.proposal.type); + + // Handle proposal type Variable / MultiVariables + if (locals.proposalClass == ProposalTypes::Class::Variable || locals.proposalClass == ProposalTypes::Class::MultiVariables) + { + // Get voting results and check if conditions for proposal acceptance are met + if (!qpi(state.proposals).getVotingSummary(locals.p.proposalIndex, locals.p.results)) + continue; + + locals.p.acceptedOption = locals.p.results.getAcceptedOption(); + if (locals.p.acceptedOption <= 0) + continue; + + locals.p.acceptedValue = locals.p.proposal.variableOptions.value; + + CALL(FinalizeShareholderProposalSetStateVar, locals.p, output); + } + } +} + +PRIVATE_PROCEDURE(FinalizeShareholderProposalSetStateVar) +{ + // your code for setting state variable, which is called by FinalizeShareholderStateVarProposals() + // after a proposal has been accepted +} +``` + + +#### User Procedure SetShareholderProposal + +The default implementation provided by `IMPLEMENT_SetShareholderProposal(numFeeStateVariables, setProposalFeeVarOrValue)` supports yes/no for setting one variable per proposal, like "I propose to change transfer fee to 1000 QU. Yes or no?". +Although it only supports one variable per proposal, an arbitrary number of different variables can be supported across multiple proposals. + +```C++ +typedef ProposalDataT SetShareholderProposal_input; +typedef uint16 SetShareholderProposal_output; + +PUBLIC_PROCEDURE(SetShareholderProposal) +{ + // check proposal input and fees + if (qpi.invocationReward() < setProposalFeeVarOrValue || (input.epoch + && (input.type != ProposalTypes::VariableYesNo || input.variableOptions.variable >= numFeeStateVariables + || input.variableOptions.value < 0))) + { + // error -> reimburse invocation reward + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output = INVALID_PROPOSAL_INDEX; + return; + } + + // try to set proposal (checks invocator's rights and general validity of input proposal), returns proposal index + output = qpi(state.proposals).setProposal(qpi.invocator(), input); + if (output == INVALID_PROPOSAL_INDEX) + { + // error -> reimburse invocation reward + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // burn fee and reimburse if payed too much + qpi.burn(setProposalFeeVarOrValue); + if (qpi.invocationReward() > setProposalFeeVarOrValue) + qpi.transfer(qpi.invocator(), qpi.invocationReward() - setProposalFeeVarOrValue); +} +``` + +Note that `input.epoch == 0` means clearing a proposal. +Returning the invalid proposal `output = INVALID_PROPOSAL_INDEX` signals an error. + + +#### User Function GetShareholderProposal + +`IMPLEMENT_GetShareholderProposal()` defines the following user function, which returns the proposal without votes or summarized results: + +```C++ +struct GetShareholderProposal_input +{ + uint16 proposalIndex; +}; +struct GetShareholderProposal_output +{ + ProposalDataT proposal; + id proposerPubicKey; +}; + +PUBLIC_FUNCTION(GetShareholderProposal) +{ + // On error, output.proposal.type is set to 0 + output.proposerPubicKey = qpi(state.proposals).proposerId(input.proposalIndex); + qpi(state.proposals).getProposal(input.proposalIndex, output.proposal); +} +``` + + +#### User Function GetShareholderProposalIndices + +The macro `IMPLEMENT_GetShareholderProposalIndices()` adds the following contract user function, which is required for listing +the active and inactive proposals. + +```C++ +struct GetShareholderProposalIndices_input +{ + bit activeProposals; // Set true to return indices of active proposals, false for proposals of prior epochs + sint32 prevProposalIndex; // Set -1 to start getting indices. If returned index array is full, call again with highest index returned. +}; +struct GetShareholderProposalIndices_output +{ + uint16 numOfIndices; // Number of valid entries in indices. Call again if it is 64. + Array indices; // Requested proposal indices. Valid entries are in range 0 ... (numOfIndices - 1). +}; + +PUBLIC_FUNCTION(GetShareholderProposalIndices) +{ + if (input.activeProposals) + { + // Return proposals that are open for voting in current epoch + // (output is initialized with zeros by contract system) + while ((input.prevProposalIndex = qpi(state.proposals).nextProposalIndex(input.prevProposalIndex, qpi.epoch())) >= 0) + { + output.indices.set(output.numOfIndices, input.prevProposalIndex); + ++output.numOfIndices; + + if (output.numOfIndices == output.indices.capacity()) + break; + } + } + else + { + // Return proposals of previous epochs not overwritten yet + // (output is initialized with zeros by contract system) + while ((input.prevProposalIndex = qpi(state.proposals).nextFinishedProposalIndex(input.prevProposalIndex)) >= 0) + { + output.indices.set(output.numOfIndices, input.prevProposalIndex); + ++output.numOfIndices; + + if (output.numOfIndices == output.indices.capacity()) + break; + } + } +} +``` + + +#### User Function GetShareholderProposalFees + +`IMPLEMENT_GetShareholderProposalFees(setProposalFeeVarOrValue)` provides the following implementation for returning fees for invoking the shareholder proposal and voting procedures. + +```C++ +typedef NoData GetShareholderProposalFees_input; +struct GetShareholderProposalFees_output +{ + sint64 setProposalFee; + sint64 setVoteFee; +}; + +PUBLIC_FUNCTION(GetShareholderProposalFees) +{ + output.setProposalFee = setProposalFeeVarOrValue; + output.setVoteFee = 0; +} +``` + +The default implementation assumes no vote fee, but the output struct and qubic-cli support voting fees. +So if you want to charge a fee for invoking `SetShareholderVotes`, you can copy the template above and just replace the 0. + + +#### User Procedure SetShareholderVotes + +The default implementation of `IMPLEMENT_SetShareholderVotes()` supports all use cases, but does not charge a fee: + +```C++ +typedef ProposalMultiVoteDataV1 SetShareholderVotes_input; +typedef bit SetShareholderVotes_output; + +PUBLIC_PROCEDURE(SetShareholderVotes) +{ + output = qpi(state.proposals).vote(qpi.invocator(), input); +} +``` + +#### User Function GetShareholderVotes + +`IMPLEMENT_GetShareholderVotes()` provides the default implementation for returning the votes of a specific voter / shareholder. + +```C++ +struct GetShareholderVotes_input +{ + id voter; + uint16 proposalIndex; +}; +typedef ProposalMultiVoteDataV1 GetShareholderVotes_output; + +PUBLIC_FUNCTION(GetShareholderVotes) +{ + // On error, output.votes.proposalType is set to 0 + qpi(state.proposals).getVotes(input.proposalIndex, input.voter, output); +} +``` + +#### User Function GetShareholderVotingResults + +`IMPLEMENT_GetShareholderVotingResults()` provides the function returning the overall voting results of a proposal: + +```C++ +struct GetShareholderVotingResults_input +{ + uint16 proposalIndex; +}; +typedef ProposalSummarizedVotingDataV1 GetShareholderVotingResults_output; + +PUBLIC_FUNCTION(GetShareholderVotingResults) +{ + // On error, output.totalVotesAuthorized is set to 0 + qpi(state.proposals).getVotingSummary( + input.proposalIndex, output); +} +``` + +### Contracts as shareholders of other contracts + +When a contract A is shareholder of another contract B, the user procedures `B::SetShareholderProposal()` and `B::SetShareholderVotes()` cannot invoked directly via transaction as usual for "normal" user entities. + +Further, the contract owning the share may be older than the contract it is shareholder of. +So the procedures cannot be invoked as in other contract interaction, because contracts can only access procedure/function of older contracts. + +In order to provide a solution for this issue, the two QPI calls `qpi.setShareholderProposal()` and `qpi.setShareholderVotes()` were introduced. +They can be called from contract A and invoke the system procedures `B::SET_SHAREHOLDER_PROPOSAL` and `B::SET_SHAREHOLDER_VOTES`, respectively. +The system procedure `B::SET_SHAREHOLDER_PROPOSAL` and `B::SET_SHAREHOLDER_VOTES` call the user procedures `B::SetShareholderProposal()` and `B::SetShareholderVotes()`. + +The mechanism is shown with both contracts TestExampleA and TestExampleB, which are shareholder of each other in `test/contract_testex.cpp`. +A custom user procedure `SetProposalInOtherContractAsShareholder` calls `qpi.setShareholderProposal()` with input given by the invocator who must be an administrator (or someone else who is allowed to create proposals in the name of the contract). +Similarly, a custom user procedure `SetVotesInOtherContractAsShareholder` calls `qpi.setShareholderVotes()` with input given by the invocator who must be an administrator (or someone else who is allowed to cast votes in the name of the contract, for example based on a community poll). + + +#### System Procedure SET_SHAREHOLDER_PROPOSAL + +`IMPLEMENT_SET_SHAREHOLDER_PROPOSAL()` adds a system procedure invoked when `qpi.setShareholderProposal()` is called in another contract that is shareholder of your contract and wants to create/change/cancel a shareholder proposal in your contract. +The input is a generic buffer of 1024 bytes size that is copied into the input structure of `SetShareholderProposal` before calling this procedure. +`SetShareholderProposal_input` may be custom defined, as in multi-variable proposals. That is why a generic buffer is needed in the cross-contract interaction. + +```C++ +struct SET_SHAREHOLDER_PROPOSAL_locals +{ + SetShareholderProposal_input userProcInput; +}; + +SET_SHAREHOLDER_PROPOSAL_WITH_LOCALS() +{ + copyFromBuffer(locals.userProcInput, input); + CALL(SetShareholderProposal, locals.userProcInput, output); +} +``` + +The input and output of `SET_SHAREHOLDER_PROPOSAL` are defined as follows in `qpi.h`: + +```C++ + // Input of SET_SHAREHOLDER_PROPOSAL system procedure (buffer for passing the contract-dependent proposal data) + typedef Array SET_SHAREHOLDER_PROPOSAL_input; + + // Output of SET_SHAREHOLDER_PROPOSAL system procedure (proposal index, or INVALID_PROPOSAL_INDEX on error) + typedef uint16 SET_SHAREHOLDER_PROPOSAL_output; +``` + +#### System Procedure SET_SHAREHOLDER_VOTES + +`IMPLEMENT_SET_SHAREHOLDER_VOTES()` adds a system procedure invoked when `qpi.setShareholderVotes()` is called in another contract that is shareholder of your contract and wants to set shareholder votes in your contract. +It simply calls the user procedure `SetShareholderVotes`. Input and output are the same. + +```C++ +SET_SHAREHOLDER_VOTES() +{ + CALL(SetShareholderVotes, input, output); +} +``` + +The input and output of `SET_SHAREHOLDER_VOTES` are defined as follows in `qpi.h`: + +```C++ + // Input of SET_SHAREHOLDER_VOTES system procedure (vote data) + typedef ProposalMultiVoteDataV1 SET_SHAREHOLDER_VOTES_input; + + // Output of SET_SHAREHOLDER_VOTES system procedure (success flag) + typedef bit SET_SHAREHOLDER_VOTES_output; +``` + + +## Voting by Computors + +Currently, the following contracts implement voting by computors: + +- GQMPROP: General Quorum Proposals are made for deciding about inclusion of new contracts, weekly computor revenue donations, and other major strategic decisions related to the Qubic project. +- CCF: The Computor Controlled Fund is a contract for financing approved projects that contribute to expanding the capabilities, reach, or efficiency of the Qubic network. Projects are proposed by community members and selected through voting by the computors. + +It is similar to shareholder voting and both share a lot of the code. +There are two major differences: + +1. Each computor has exactly one vote. In shareholder voting, a shareholder often has multiple votes / shares. +2. The entities who are allowed to propose and vote aren't shareholders but computors. + +Both is implemented in the `ProposersAndVotersT` by using `ProposalAndVotingByComputors` or `ProposalByAnyoneVotingByComputors` instead of `ProposalAndVotingByShareholders`. + + +## Proposal types + +Each proposal type is composed of a class and a number of options. As an alternative to having N options (option votes), some proposal classes (currently the one to set a variable) may allow to vote with a scalar value in a range defined by the proposal (scalar voting). + +The proposal type classes are defined in `QPI::ProposalTypes::Class`. The following are available at the moment: + +```C++ + // Options without extra data. Supported options: 2 <= N <= 8 with ProposalDataV1. + static constexpr uint16 GeneralOptions = 0; + + // Propose to transfer amount to address. Supported options: 2 <= N <= 5 with ProposalDataV1. + static constexpr uint16 Transfer = 0x100; + + // Propose to set variable to a value. Supported options: 2 <= N <= 5 with ProposalDataV1; N == 0 means scalar voting. + static constexpr uint16 Variable = 0x200; + + // Propose to set multiple variables. Supported options: 2 <= N <= 8 with ProposalDataV1 + static constexpr uint16 MultiVariables = 0x300; + + // Propose to transfer amount to address in a specific epoch. Supported options: 1 with ProposalDataV1. + static constexpr uint16 TransferInEpoch = 0x400; +``` + +``QPI::ProposalType`` provides the following functions to work with proposal types: + +```C++ + // Construct type from class + number of options (no checking if type is valid) + uint16 type(uint16 cls, uint16 options); + + // Return option count for a given proposal type (including "no change" option), + // 0 for scalar voting (no checking if type is valid). + uint16 optionCount(uint16 proposalType); + + // Return class of proposal type (no checking if type is valid). + uint16 cls(uint16 proposalType); + + // Check if given type is valid (supported by most comprehensive ProposalData class). + bool isValid(uint16 proposalType); +``` + +For convenience, ``QPI::ProposalType`` also provides many proposal type constants, for example: + +```C++ + // Set given variable to proposed value with options yes/no + static constexpr uint16 VariableYesNo = Class::Variable | 2; + + // Set given variable to proposed value with two options of values and option "no change" + static constexpr uint16 VariableTwoValues = Class::Variable | 3; + + // Set given variable to proposed value with three options of values and option "no change" + static constexpr uint16 VariableThreeValues = Class::Variable | 4; + + // Set multiple variables with options yes/no (data stored by contract) -> result is histogram of options + static constexpr uint16 MultiVariablesYesNo = Class::MultiVariables | 2; + + // Options yes and no without extra data -> result is histogram of options + static constexpr uint16 YesNo = Class::GeneralOptions | 2; + + // Transfer given amount to address with options yes/no + static constexpr uint16 TransferYesNo = Class::Transfer | 2; +``` + +See `qpi.h` for more. diff --git a/doc/contributing.md b/doc/contributing.md index fbd1aa55d..761a61449 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -1,5 +1,16 @@ # How to Contribute +## Table of contents + +1. [Contributing as an external developer](#contributing-as-an-external-developer) +2. [Development workflow / branches](#development-workflow--branches) +3. [Coding guidelines](#coding-guidelines) + 1. [Most important principles](#most-important-principles) + 2. [General guidelines](#general-guidelines) + 3. [Style guidelines](#style-guidelines) +4. [Version naming scheme](#version-naming-scheme) +5. [Profiling](#profiling) + ## Contributing as an external developer If you find bugs, typos, or other problems that can be fixed with a few changes, you are more than welcome to contribute these fixes with a pull request as follows. @@ -191,6 +202,8 @@ The code formatting rules are enforced using `clang-format`, ideally setup as a They are based on the "Microsoft" style with some custom modifications. Currently, the style guidelines are designed to improve consistency while minimizing the number of changes needed in the existing codebase. +#### Naming + The following naming rules are not strictly enforced, but should be followed at least in new code: - **Preprocessor symbols** must be defined `ALL_CAPS`. @@ -221,6 +234,21 @@ The following naming rules are not strictly enforced, but should be followed at - **Functions** are named following the same pattern as variables. They usually start with a verb. Examples: `getComputerDigest()`, `processExchangePublicPeers()`, `initContractExec()`. +#### Curly Braces Style + +Every opening curly brace should be on a new line. This applies to conditional blocks, loops, functions, classes, structs, etc. For example: + +``` +if (cond) +{ + // do something +} +else +{ + // do something else +} +``` + ## Version naming scheme @@ -373,3 +401,5 @@ Even when bound by serializing instructions, the system environment at the time [AMD64 Architecture Programmer's Manual, Volumes 1-5, 13.2.4 Time-Stamp Counter](https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/40332.pdf) Another rich source: [Intel® 64 and IA-32 Architectures Software Developer's Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4](https://cdrdv2.intel.com/v1/dl/getContent/671200) + + diff --git a/doc/execution_fees.md b/doc/execution_fees.md new file mode 100644 index 000000000..65be51290 --- /dev/null +++ b/doc/execution_fees.md @@ -0,0 +1,73 @@ +# Contract Execution Fees + +## Overview + +Every smart contract in Qubic has an **execution fee reserve** that determines whether the contract can execute its procedures. This reserve is stored in Contract 0's state and is initially funded during the contract's IPO (Initial Public Offering). Contracts must maintain a positive execution fee reserve to remain operational. It is important to note that these execution fees are different from the fees a user pays to a contract upon calling a procedure. To avoid confusion we will call the fees a user pays to a contract 'invocation reward' throughout this document. + + + +## Fee Management + +Each contract's execution fee reserve is stored in Contract 0's state in an array `contractFeeReserves[MAX_NUMBER_OF_CONTRACTS]`. The current value of the executionFeeReserve can be queried with the function `qpi.queryFeeReserve(contractIndex)` and returns a `sint64`. + +When a contract's IPO completes, the execution fee reserve is initialized based on the IPO's final price. If the `finalPrice > 0`, the reserve is set to `finalPrice * NUMBER_OF_COMPUTORS` (676 computors). However, if the IPO fails and `finalPrice = 0`, the contract is marked as failed with `ContractErrorIPOFailed` and the reserve remains 0 and can't be filled anymore. A contract which failed the IPO will remain unusable. + +Contracts can refill their execution fee reserves in the following ways: + +- **Contract internal burning**: Any contract procedure can burn its own QU using `qpi.burn(amount)` to refill its own reserve, or `qpi.burn(amount, targetContractIndex)` to refill another contract's reserve. +- **External refill via QUtil**: Anyone can refill any contract's reserve by sending QU to the QUtil contract's `BurnQubicForContract` procedure with the target contract index. All sent QU is burned and added to the target contract's reserve. +- **Legacy QUtil burn**: QUtil provides a `BurnQubic` procedure that burns to QUtil's own reserve specifically. + +The execution fee system follows a key principle: **"The Contract Initiating Execution Pays"**. When a user initiates a transaction, the user's destination contract must have a positive executionFeeReserve. When a contract initiates an operation (including any callbacks it triggers), that contract must have positive executionFeeReserve. + +Currently, execution fees are checked (contracts must have `executionFeeReserve > 0`) but **not yet deducted** based on actual computation. Future implementation will measure execution time and resources per procedure call, deduct proportional fees from the reserve. + +## What Operations Require Execution Fees + +The execution fee system checks whether a contract has positive `executionFeeReserve` at different entry points. The table below summarizes when fees are checked and who pays: + +| Entry Point | Initiator | executionFeeReserve Checked | Code Location | +|------------|-----------|----------------------------|---------------| +| System procedures (`BEGIN_TICK`, `END_TICK`, etc.) | System | ✅ Contract must have positive reserve | qubic.cpp | +| User procedure call | User | ✅ Contract must have positive reserve | qubic.cpp | +| Contract-to-contract procedure | Contract A | ✅ Called contract (B) must have positive reserve, otherwise error is returned to caller | contract_exec.h | +| Contract-to-contract function | Contract A | ✅ Called contract (B) must have positive reserve, otherwise error is returned to caller | contract_exec.h | +| Contract-to-contract callback (`POST_INCOMING_TRANSFER`, etc.) | System | ❌ Not checked (callbacks execute regardless of reserve) | contract_exec.h | +| Epoch transistion system procedures (`BEGIN_EPOCH`, `END_EPOCH`) | System | ❌ Not checked | qubic.cpp | +| Revenue donation (`POST_INCOMING_TRANSFER`) | System | ❌ Not checked | qubic.cpp | +| IPO refund (`POST_INCOMING_TRANSFER`) | System | ❌ Not checked | ipo.h | +| User functions | User | ❌ Never checked (read-only) | N/A | + +**Basic system procedures** (`BEGIN_TICK`, `END_TICK`) require the contract to have `executionFeeReserve > 0`. If the reserve is depleted, these procedures are skipped and the contract becomes dormant. These procedures are invoked by the system directly. + +**Epoch transistion system procedures** `BEGIN_EPOCH`, `END_EPOCH` are executed even with a non-positive `executionFeeReserve` to keep contract state in a valid state. + +**User procedure calls** check the contract's execution fee reserve before execution. If `executionFeeReserve <= 0`, the transaction fails and any attached amount is refunded to the user. If the contract has fees, the procedure executes normally and may trigger `POST_INCOMING_TRANSFER` callback first if amount > 0. + +**User functions** (read-only queries) are always available regardless of executionFeeReserve. They are defined with `PUBLIC_FUNCTION()` or `PRIVATE_FUNCTION()` macros, provide read-only access to contract state, and cannot modify state or trigger procedures. + +**Contract-to-contract procedure calls** via `INVOKE_OTHER_CONTRACT_PROCEDURE` check that the **called contract (B) has positive executionFeeReserve**. If Contract B has insufficient fees (`executionFeeReserve <= 0`), the call fails and returns `CallErrorInsufficientFees` to Contract A. The procedure is not executed, and Contract A can check the error via the `interContractCallError` variable (or a custom error variable when using `INVOKE_OTHER_CONTRACT_PROCEDURE_E`). Contract developers should check the error after invoking procedures or proactively verify the called contract's fee reserve with `qpi.queryFeeReserve(contractIndex)` before invoking it. + +**Contract-to-contract function calls** via `CALL_OTHER_CONTRACT_FUNCTION` also check that the **called contract (B) has positive executionFeeReserve**. If Contract B has insufficient fees or is in an error state, the call fails and returns `CallErrorInsufficientFees` or `CallErrorContractInErrorState` to Contract A. The function is not executed, and Contract A can check the error via the `interContractCallError` variable (or a custom error variable when using `CALL_OTHER_CONTRACT_FUNCTION_E`). This graceful error handling allows Contract A to continue execution and handle failures appropriately. + +**Contract-to-contract callbacks** (`POST_INCOMING_TRANSFER`, `PRE_ACQUIRE_SHARES`, `POST_ACQUIRE_SHARES`, etc.) are system-initiated and **do not check executionFeeReserve**. These callbacks execute regardless of the called contract's fee reserve status, allowing contracts to receive system-initiated transfers and notifications even when dormant. This design ensures that contracts can receive revenue donations, IPO refunds, and other system transfers without requiring positive fee reserves. + +Example: Contract A (executionFeeReserve = 1000) transfers 500 QU to Contract B (executionFeeReserve = 0) using `qpi.transfer()`. The transfer succeeds and Contract B's `POST_INCOMING_TRANSFER` callback executes regardless of Contract B having no fees, because the callback is system-initiated. However, if Contract A tries to invoke a procedure of Contract B using `INVOKE_OTHER_CONTRACT_PROCEDURE`, the call will fail and return `CallErrorInsufficientFees`. Contract A should check `interContractCallError` after the call and handle the error gracefully (e.g., skip the operation or use fallback logic). + +**System-initiated transfers** (revenue donations and IPO refunds) do not require the recipient contract to have positive executionFeeReserve. The `POST_INCOMING_TRANSFER` callback executes regardless of the destination's reserve status. These are system-initiated transfers that contracts didn't request, so contracts should be able to receive system funds even if dormant. + +## Best Practices + +### For Contract Developers + +1. **Plan for sustainability**: Charge invocation rewards for running user procedures +2. **Burn collected invocation rewards**: Regularly call `qpi.burn()` to replenish executionFeeReserve +3. **Monitor reserve**: Implement a function to expose current reserve level +4. **Graceful degradation**: Consider what happens when reserve runs low +5. **Handle inter-contract call errors**: After using `INVOKE_OTHER_CONTRACT_PROCEDURE`, check the `interContractCallError` variable to verify the call succeeded. Handle errors gracefully (e.g., skip operations, use fallback logic). You can also proactively verify the called contract has positive `executionFeeReserve` using `qpi.queryFeeReserve(contractIndex) > 0` before calling. + +### For Contract Users + +1. **Check contract status**: Before using a contract, verify it has positive executionFeeReserve +2. **Transaction failures**: If your transaction fails due to insufficient execution fees reserve, the attached amount will be automatically refunded +3. **No funds lost**: The system ensures amounts are refunded if a contract cannot execute diff --git a/doc/protocol.md b/doc/protocol.md index 2b3bbf784..987ad6645 100644 --- a/doc/protocol.md +++ b/doc/protocol.md @@ -9,61 +9,9 @@ If you find such protocol violations in the code, feel free to [contribute](cont ## List of network message types -The network message types are defined in `src/network_messages/`. -This is its current list ordered by type. +The network message types are defined in a single `enum` in [src/network_messages/network_message_type.h](https://github.com/qubic/core/blob/main/src/network_messages/network_message_type.h). The type number is the identifier used in `RequestResponseHeader` (defined in `header.h`). - -- `ExchangePublicPeers`, type 0, defined in `public_peers.h`. -- `BroadcastMessage`, type 1, defined in `broadcast_message.h`. -- `BroadcastComputors`, type 2, defined in `computors.h`. -- `BroadcastTick`, type 3, defined in `tick.h`. -- `BroadcastFutureTickData`, type 8, defined in `tick.h`. -- `RequestComputors`, type 11, defined in `computors.h`. -- `RequestQuorumTick`, type 14, defined in `tick.h`. -- `RequestTickData`, type 16, defined in `tick.h`. -- `BROADCAST_TRANSACTION`, type 24, defined in `transactions.h`. -- `REQUEST_TRANSACTION_INFO`, type 26, defined in `transactions.h`. -- `REQUEST_CURRENT_TICK_INFO`, type 27, defined in `tick.h`. -- `RESPOND_CURRENT_TICK_INFO`, type 28, defined in `tick.h`. -- `REQUEST_TICK_TRANSACTIONS`, type 29, defined in `transactions.h`. -- `RequestedEntity`, type 31, defined in `entity.h`. -- `RespondedEntity`, type 32, defined in `entity.h`. -- `RequestContractIPO`, type 33, defined in `contract.h`. -- `RespondContractIPO`, type 34, defined in `contract.h`. -- `EndResponse`, type 35, defined in `common_response.h`. -- `RequestIssuedAssets`, type 36, defined in `assets.h`. -- `RespondIssuedAssets`, type 37, defined in `assets.h`. -- `RequestOwnedAssets`, type 38, defined in `assets.h`. -- `RespondOwnedAssets`, type 39, defined in `assets.h`. -- `RequestPossessedAssets`, type 40, defined in `assets.h`. -- `RespondPossessedAssets`, type 41, defined in `assets.h`. -- `RequestContractFunction`, type 42, defined in `contract.h`. -- `RespondContractFunction`, type 43, defined in `contract.h`. -- `RequestLog`, type 44, defined in `logging.h`. -- `RespondLog`, type 45, defined in `logging.h`. -- `REQUEST_SYSTEM_INFO`, type 46, defined in `system_info.h`. -- `RespondSystemInfo`, type 47, defined in `system_info.h`. -- `RequestLogIdRangeFromTx`, type 48, defined in `logging.h`. -- `ResponseLogIdRangeFromTx`, type 49, defined in `logging.h`. -- `RequestAllLogIdRangesFromTick`, type 50, defined in `logging.h`. -- `ResponseAllLogIdRangesFromTick`, type 51, defined in `logging.h`. -- `RequestAssets`, type 52, defined in `assets.h`. -- `RespondAssets` and `RespondAssetsWithSiblings`, type 53, defined in `assets.h`. -- `TryAgain`, type 54, defined in `common_response.h`. -- `RequestPruningLog`, type 56, defined in `logging.h`. -- `ResponsePruningLog`, type 57, defined in `logging.h`. -- `RequestLogStateDigest`, type 58, defined in `logging.h`. -- `ResponseLogStateDigest`, type 59, defined in `logging.h`. -- `RequestedCustomMiningData`, type 60, defined in `custom_mining.h`. -- `RespondCustomMiningData`, type 61, defined in `custom_mining.h`. -- `RequestedCustomMiningSolutionVerification`, type 62, defined in `custom_mining.h`. -- `RespondCustomMiningSolutionVerification`, type 63, defined in `custom_mining.h`. -- `SpecialCommand`, type 255, defined in `special_command.h`. - -Addon messages (supported if addon is enabled): -- `REQUEST_TX_STATUS`, type 201, defined in `src/addons/tx_status_request.h`. -- `RESPOND_TX_STATUS`, type 202, defined in `src/addons/tx_status_request.h`. - +The type number is usually available from the network message type via the `static constexpr unsigned char type()` method. ## Peer Sharing @@ -123,3 +71,7 @@ The message is processed as follows, depending on the message type: ## ... + + + + diff --git a/doc/stable_computor_index_diagram.svg b/doc/stable_computor_index_diagram.svg new file mode 100644 index 000000000..5cffdd46c --- /dev/null +++ b/doc/stable_computor_index_diagram.svg @@ -0,0 +1,233 @@ + + + + + + + + + Stable Computor Index Algorithm + + + Memory Layout in tempBuffer: + + + + + + + tempComputorList[676] + (m256i array: 676 × 32 = 21,632 bytes) + + + + isIndexTaken[676] + (bool array: 676 bytes) + + + + isFutureComputorUsed[676] + (bool array: 676 bytes) + + + m256i* tempComputorList = (m256i*)tempBuffer; + bool* isIndexTaken = (bool*)(tempComputorList + NUMBER_OF_COMPUTORS); // advances by 676 × sizeof(m256i) bytes + bool* isFutureComputorUsed = isIndexTaken + NUMBER_OF_COMPUTORS; // advances by 676 bytes + + + Example (simplified to 6 computors): + + + Current Computors (epoch N): + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + ID_D + + + idx 4 + ID_E + + + idx 5 + ID_F + + + + Future Computors BEFORE (sorted by mining score): + + + idx 0 + ID_C + + + idx 1 + ID_X + + + idx 2 + ID_A + + + idx 3 + ID_Y + + + idx 4 + ID_E + + + idx 5 + ID_B + + ← ID_C moved from idx 2→0, ID_A from idx 0→2, etc. + Problem: Requalifying IDs have different indices! + + + Step 1: Find requalifying computors, assign to their ORIGINAL index + + + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + empty + + + idx 4 + ID_E + + + idx 5 + empty + + + + + isIndexTaken: + + T + + T + + T + + F + + T + + F + + isFutureUsed: + + T + + F + + T + + F + + T + + T + ← ID_X, ID_Y unused + + + + Step 2: Fill empty slots with new computors (ID_X, ID_Y) + + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + ID_X + + + idx 4 + ID_E + + + idx 5 + ID_Y + + ← New computors fill vacated slots (D→X, F→Y) + + + Result: Future Computors AFTER (stable indices): + + + idx 0 + ID_A + + + idx 1 + ID_B + + + idx 2 + ID_C + + + idx 3 + ID_X + + + idx 4 + ID_E + + + idx 5 + ID_Y + + + + + + Requalifying (kept same index) + + + New computor (fills vacated slot) + + + + + Key Benefit for Execution Fee Reporting: + ID_A reports at ticks where tick % 676 == 0 in BOTH epochs + ID_B reports at ticks where tick % 676 == 1 in BOTH epochs → No gaps, no duplicates! + + diff --git a/lib/platform_common/platform_common.vcxproj b/lib/platform_common/platform_common.vcxproj index 62a021ce3..92709ab2e 100644 --- a/lib/platform_common/platform_common.vcxproj +++ b/lib/platform_common/platform_common.vcxproj @@ -20,6 +20,7 @@ + diff --git a/lib/platform_common/platform_common.vcxproj.filters b/lib/platform_common/platform_common.vcxproj.filters index 4b33271e3..b032333ca 100644 --- a/lib/platform_common/platform_common.vcxproj.filters +++ b/lib/platform_common/platform_common.vcxproj.filters @@ -19,6 +19,8 @@ + + diff --git a/lib/platform_common/sorting.h b/lib/platform_common/sorting.h new file mode 100644 index 000000000..56556f0dc --- /dev/null +++ b/lib/platform_common/sorting.h @@ -0,0 +1,57 @@ +#pragma once + +enum class SortingOrder +{ + SortAscending, + SortDescending, +}; + +// Lomuto's partition scheme for quick sort: +// Uses the last element in the range as pivot. Swaps elements until all elements that should go before the pivot +// (according to the sorting order) are on the left side of the pivot and all others are on the right side of the pivot. +// Returns the index of the pivot in the range after partitioning. +template +unsigned int partition(T* range, int first, int last, SortingOrder order) +{ + constexpr auto swap = [](T& a, T& b) { T tmp = b; b = a; a = tmp; }; + + T pivot = range[last]; + + // Next available index to swap to. Elements with indices < nextIndex are certain to go before the pivot. + int nextIndex = first; + for (int i = first; i < last; ++i) + { + bool shouldGoBefore = range[i] < pivot; // SortAscending + if (order == SortingOrder::SortDescending) + shouldGoBefore = !shouldGoBefore; + + if (shouldGoBefore) + { + swap(range[nextIndex], range[i]); + ++nextIndex; + } + } + + // move pivot after all elements that should go before the pivot + swap(range[nextIndex], range[last]); + + return nextIndex; +} + +// Sorts the elements from range[first] to range[last] according to the given `order`. +// The sorting happens in place and requires type T to have the comparison operator < defined. +template +void quickSort(T* range, int first, int last, SortingOrder order) +{ + if (first >= last) + return; + + // pivot is the partitioning index, range[pivot] is in correct position + unsigned int pivot = partition(range, first, last, order); + + // recursively sort smaller ranges to the left and right of the pivot + quickSort(range, first, pivot - 1, order); + quickSort(range, pivot + 1, last, order); + + return; +} diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 9dd7a3aa6..78634bba1 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -23,7 +23,10 @@ + + + @@ -39,6 +42,9 @@ + + + @@ -46,10 +52,13 @@ + + + @@ -64,6 +73,7 @@ + @@ -72,6 +82,7 @@ + @@ -93,6 +104,7 @@ + @@ -115,6 +127,9 @@ + + + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 3d0f9ab18..2c0da5fb5 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -123,6 +123,15 @@ contracts + + contracts + + + contracts + + + contracts + contract_core @@ -265,11 +274,47 @@ network_messages + + network_messages + platform + + platform + + + contract_core + + + ticking + + + ticking + + + ticking + + + contracts + + + contracts + + + contract_core + + + network_messages + + + contract_core + + + contracts + @@ -317,4 +362,4 @@ platform - + \ No newline at end of file diff --git a/src/addons/tx_status_request.h b/src/addons/tx_status_request.h index 9b51503bc..db8187f68 100644 --- a/src/addons/tx_status_request.h +++ b/src/addons/tx_status_request.h @@ -9,6 +9,7 @@ #include "../platform/memory_util.h" #include "../network_messages/header.h" +#include "../network_messages/network_message_type.h" #include "../public_settings.h" #include "../system.h" @@ -39,20 +40,27 @@ static struct } txStatusData; -#define REQUEST_TX_STATUS 201 - struct RequestTxStatus { unsigned int tick; + + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_TX_STATUS; + } }; static_assert(sizeof(RequestTxStatus) == 4, "unexpected size"); -#define RESPOND_TX_STATUS 202 #pragma pack(push, 1) struct RespondTxStatus { + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_TX_STATUS; + } + unsigned int currentTickOfNode; unsigned int tick; unsigned int txCount; @@ -245,7 +253,7 @@ static void processRequestConfirmedTx(long long processorNumber, Peer *peer, Req } ASSERT(tickTxStatus.size() <= sizeof(tickTxStatus)); - enqueueResponse(peer, tickTxStatus.size(), RESPOND_TX_STATUS, header->dejavu(), &tickTxStatus); + enqueueResponse(peer, tickTxStatus.size(), RespondTxStatus::type(), header->dejavu(), &tickTxStatus); } #if TICK_STORAGE_AUTOSAVE_MODE diff --git a/src/assets/assets.h b/src/assets/assets.h index d0fc19589..2bee80603 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -112,7 +112,7 @@ struct AssetStorage PROFILE_SCOPE(); reset(); - for (int index = 0; index < ASSETS_CAPACITY; index++) + for (int index = ASSETS_CAPACITY - 1; index >= 0; index--) { switch (assets[index].varStruct.issuance.type) { @@ -245,6 +245,7 @@ static long long issueAsset(const m256i& issuerPublicKey, const char name[7], ch AssetIssuance assetIssuance; assetIssuance.issuerPublicKey = issuerPublicKey; assetIssuance.numberOfShares = numberOfShares; + assetIssuance.managingContractIndex = managingContractIndex; // any SC can call issueAsset now (eg: QBOND) not just QX *((unsigned long long*) assetIssuance.name) = *((unsigned long long*) name); // Order must be preserved! assetIssuance.numberOfDecimalPlaces = numberOfDecimalPlaces; // Order must be preserved! *((unsigned long long*) assetIssuance.unitOfMeasurement) = *((unsigned long long*) unitOfMeasurement); // Order must be preserved! @@ -417,8 +418,8 @@ static bool transferShareManagementRights(int sourceOwnershipIndex, int sourcePo logPM.possessionPublicKey = possessionPublicKey; logPM.ownershipPublicKey = ownershipPublicKey; logPM.issuerPublicKey = assets[issuanceIndex].varStruct.issuance.publicKey; - logOM.sourceContractIndex = assets[sourcePossessionIndex].varStruct.ownership.managingContractIndex; - logOM.destinationContractIndex = destinationPossessionManagingContractIndex; + logPM.sourceContractIndex = assets[sourcePossessionIndex].varStruct.ownership.managingContractIndex; + logPM.destinationContractIndex = destinationPossessionManagingContractIndex; logPM.numberOfShares = numberOfShares; *((unsigned long long*) & logPM.assetName) = *((unsigned long long*) & assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.name); // possible with 7 byte array, because it is followed by memory reserved for terminator byte logger.logAssetPossessionManagingContractChange(logPM); @@ -515,6 +516,7 @@ static bool transferShareOwnershipAndPossession(int sourceOwnershipIndex, int so assetOwnershipChange.destinationPublicKey = destinationPublicKey; assetOwnershipChange.issuerPublicKey = issuance.publicKey; assetOwnershipChange.numberOfShares = numberOfShares; + assetOwnershipChange.managingContractIndex = assets[sourceOwnershipIndex].varStruct.ownership.managingContractIndex; *((unsigned long long*) & assetOwnershipChange.name) = *((unsigned long long*) & issuance.name); // Order must be preserved! assetOwnershipChange.numberOfDecimalPlaces = issuance.numberOfDecimalPlaces; // Order must be preserved! *((unsigned long long*) & assetOwnershipChange.unitOfMeasurement) = *((unsigned long long*) & issuance.unitOfMeasurement); // Order must be preserved! @@ -525,6 +527,7 @@ static bool transferShareOwnershipAndPossession(int sourceOwnershipIndex, int so assetPossessionChange.destinationPublicKey = destinationPublicKey; assetPossessionChange.issuerPublicKey = issuance.publicKey; assetPossessionChange.numberOfShares = numberOfShares; + assetPossessionChange.managingContractIndex = assets[sourcePossessionIndex].varStruct.possession.managingContractIndex; *((unsigned long long*) & assetPossessionChange.name) = *((unsigned long long*) & issuance.name); // Order must be preserved! assetPossessionChange.numberOfDecimalPlaces = issuance.numberOfDecimalPlaces; // Order must be preserved! *((unsigned long long*) & assetPossessionChange.unitOfMeasurement) = *((unsigned long long*) & issuance.unitOfMeasurement); // Order must be preserved! @@ -593,6 +596,7 @@ static bool transferShareOwnershipAndPossession(int sourceOwnershipIndex, int so assetOwnershipChange.destinationPublicKey = destinationPublicKey; assetOwnershipChange.issuerPublicKey = assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.publicKey; assetOwnershipChange.numberOfShares = numberOfShares; + assetOwnershipChange.managingContractIndex = assets[sourceOwnershipIndex].varStruct.ownership.managingContractIndex; *((unsigned long long*) & assetOwnershipChange.name) = *((unsigned long long*) & assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.name); // Order must be preserved! assetOwnershipChange.numberOfDecimalPlaces = assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.numberOfDecimalPlaces; // Order must be preserved! *((unsigned long long*) & assetOwnershipChange.unitOfMeasurement) = *((unsigned long long*) & assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.unitOfMeasurement); // Order must be preserved! @@ -603,6 +607,7 @@ static bool transferShareOwnershipAndPossession(int sourceOwnershipIndex, int so assetPossessionChange.destinationPublicKey = destinationPublicKey; assetPossessionChange.issuerPublicKey = assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.publicKey; assetPossessionChange.numberOfShares = numberOfShares; + assetPossessionChange.managingContractIndex = assets[sourcePossessionIndex].varStruct.possession.managingContractIndex; *((unsigned long long*) & assetPossessionChange.name) = *((unsigned long long*) & assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.name); // Order must be preserved! assetPossessionChange.numberOfDecimalPlaces = assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.numberOfDecimalPlaces; // Order must be preserved! *((unsigned long long*) & assetPossessionChange.unitOfMeasurement) = *((unsigned long long*) & assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.unitOfMeasurement); // Order must be preserved! diff --git a/src/assets/net_msg_impl.h b/src/assets/net_msg_impl.h index fda300c52..a8afafb40 100644 --- a/src/assets/net_msg_impl.h +++ b/src/assets/net_msg_impl.h @@ -18,7 +18,7 @@ static void processRequestIssuedAssets(Peer* peer, RequestResponseHeader* header if (universeIndex >= ASSETS_CAPACITY || assets[universeIndex].varStruct.issuance.type == EMPTY) { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } else { @@ -30,7 +30,7 @@ static void processRequestIssuedAssets(Peer* peer, RequestResponseHeader* header response.universeIndex = universeIndex; getSiblings(response.universeIndex, assetDigests, response.siblings); - enqueueResponse(peer, sizeof(response), RespondIssuedAssets::type, header->dejavu(), &response); + enqueueResponse(peer, sizeof(response), RespondIssuedAssets::type(), header->dejavu(), &response); } universeIndex = (universeIndex + 1) & (ASSETS_CAPACITY - 1); @@ -55,7 +55,7 @@ static void processRequestOwnedAssets(Peer* peer, RequestResponseHeader* header) if (universeIndex >= ASSETS_CAPACITY || assets[universeIndex].varStruct.issuance.type == EMPTY) { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } else { @@ -68,7 +68,7 @@ static void processRequestOwnedAssets(Peer* peer, RequestResponseHeader* header) response.universeIndex = universeIndex; getSiblings(response.universeIndex, assetDigests, response.siblings); - enqueueResponse(peer, sizeof(response), RespondOwnedAssets::type, header->dejavu(), &response); + enqueueResponse(peer, sizeof(response), RespondOwnedAssets::type(), header->dejavu(), &response); } universeIndex = (universeIndex + 1) & (ASSETS_CAPACITY - 1); @@ -93,7 +93,7 @@ static void processRequestPossessedAssets(Peer* peer, RequestResponseHeader* hea if (universeIndex >= ASSETS_CAPACITY || assets[universeIndex].varStruct.issuance.type == EMPTY) { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } else { @@ -107,7 +107,7 @@ static void processRequestPossessedAssets(Peer* peer, RequestResponseHeader* hea response.universeIndex = universeIndex; getSiblings(response.universeIndex, assetDigests, response.siblings); - enqueueResponse(peer, sizeof(response), RespondPossessedAssets::type, header->dejavu(), &response); + enqueueResponse(peer, sizeof(response), RespondPossessedAssets::type(), header->dejavu(), &response); } universeIndex = (universeIndex + 1) & (ASSETS_CAPACITY - 1); @@ -149,7 +149,7 @@ static void processRequestAssets(Peer* peer, RequestResponseHeader* header) RespondAssetsWithSiblings payload; } response; setMemory(response, 0); - response.header.setType(RespondAssets::type); + response.header.setType(RespondAssets::type()); response.header.setDejavu(header->dejavu()); // size of output message depends on whether sibilings are requested @@ -267,5 +267,5 @@ static void processRequestAssets(Peer* peer, RequestResponseHeader* header) break; } - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), nullptr); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), nullptr); } diff --git a/src/common_buffers.h b/src/common_buffers.h index 7bace2777..18dcd4478 100644 --- a/src/common_buffers.h +++ b/src/common_buffers.h @@ -2,6 +2,7 @@ #include "platform/global_var.h" #include "platform/memory_util.h" +#include "platform/assert.h" #include "network_messages/entity.h" #include "network_messages/assets.h" @@ -35,7 +36,10 @@ static void deinitCommonBuffers() } } -static void* __scratchpad() +static void* __scratchpad(unsigned long long sizeToMemsetZero) { + ASSERT(sizeToMemsetZero <= reorgBufferSize); + if (sizeToMemsetZero) + setMem(reorgBuffer, sizeToMemsetZero, 0); return reorgBuffer; } diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 9d4c41d94..d588b3131 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -1,7 +1,5 @@ #pragma once -#include "platform/m256.h" - ////////// Smart contracts \\\\\\\\\\ // The order in this file is very important, because it restricts what is available to the contracts. @@ -10,61 +8,10 @@ // Additionally, most types, functions, and variables of the core have to be defined after including // the contract to keep them unavailable in the contract code. -namespace QPI -{ - struct QpiContextProcedureCall; - struct QpiContextFunctionCall; -} - -// TODO: add option for having locals to SYSTEM and EXPAND procedures -typedef void (*SYSTEM_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); -typedef void (*EXPAND_PROCEDURE)(const QPI::QpiContextFunctionCall&, void*, void*); // cannot not change anything except state -typedef void (*USER_FUNCTION)(const QPI::QpiContextFunctionCall&, void* state, void* input, void* output, void* locals); -typedef void (*USER_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); - -constexpr unsigned long long MAX_CONTRACT_STATE_SIZE = 1073741824; - -// Maximum size of local variables that may be used by a contract function or procedure -// If increased, the size of contractLocalsStack should be increased as well. -constexpr unsigned int MAX_SIZE_OF_CONTRACT_LOCALS = 32 * 1024; - -// TODO: make sure the limit of nested calls is not violated -constexpr unsigned short MAX_NESTED_CONTRACT_CALLS = 10; - -// Size of the contract action tracker, limits the number of transfers that one contract call can execute. -constexpr unsigned long long CONTRACT_ACTION_TRACKER_SIZE = 16 * 1024 * 1024; - - -static void __beginFunctionOrProcedure(const unsigned int); // TODO: more human-readable form of function ID? -static void __endFunctionOrProcedure(const unsigned int); -template static m256i __K12(T); -template static void __logContractDebugMessage(unsigned int, T&); -template static void __logContractErrorMessage(unsigned int, T&); -template static void __logContractInfoMessage(unsigned int, T&); -template static void __logContractWarningMessage(unsigned int, T&); -static void* __scratchpad(); // TODO: concurrency support (n buffers for n allowed concurrent contract executions) -// static void* __tryAcquireScratchpad(unsigned int size); // Thread-safe, may return nullptr if no appropriate buffer is available -// static void __ReleaseScratchpad(void*); - -template -struct __FunctionOrProcedureBeginEndGuard -{ - // Constructor calling __beginFunctionOrProcedure() - __FunctionOrProcedureBeginEndGuard() - { - __beginFunctionOrProcedure(functionOrProcedureId); - } - - // Destructor making sure __endFunctionOrProcedure() is called for every return path - ~__FunctionOrProcedureBeginEndGuard() - { - __endFunctionOrProcedure(functionOrProcedureId); - } -}; - // With no other includes before, the following are the only headers available to contracts. // When adding something, be cautious to keep access of contracts limited to safe features only. +#include "pre_qpi_def.h" #include "contracts/qpi.h" #include "qpi_proposal_voting.h" @@ -194,8 +141,6 @@ struct __FunctionOrProcedureBeginEndGuard #define CONTRACT_STATE2_TYPE QSWAP2 #include "contracts/Qswap.h" -#ifndef NO_NOST - #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE @@ -206,11 +151,86 @@ struct __FunctionOrProcedureBeginEndGuard #define CONTRACT_STATE2_TYPE NOST2 #include "contracts/Nostromo.h" +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QDRAW_CONTRACT_INDEX 15 +#define CONTRACT_INDEX QDRAW_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QDRAW +#define CONTRACT_STATE2_TYPE QDRAW2 +#include "contracts/Qdraw.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define RL_CONTRACT_INDEX 16 +#define CONTRACT_INDEX RL_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE RL +#define CONTRACT_STATE2_TYPE RL2 +#include "contracts/RandomLottery.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QBOND_CONTRACT_INDEX 17 +#define CONTRACT_INDEX QBOND_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QBOND +#define CONTRACT_STATE2_TYPE QBOND2 +#include "contracts/QBond.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QIP_CONTRACT_INDEX 18 +#define CONTRACT_INDEX QIP_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QIP +#define CONTRACT_STATE2_TYPE QIP2 +#include "contracts/QIP.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QRAFFLE_CONTRACT_INDEX 19 +#define CONTRACT_INDEX QRAFFLE_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QRAFFLE +#define CONTRACT_STATE2_TYPE QRAFFLE2 +#include "contracts/QRaffle.h" + +#ifndef NO_QRWA + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QRWA_CONTRACT_INDEX 20 +#define CONTRACT_INDEX QRWA_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QRWA +#define CONTRACT_STATE2_TYPE QRWA2 +#include "contracts/qRWA.h" + #endif +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define VOTTUNBRIDGE_CONTRACT_INDEX 21 +#define CONTRACT_INDEX VOTTUNBRIDGE_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE VOTTUNBRIDGE +#define CONTRACT_STATE2_TYPE VOTTUNBRIDGE2 +#include "contracts/VottunBridge.h" + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES +// forward declaration, defined in qpi_spectrum_impl.h +static void setContractFeeReserve(unsigned int contractIndex, long long newValue); + constexpr unsigned short TESTEXA_CONTRACT_INDEX = (CONTRACT_INDEX + 1); #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE @@ -257,6 +277,8 @@ constexpr unsigned short TESTEXD_CONTRACT_INDEX = (CONTRACT_INDEX + 1); #undef POST_RELEASE_SHARES #undef POST_ACQUIRE_SHARES #undef POST_INCOMING_TRANSFER +#undef SET_SHAREHOLDER_PROPOSAL +#undef SET_SHAREHOLDER_VOTES // The following are included after the contracts to keep their definitions and dependencies @@ -304,13 +326,20 @@ constexpr struct ContractDescription {"MSVAULT", 149, 10000, sizeof(MSVAULT)}, // proposal in epoch 147, IPO in 148, construction and first use in 149 {"QBAY", 154, 10000, sizeof(QBAY)}, // proposal in epoch 152, IPO in 153, construction and first use in 154 {"QSWAP", 171, 10000, sizeof(QSWAP)}, // proposal in epoch 169, IPO in 170, construction and first use in 171 -#ifndef NO_NOST {"NOST", 172, 10000, sizeof(NOST)}, // proposal in epoch 170, IPO in 171, construction and first use in 172 + {"QDRAW", 179, 10000, sizeof(QDRAW)}, // proposal in epoch 177, IPO in 178, construction and first use in 179 + {"RL", 182, 10000, sizeof(RL)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 + {"QBOND", 182, 10000, sizeof(QBOND)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 + {"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189 + {"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192 +#ifndef NO_QRWA + {"QRWA", 197, 10000, sizeof(QRWA)}, // proposal in epoch 195, IPO in 196, construction and first use in 197 #endif + {"VOTTUN", 199, 10000, sizeof(VOTTUNBRIDGE)}, // proposal in epoch 197, IPO in 200, construction and first use in 197 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES - {"TESTEXA", 138, 10000, sizeof(IPO)}, - {"TESTEXB", 138, 10000, sizeof(IPO)}, + {"TESTEXA", 138, 10000, sizeof(TESTEXA)}, + {"TESTEXB", 138, 10000, sizeof(TESTEXB)}, {"TESTEXC", 138, 10000, sizeof(IPO)}, {"TESTEXD", 155, 10000, sizeof(IPO)}, #endif @@ -350,6 +379,8 @@ enum SystemProcedureID POST_RELEASE_SHARES, POST_ACQUIRE_SHARES, POST_INCOMING_TRANSFER, + SET_SHAREHOLDER_PROPOSAL, + SET_SHAREHOLDER_VOTES, contractSystemProcedureCount, }; @@ -358,7 +389,8 @@ enum OtherEntryPointIDs // Used together with SystemProcedureID values, so there must be no overlap! USER_PROCEDURE_CALL = contractSystemProcedureCount + 1, USER_FUNCTION_CALL = contractSystemProcedureCount + 2, - REGISTER_USER_FUNCTIONS_AND_PROCEDURES_CALL = contractSystemProcedureCount + 3 + REGISTER_USER_FUNCTIONS_AND_PROCEDURES_CALL = contractSystemProcedureCount + 3, + USER_PROCEDURE_NOTIFICATION_CALL = contractSystemProcedureCount + 4, }; GLOBAL_VAR_DECL SYSTEM_PROCEDURE contractSystemProcedures[contractCount][contractSystemProcedureCount]; @@ -387,6 +419,10 @@ if (!contractName::__postReleaseSharesEmpty) contractSystemProcedures[contractIn contractSystemProcedureLocalsSizes[contractIndex][POST_RELEASE_SHARES] = contractName::__postReleaseSharesLocalsSize; \ if (!contractName::__postIncomingTransferEmpty) contractSystemProcedures[contractIndex][POST_INCOMING_TRANSFER] = (SYSTEM_PROCEDURE)contractName::__postIncomingTransfer;\ contractSystemProcedureLocalsSizes[contractIndex][POST_INCOMING_TRANSFER] = contractName::__postIncomingTransferLocalsSize; \ +if (!contractName::__setShareholderProposalEmpty) contractSystemProcedures[contractIndex][SET_SHAREHOLDER_PROPOSAL] = (SYSTEM_PROCEDURE)contractName::__setShareholderProposal;\ +contractSystemProcedureLocalsSizes[contractIndex][SET_SHAREHOLDER_PROPOSAL] = contractName::__setShareholderProposalLocalsSize; \ +if (!contractName::__setShareholderVotesEmpty) contractSystemProcedures[contractIndex][SET_SHAREHOLDER_VOTES] = (SYSTEM_PROCEDURE)contractName::__setShareholderVotes;\ +contractSystemProcedureLocalsSizes[contractIndex][SET_SHAREHOLDER_VOTES] = contractName::__setShareholderVotesLocalsSize; \ if (!contractName::__expandEmpty) contractExpandProcedures[contractIndex] = (EXPAND_PROCEDURE)contractName::__expand;\ QpiContextForInit qpi(contractIndex); \ contractName::__registerUserFunctionsAndProcedures(qpi); \ @@ -409,8 +445,15 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(MSVAULT); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBAY); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QSWAP); -#ifndef NO_NOST REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(NOST); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDRAW); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(RL); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBOND); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); +#ifndef NO_QRWA + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRWA); #endif // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -418,5 +461,12 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXB); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXC); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXD); + + // fill execution fee reserves for test contracts + setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 10000); + setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 10000); + setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 10000); + setContractFeeReserve(TESTEXD_CONTRACT_INDEX, 10000); #endif } + diff --git a/src/contract_core/contract_exec.h b/src/contract_core/contract_exec.h index 74f96accb..5d168e6ba 100644 --- a/src/contract_core/contract_exec.h +++ b/src/contract_core/contract_exec.h @@ -9,9 +9,12 @@ #include "platform/debugging.h" #include "platform/memory.h" +#include "assets/assets.h" + #include "contract_core/contract_def.h" #include "contract_core/stack_buffer.h" #include "contract_core/contract_action_tracker.h" +#include "contract_core/execution_time_accumulator.h" #include "logging/logging.h" #include "common_buffers.h" @@ -32,6 +35,7 @@ enum ContractError ContractErrorTooManyActions, ContractErrorTimeout, ContractErrorStoppedToResolveDeadlock, // only returned by function call, not set to contractError + ContractErrorIPOFailed, // IPO failed i.e. final price was 0. This contract is not constructed. }; // Used to store: locals and for first invocation level also input and output @@ -51,7 +55,10 @@ GLOBAL_VAR_DECL ContractExecErrorData contractExecutionErrorData[contractCount]; GLOBAL_VAR_DECL ReadWriteLock contractStateLock[contractCount]; GLOBAL_VAR_DECL unsigned char* contractStates[contractCount]; -GLOBAL_VAR_DECL volatile long long contractTotalExecutionTicks[contractCount]; + +// Total contract execution time (as CPU clock cycles) accumulated over the whole runtime of the node (reset on restart, includes contract functions). +GLOBAL_VAR_DECL volatile long long contractTotalExecutionTime[contractCount]; +GLOBAL_VAR_DECL ExecutionTimeAccumulator executionTimeAccumulator; // Contract error state, persistent and only set on error of procedure (TODO: only execute procedures if NoContractError) GLOBAL_VAR_DECL unsigned int contractError[contractCount]; @@ -60,6 +67,8 @@ GLOBAL_VAR_DECL unsigned int contractError[contractCount]; // access to contractStateChangeFlags thread-safe GLOBAL_VAR_DECL unsigned long long* contractStateChangeFlags GLOBAL_VAR_INIT(nullptr); +// Forward declaration for getContractFeeReserve (defined in qpi_spectrum_impl.h) +static long long getContractFeeReserve(unsigned int contractIndex); // Contract system procedures that serve as callbacks, such as PRE_ACQUIRE_SHARES, // break the rule that contracts can only call other contracts with lower index. @@ -72,6 +81,7 @@ enum ContractCallbacksRunningFlags NoContractCallback = 0, ContractCallbackManagementRightsTransfer = 1, ContractCallbackPostIncomingTransfer = 2, + ContractCallbackShareholderProposalAndVoting = 4, }; @@ -161,7 +171,9 @@ static bool initContractExec() contractLocalsStackLockWaitingCount = 0; contractLocalsStackLockWaitingCountMax = 0; - setMem((void*)contractTotalExecutionTicks, sizeof(contractTotalExecutionTicks), 0); + setMem((void*)contractTotalExecutionTime, sizeof(contractTotalExecutionTime), 0); + executionTimeAccumulator.init(); + setMem((void*)contractError, sizeof(contractError), 0); setMem((void*)contractExecutionErrorData, sizeof(contractExecutionErrorData), 0); for (int i = 0; i < contractCount; ++i) @@ -183,6 +195,22 @@ static bool initContractExec() return true; } +static void initializeContractErrors() +{ + unsigned int endIndex = contractCount; +#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES + endIndex = TESTEXA_CONTRACT_INDEX; +#endif + // At initialization, all contract errors are set to 0 (= no error). + // If IPO failed (number of contract shares in universe != NUMBER_OF_COMPUTERS), the error status needs to be set accordingly. + for (unsigned int contractIndex = 1; contractIndex < endIndex; ++contractIndex) + { + long long numShares = numberOfShares({ m256i::zero(), *(uint64*)contractDescriptions[contractIndex].assetName }); + if (numShares != NUMBER_OF_COMPUTORS) + contractError[contractIndex] = ContractErrorIPOFailed; + } +} + static void deinitContractExec() { if (contractStateChangeFlags) @@ -284,10 +312,18 @@ void QPI::QpiContextFunctionCall::__qpiFreeLocals() const } // Called before one contract calls a function of a different contract -const QpiContextFunctionCall& QPI::QpiContextFunctionCall::__qpiConstructContextOtherContractFunctionCall(unsigned int otherContractIndex) const +const QpiContextFunctionCall* QPI::QpiContextFunctionCall::__qpiConstructContextOtherContractFunctionCall(unsigned int otherContractIndex, InterContractCallError& callError) const { ASSERT(otherContractIndex < _currentContractIndex); ASSERT(_stackIndex >= 0 && _stackIndex < NUMBER_OF_CONTRACT_EXECUTION_BUFFERS); + + // Check if called contract is in an error state + if (contractError[otherContractIndex] != NoContractError) + { + callError = CallErrorContractInErrorState; + return nullptr; + } + char * buffer = contractLocalsStack[_stackIndex].allocate(sizeof(QpiContextFunctionCall)); if (!buffer) { @@ -304,16 +340,18 @@ const QpiContextFunctionCall& QPI::QpiContextFunctionCall::__qpiConstructContext appendNumber(dbgMsgBuf, _stackIndex, FALSE); addDebugMessage(dbgMsgBuf); #endif - // abort execution of contract here - __qpiAbort(ContractErrorAllocContextOtherFunctionCallFailed); + callError = CallErrorAllocationFailed; + return nullptr; } - QpiContextFunctionCall& newContext = *reinterpret_cast(buffer); - newContext.init(otherContractIndex, _originator, _currentContractId, _invocationReward, _entryPoint, _stackIndex); + + callError = NoCallError; + QpiContextFunctionCall* newContext = reinterpret_cast(buffer); + newContext->init(otherContractIndex, _originator, _currentContractId, _invocationReward, _entryPoint, _stackIndex); return newContext; } // Called before a contract runs a user procedure of another contract or a system procedure -const QpiContextProcedureCall& QPI::QpiContextProcedureCall::__qpiConstructProcedureCallContext(unsigned int procContractIndex, QPI::sint64 invocationReward) const +const QpiContextProcedureCall* QPI::QpiContextProcedureCall::__qpiConstructProcedureCallContext(unsigned int procContractIndex, QPI::sint64 invocationReward, InterContractCallError& callError, bool skipFeeCheck) const { ASSERT(_entryPoint != USER_FUNCTION_CALL); ASSERT(_stackIndex >= 0 && _stackIndex < NUMBER_OF_CONTRACT_EXECUTION_BUFFERS); @@ -321,6 +359,20 @@ const QpiContextProcedureCall& QPI::QpiContextProcedureCall::__qpiConstructProce // A contract can only run a procedure of a contract with a lower index, exceptions are callback system procedures ASSERT(procContractIndex < _currentContractIndex || contractCallbacksRunning != NoContractCallback); + // Check if called contract is in an error state + if (contractError[procContractIndex] != NoContractError) + { + callError = CallErrorContractInErrorState; + return nullptr; + } + + // Check if called contract has sufficient execution fee reserve (can be skipped for system callbacks) + if (!skipFeeCheck && getContractFeeReserve(procContractIndex) <= 0) + { + callError = CallErrorInsufficientFees; + return nullptr; + } + char* buffer = contractLocalsStack[_stackIndex].allocate(sizeof(QpiContextProcedureCall)); if (!buffer) { @@ -337,16 +389,17 @@ const QpiContextProcedureCall& QPI::QpiContextProcedureCall::__qpiConstructProce appendNumber(dbgMsgBuf, _stackIndex, FALSE); addDebugMessage(dbgMsgBuf); #endif - // abort execution of contract here - __qpiAbort(ContractErrorAllocContextOtherProcedureCallFailed); + callError = CallErrorAllocationFailed; + return nullptr; } // If transfer isn't possible, set invocation reward to 0 - if (transfer(QPI::id(procContractIndex, 0, 0, 0), invocationReward) < 0) + if (__transfer(QPI::id(procContractIndex, 0, 0, 0), invocationReward, TransferType::procedureInvocationByOtherContract) < 0) invocationReward = 0; - QpiContextProcedureCall& newContext = *reinterpret_cast(buffer); - newContext.init(procContractIndex, _originator, _currentContractId, invocationReward, _entryPoint, _stackIndex); + callError = NoCallError; + QpiContextProcedureCall* newContext = reinterpret_cast(buffer); + newContext->init(procContractIndex, _originator, _currentContractId, invocationReward, _entryPoint, _stackIndex); return newContext; } @@ -609,7 +662,7 @@ void QPI::QpiContextProcedureCall::__qpiReleaseStateForWriting(unsigned int cont // Used to run a special system procedure from within a contract for example in asset management rights transfer template -void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContractIndex, InputType& input, OutputType& output, QPI::sint64 invocationReward) const +bool QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContractIndex, InputType& input, OutputType& output, QPI::sint64 invocationReward) const { // Make sure this function is used with an expected combination of sysProcId, input, // and output @@ -619,6 +672,8 @@ void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContr || (sysProcId == POST_RELEASE_SHARES && sizeof(InputType) == sizeof(QPI::PostManagementRightsTransfer_input) && sizeof(OutputType) == sizeof(QPI::NoData)) || (sysProcId == POST_ACQUIRE_SHARES && sizeof(InputType) == sizeof(QPI::PostManagementRightsTransfer_input) && sizeof(OutputType) == sizeof(QPI::NoData)) || (sysProcId == POST_INCOMING_TRANSFER && sizeof(InputType) == sizeof(QPI::PostIncomingTransfer_input) && sizeof(OutputType) == sizeof(QPI::NoData)) + || (sysProcId == SET_SHAREHOLDER_PROPOSAL && sizeof(InputType) == sizeof(QPI::SET_SHAREHOLDER_PROPOSAL_input) && sizeof(OutputType) == sizeof(QPI::SET_SHAREHOLDER_PROPOSAL_output)) + || (sysProcId == SET_SHAREHOLDER_VOTES && sizeof(InputType) == sizeof(QPI::SET_SHAREHOLDER_VOTES_input) && sizeof(OutputType) == sizeof(QPI::SET_SHAREHOLDER_VOTES_output)) , "Unsupported __qpiCallSystemProc() call" ); @@ -627,7 +682,8 @@ void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContr ASSERT(sysProcContractIndex < contractCount); ASSERT(contractStates[sysProcContractIndex] != nullptr); if (sysProcId == PRE_RELEASE_SHARES || sysProcId == PRE_ACQUIRE_SHARES - || sysProcId == POST_RELEASE_SHARES || sysProcId == POST_ACQUIRE_SHARES) + || sysProcId == POST_RELEASE_SHARES || sysProcId == POST_ACQUIRE_SHARES + || sysProcId == SET_SHAREHOLDER_PROPOSAL || sysProcId == SET_SHAREHOLDER_VOTES) { ASSERT(sysProcContractIndex != _currentContractIndex); } @@ -637,7 +693,11 @@ void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContr // Empty procedures lead to null pointer in contractSystemProcedures -> return default output (all zero/false) if (!contractSystemProcedures[sysProcContractIndex][sysProcId]) - return; + { + // Returning false informs the caller that the system procedure isn't defined, which is useful if the + // zeroed output does not indicate an error but is a valid output value. + return false; + } // Set flags of callbacks currently running (to prevent deadlocks and nested calling of QPI functions) auto contractCallbacksRunningBefore = contractCallbacksRunning; @@ -645,6 +705,10 @@ void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContr { contractCallbacksRunning |= ContractCallbackPostIncomingTransfer; } + else if (sysProcId == SET_SHAREHOLDER_PROPOSAL || sysProcId == SET_SHAREHOLDER_VOTES) + { + contractCallbacksRunning |= ContractCallbackShareholderProposalAndVoting; + } else if (sysProcId == PRE_RELEASE_SHARES || sysProcId == PRE_ACQUIRE_SHARES || sysProcId == POST_RELEASE_SHARES || sysProcId == POST_ACQUIRE_SHARES) { @@ -652,7 +716,15 @@ void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContr } // Create context - const QpiContextProcedureCall& context = __qpiConstructProcedureCallContext(sysProcContractIndex, invocationReward); + InterContractCallError callError; + const QpiContextProcedureCall* context = __qpiConstructProcedureCallContext(sysProcContractIndex, invocationReward, callError, /*skipFeeCheck=*/ true); + if (!context) + { + if (callError == CallErrorContractInErrorState) + __qpiAbort(contractError[sysProcContractIndex]); + else + __qpiAbort(ContractErrorAllocContextOtherProcedureCallFailed); + } // Get state (lock state for writing if other contract) const bool otherContract = sysProcContractIndex != _currentContractIndex; @@ -666,7 +738,7 @@ void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContr setMem(localsBuffer, localsSize, 0); // Run procedure - contractSystemProcedures[sysProcContractIndex][sysProcId](context, state, &input, &output, localsBuffer); + contractSystemProcedures[sysProcContractIndex][sysProcId](*context, state, &input, &output, localsBuffer); // Cleanup: free locals, release state, and free context contractLocalsStack[_stackIndex].free(); @@ -676,6 +748,8 @@ void QPI::QpiContextProcedureCall::__qpiCallSystemProc(unsigned int sysProcContr // Restore flags of callbacks currently running contractCallbacksRunning = contractCallbacksRunningBefore; + + return true; } // If dest is a contract, notify contract by running system procedure POST_INCOMING_TRANSFER @@ -693,6 +767,70 @@ void QPI::QpiContextProcedureCall::__qpiNotifyPostIncomingTransfer(const QPI::id __qpiCallSystemProc(destContractIndex, input, output, 0); } +inline uint16 QPI::QpiContextProcedureCall::setShareholderProposal( + uint16 contractIndex, + const Array& proposalDataBuffer, + sint64 invocationReward +) const +{ + // prevent nested calling from callbacks + if (contractCallbacksRunning & ContractCallbackShareholderProposalAndVoting) + { + return INVALID_PROPOSAL_INDEX; + } + + // check for invalid inputs + if (contractIndex == _currentContractIndex + || contractIndex == 0 + || contractIndex >= contractCount + || invocationReward < 0) + { + return INVALID_PROPOSAL_INDEX; + } + + // Copy proposalDataBuffer, because procedures are allowed to change their input + Array inputBuffer = proposalDataBuffer; + + // run SET_SHAREHOLDER_PROPOSAL callback in other contract + uint16 outputProposalIndex; + if (!__qpiCallSystemProc(contractIndex, inputBuffer, outputProposalIndex, invocationReward)) + return INVALID_PROPOSAL_INDEX; + + return outputProposalIndex; +} + +inline bool QPI::QpiContextProcedureCall::setShareholderVotes( + uint16 contractIndex, + const ProposalMultiVoteDataV1& shareholderVoteData, + sint64 invocationReward +) const +{ + // prevent nested calling from callbacks + if (contractCallbacksRunning & ContractCallbackShareholderProposalAndVoting) + { + return false; + } + + // check for invalid inputs + if (contractIndex == _currentContractIndex + || contractIndex == 0 + || contractIndex >= contractCount + || invocationReward < 0) + { + return false; + } + + // Copy proposalDataBuffer, because procedures are allowed to change their input + ProposalMultiVoteDataV1 inputVote = shareholderVoteData; + + // run SET_SHAREHOLDER_VOTES callback in other contract + bit success; // initialized with zero by __qpiCallSystemProc + __qpiCallSystemProc(contractIndex, inputVote, success, invocationReward); + + return success; +} + + // Enter endless loop leading to timeout // -> TODO: unlock everything in case of function entry point, maybe retry later in case of deadlock handling // -> TODO: rollback of contract actions on contractProcessor() @@ -833,13 +971,15 @@ struct QpiContextSystemProcedureCall : public QPI::QpiContextProcedureCall // acquire state for writing (may block) contractStateLock[_currentContractIndex].acquireWrite(); - const unsigned long long startTick = __rdtsc(); + unsigned long long startTime, endTime; unsigned short localsSize = contractSystemProcedureLocalsSizes[_currentContractIndex][systemProcId]; if (localsSize == sizeof(QPI::NoData)) { // no locals -> call QPI::NoData locals; + startTime = __rdtsc(); contractSystemProcedures[_currentContractIndex][systemProcId](*this, contractStates[_currentContractIndex], input, output, &locals); + endTime = __rdtsc(); } else { @@ -850,13 +990,17 @@ struct QpiContextSystemProcedureCall : public QPI::QpiContextProcedureCall setMem(localsBuffer, localsSize, 0); // call system proc + startTime = __rdtsc(); contractSystemProcedures[_currentContractIndex][systemProcId](*this, contractStates[_currentContractIndex], input, output, localsBuffer); + endTime = __rdtsc(); // free data on stack contractLocalsStack[_stackIndex].free(); ASSERT(contractLocalsStack[_stackIndex].size() == 0); } - _interlockedadd64(&contractTotalExecutionTicks[_currentContractIndex], __rdtsc() - startTick); + const unsigned long long executionTime = endTime - startTime; + _interlockedadd64(&contractTotalExecutionTime[_currentContractIndex], executionTime); + executionTimeAccumulator.addTime(_currentContractIndex, executionTime); // release lock of contract state and set state to changed contractStateLock[_currentContractIndex].releaseWrite(); @@ -956,9 +1100,12 @@ struct QpiContextUserProcedureCall : public QPI::QpiContextProcedureCall contractStateLock[_currentContractIndex].acquireWrite(); // run procedure - const unsigned long long startTick = __rdtsc(); + const unsigned long long startTime = __rdtsc(); contractUserProcedures[_currentContractIndex][inputType](*this, contractStates[_currentContractIndex], inputBuffer, outputBuffer, localsBuffer); - _interlockedadd64(&contractTotalExecutionTicks[_currentContractIndex], __rdtsc() - startTick); + + const unsigned long long executionTime = __rdtsc() - startTime; + _interlockedadd64(&contractTotalExecutionTime[_currentContractIndex], executionTime); + executionTimeAccumulator.addTime(_currentContractIndex, executionTime); // release lock of contract state and set state to changed contractStateLock[_currentContractIndex].releaseWrite(); @@ -1016,6 +1163,12 @@ struct QpiContextUserFunctionCall : public QPI::QpiContextFunctionCall ASSERT(_currentContractIndex < contractCount); ASSERT(contractUserFunctions[_currentContractIndex][inputType]); + // Check if contract is in an error state before executing function + if (contractError[_currentContractIndex] != NoContractError) + { + return contractError[_currentContractIndex]; + } + // reserve stack for this processor (may block) constexpr unsigned int stacksNotUsedToReserveThemForStateWriter = 1; acquireContractLocalsStack(_stackIndex, stacksNotUsedToReserveThemForStateWriter); @@ -1087,9 +1240,9 @@ struct QpiContextUserFunctionCall : public QPI::QpiContextFunctionCall __qpiAcquireStateForReading(_currentContractIndex); // run function - const unsigned long long startTick = __rdtsc(); + const unsigned long long startTime = __rdtsc(); contractUserFunctions[_currentContractIndex][inputType](*this, contractStates[_currentContractIndex], inputBuffer, outputBuffer, localsBuffer); - _interlockedadd64(&contractTotalExecutionTicks[_currentContractIndex], __rdtsc() - startTick); + _interlockedadd64(&contractTotalExecutionTime[_currentContractIndex], __rdtsc() - startTime); // release lock of contract state __qpiReleaseStateForReading(_currentContractIndex); @@ -1111,3 +1264,94 @@ struct QpiContextUserFunctionCall : public QPI::QpiContextFunctionCall releaseContractLocalsStack(_stackIndex); } }; + + +struct UserProcedureNotification +{ + unsigned int contractIndex; + USER_PROCEDURE procedure; + const void* inputPtr; + unsigned short inputSize; + unsigned int localsSize; +}; + +// QPI context used to call contract user procedure as a notification from qubic core (contract processor). +// This means, it isn't triggered by a transaction, but following an event after having setup the notification +// callback in the contract code. +// Notification user procedures never receive an invocation reward. Invocator is NULL_ID. +// Currently, no output is supported, which may change in the future. +// The procedure pointer, the expected inputSize, and the expected localsSize, which are passed via +// UserProcedureNotification, must be consistent. The code using notifications is responible for ensuring that. +// Use cases: +// - oracle notifications (managed by oracleEngine) +struct QpiContextUserProcedureNotificationCall : public QPI::QpiContextProcedureCall +{ + QpiContextUserProcedureNotificationCall(const UserProcedureNotification& notification) : QPI::QpiContextProcedureCall(notif.contractIndex, NULL_ID, 0, USER_PROCEDURE_NOTIFICATION_CALL), notif(notification) + { + contractActionTracker.init(); + } + + // Run user procedure notification + void call() + { + ASSERT(_currentContractIndex < contractCount); + + // Return if nothing to call + if (!notif.procedure) + return; + + // reserve stack for this processor (may block), needed even if there are no locals, because procedure may call + // functions / procedures / notifications that create locals etc. + acquireContractLocalsStack(_stackIndex); + + // acquire state for writing (may block) + contractStateLock[_currentContractIndex].acquireWrite(); + + QPI::NoData output; + char* input = contractLocalsStack[_stackIndex].allocate(notif.inputSize + notif.localsSize); + if (!input) + { +#ifndef NDEBUG + CHAR16 dbgMsgBuf[400]; + setText(dbgMsgBuf, L"QpiContextUserProcedureNotificationCall stack buffer alloc failed in tick "); + appendNumber(dbgMsgBuf, system.tick, FALSE); + addDebugMessage(dbgMsgBuf); + setText(dbgMsgBuf, L"inputSize "); + appendNumber(dbgMsgBuf, notif.inputSize, FALSE); + appendText(dbgMsgBuf, L", localsSize "); + appendNumber(dbgMsgBuf, notif.localsSize, FALSE); + appendText(dbgMsgBuf, L", contractIndex "); + appendNumber(dbgMsgBuf, _currentContractIndex, FALSE); + appendText(dbgMsgBuf, L", stackIndex "); + appendNumber(dbgMsgBuf, _stackIndex, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + // abort execution of contract here + __qpiAbort(ContractErrorAllocInputOutputFailed); + } + char* locals = input + notif.inputSize; + copyMem(input, notif.inputPtr, notif.inputSize); + setMem(locals, notif.localsSize, 0); + + // call user procedure + const unsigned long long startTick = __rdtsc(); + notif.procedure(*this, contractStates[_currentContractIndex], input, &output, locals); + const unsigned long long executionTime = __rdtsc() - startTick; + _interlockedadd64(&contractTotalExecutionTime[_currentContractIndex], executionTime); + executionTimeAccumulator.addTime(_currentContractIndex, executionTime); + + // free data on stack + contractLocalsStack[_stackIndex].free(); + ASSERT(contractLocalsStack[_stackIndex].size() == 0); + + // release lock of contract state and set state to changed + contractStateLock[_currentContractIndex].releaseWrite(); + contractStateChangeFlags[_currentContractIndex >> 6] |= (1ULL << (_currentContractIndex & 63)); + + // release stack + releaseContractLocalsStack(_stackIndex); + } + +private: + const UserProcedureNotification& notif; +}; diff --git a/src/contract_core/execution_time_accumulator.h b/src/contract_core/execution_time_accumulator.h new file mode 100644 index 000000000..efff5cf87 --- /dev/null +++ b/src/contract_core/execution_time_accumulator.h @@ -0,0 +1,98 @@ +#pragma once + +#include "platform/file_io.h" +#include "platform/time_stamp_counter.h" +#include "../contracts/math_lib.h" + +// A class for accumulating contract execution time over a phase. +// Also saves the accumulation result of the previous phase. +class ExecutionTimeAccumulator +{ +private: + // Two arrays to accumulate and save the contract execution time (as CPU clock cycles) for two consecutive phases. + // This only includes actions that are charged an execution fee (digest computation, system procedures, user procedures). + // contractExecutionTimePerPhase[contractExecutionTimeActiveArrayIndex] is used to accumulate the contract execution ticks for the current phase n. + // contractExecutionTimePerPhase[!contractExecutionTimeActiveArrayIndex] saves the contract execution ticks from the previous phase n-1 that are sent out as transactions in phase n. + + unsigned long long contractExecutionTimePerPhase[2][contractCount]; + bool contractExecutionTimeActiveArrayIndex = 0; + volatile char lock = 0; + +public: + void init() + { + setMem((void*)contractExecutionTimePerPhase, sizeof(contractExecutionTimePerPhase), 0); + contractExecutionTimeActiveArrayIndex = 0; + + ASSERT(lock == 0); + } + + void acquireLock() + { + ACQUIRE(lock); + } + + void releaseLock() + { + RELEASE(lock); + } + + void startNewAccumulation() + { + ACQUIRE(lock); + contractExecutionTimeActiveArrayIndex = !contractExecutionTimeActiveArrayIndex; + setMem((void*)contractExecutionTimePerPhase[contractExecutionTimeActiveArrayIndex], sizeof(contractExecutionTimePerPhase[contractExecutionTimeActiveArrayIndex]), 0); + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Switched contract execution time array for new accumulation phase"); +#endif + } + + // Converts the input time specified as CPU ticks to microseconds and accumulates it for the current phase. + // If the CPU frequency is not available, the time will be added as raw CPU ticks. + void addTime(unsigned int contractIndex, unsigned long long time) + { + unsigned long long timeMicroSeconds = frequency > 0 ? (time * 1000000 / frequency) : time; + ACQUIRE(lock); + contractExecutionTimePerPhase[contractExecutionTimeActiveArrayIndex][contractIndex] = + math_lib::sadd(contractExecutionTimePerPhase[contractExecutionTimeActiveArrayIndex][contractIndex], timeMicroSeconds); + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + CHAR16 dbgMsgBuf[128]; + setText(dbgMsgBuf, L"Execution time added for contract "); + appendNumber(dbgMsgBuf, contractIndex, FALSE); + appendText(dbgMsgBuf, L": "); + appendNumber(dbgMsgBuf, timeMicroSeconds, FALSE); + appendText(dbgMsgBuf, L" microseconds"); + addDebugMessage(dbgMsgBuf); +#endif + } + + // Returns a pointer to the accumulated times from the previous phase for each contract. + // Make sure to acquire the lock before calling this function and only release it when finished accessing the returned data. + const unsigned long long* getPrevPhaseAccumulatedTimes() + { + return contractExecutionTimePerPhase[!contractExecutionTimeActiveArrayIndex]; + } + + bool saveToFile(const CHAR16* fileName, const CHAR16* directory = NULL) + { + long long savedSize = save(fileName, sizeof(ExecutionTimeAccumulator), (unsigned char*)this, directory); + if (savedSize == sizeof(ExecutionTimeAccumulator)) + return true; + else + return false; + } + + bool loadFromFile(const CHAR16* fileName, const CHAR16* directory = NULL) + { + long long loadedSize = load(fileName, sizeof(ExecutionTimeAccumulator), (unsigned char*)this, directory); + if (loadedSize == sizeof(ExecutionTimeAccumulator)) + return true; + else + return false; + } + +}; diff --git a/src/contract_core/ipo.h b/src/contract_core/ipo.h index ab1b1ae55..f0e56d969 100644 --- a/src/contract_core/ipo.h +++ b/src/contract_core/ipo.h @@ -26,7 +26,7 @@ static long long bidInContractIPO(long long price, unsigned short quantity, cons ASSERT(spectrumIndex >= 0); ASSERT(spectrumIndex == ::spectrumIndex(sourcePublicKey)); ASSERT(contractIndex < contractCount); - ASSERT(system.epoch < contractDescriptions[contractIndex].constructionEpoch); + ASSERT(system.epoch == contractDescriptions[contractIndex].constructionEpoch - 1); long long registeredBids = -1; @@ -128,7 +128,7 @@ static void finishIPOs() { for (unsigned int contractIndex = 1; contractIndex < contractCount; contractIndex++) { - if (system.epoch < contractDescriptions[contractIndex].constructionEpoch && contractStates[contractIndex]) + if (system.epoch == (contractDescriptions[contractIndex].constructionEpoch - 1) && contractStates[contractIndex]) { contractStateLock[contractIndex].acquireRead(); IPO* ipo = (IPO*)contractStates[contractIndex]; @@ -180,9 +180,14 @@ static void finishIPOs() } contractStateLock[contractIndex].releaseRead(); - contractStateLock[0].acquireWrite(); - contractFeeReserve(contractIndex) = finalPrice * NUMBER_OF_COMPUTORS; - contractStateLock[0].releaseWrite(); + if (finalPrice > 0) + { + setContractFeeReserve(contractIndex, finalPrice * NUMBER_OF_COMPUTORS); + } + else + { + contractError[contractIndex] = ContractErrorIPOFailed; + } } } } diff --git a/src/contract_core/pre_qpi_def.h b/src/contract_core/pre_qpi_def.h new file mode 100644 index 000000000..7a5aabd49 --- /dev/null +++ b/src/contract_core/pre_qpi_def.h @@ -0,0 +1,62 @@ +#pragma once + +#include "network_messages/common_def.h" +#include "platform/m256.h" + +namespace QPI +{ + struct QpiContextProcedureCall; + struct QpiContextFunctionCall; +} + +// TODO: add option for having locals to SYSTEM and EXPAND procedures +typedef void (*SYSTEM_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); +typedef void (*EXPAND_PROCEDURE)(const QPI::QpiContextFunctionCall&, void*, void*); // cannot not change anything except state +typedef void (*USER_FUNCTION)(const QPI::QpiContextFunctionCall&, void* state, void* input, void* output, void* locals); +typedef void (*USER_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); + +constexpr unsigned long long MAX_CONTRACT_STATE_SIZE = 1073741824; + +// Maximum size of local variables that may be used by a contract function or procedure +// If increased, the size of contractLocalsStack should be increased as well. +constexpr unsigned int MAX_SIZE_OF_CONTRACT_LOCALS = 32 * 1024; + +// TODO: make sure the limit of nested calls is not violated +constexpr unsigned short MAX_NESTED_CONTRACT_CALLS = 10; + +// Size of the contract action tracker, limits the number of transfers that one contract call can execute. +constexpr unsigned long long CONTRACT_ACTION_TRACKER_SIZE = 16 * 1024 * 1024; + + +static void __beginFunctionOrProcedure(const unsigned int); // TODO: more human-readable form of function ID? +static void __endFunctionOrProcedure(const unsigned int); +template static m256i __K12(T); +template static void __logContractDebugMessage(unsigned int, T&); +template static void __logContractErrorMessage(unsigned int, T&); +template static void __logContractInfoMessage(unsigned int, T&); +template static void __logContractWarningMessage(unsigned int, T&); +static void __pauseLogMessage(); +static void __resumeLogMessage(); + +// Get buffer for temporary use. Can only be used in contract procedures / tick processor / contract processor! +// Always returns the same one buffer, no concurrent access! +static void* __scratchpad(unsigned long long sizeToMemsetZero = 0); + +// static void* __tryAcquireScratchpad(unsigned int size); // Thread-safe, may return nullptr if no appropriate buffer is available +// static void __ReleaseScratchpad(void*); + +template +struct __FunctionOrProcedureBeginEndGuard +{ + // Constructor calling __beginFunctionOrProcedure() + __FunctionOrProcedureBeginEndGuard() + { + __beginFunctionOrProcedure(functionOrProcedureId); + } + + // Destructor making sure __endFunctionOrProcedure() is called for every return path + ~__FunctionOrProcedureBeginEndGuard() + { + __endFunctionOrProcedure(functionOrProcedureId); + } +}; diff --git a/src/contract_core/qpi_asset_impl.h b/src/contract_core/qpi_asset_impl.h index dc1520a05..07c4cba00 100644 --- a/src/contract_core/qpi_asset_impl.h +++ b/src/contract_core/qpi_asset_impl.h @@ -486,6 +486,13 @@ bool QPI::QpiContextProcedureCall::distributeDividends(long long amountPerShare) return false; } + // this part of code doesn't perform completed QuTransfers, instead it `decreaseEnergy` all QUs at once and `increaseEnergy` multiple times. + // Meanwhile, a QUTransfer requires a pair of both decrease & increase calls. + // This behavior will produce different numberOfOutgoingTransfers for the SC index. + // 3rd party software needs to catch the HINT message to know the distribute dividends operation + DummyCustomMessage dcm{ CUSTOM_MESSAGE_OP_START_DISTRIBUTE_DIVIDENDS }; + logger.logCustomMessage(dcm); + if (decreaseEnergy(index, amountPerShare * NUMBER_OF_COMPUTORS)) { ACQUIRE(universeLock); @@ -499,19 +506,22 @@ bool QPI::QpiContextProcedureCall::distributeDividends(long long amountPerShare) ASSERT(iter.possessionIndex() < ASSETS_CAPACITY); const auto& possession = assets[iter.possessionIndex()].varStruct.possession; - const long long dividend = amountPerShare * possession.numberOfShares; - increaseEnergy(possession.publicKey, dividend); + if (possession.numberOfShares) + { + const long long dividend = amountPerShare * possession.numberOfShares; + increaseEnergy(possession.publicKey, dividend); - if (!contractActionTracker.addQuTransfer(_currentContractId, possession.publicKey, dividend)) - __qpiAbort(ContractErrorTooManyActions); + if (!contractActionTracker.addQuTransfer(_currentContractId, possession.publicKey, dividend)) + __qpiAbort(ContractErrorTooManyActions); - __qpiNotifyPostIncomingTransfer(_currentContractId, possession.publicKey, dividend, TransferType::qpiDistributeDividends); + __qpiNotifyPostIncomingTransfer(_currentContractId, possession.publicKey, dividend, TransferType::qpiDistributeDividends); - const QuTransfer quTransfer = { _currentContractId, possession.publicKey, dividend }; - logger.logQuTransfer(quTransfer); + const QuTransfer quTransfer = { _currentContractId, possession.publicKey, dividend }; + logger.logQuTransfer(quTransfer); - totalShareCounter += possession.numberOfShares; + totalShareCounter += possession.numberOfShares; + } iter.next(); } @@ -520,7 +530,8 @@ bool QPI::QpiContextProcedureCall::distributeDividends(long long amountPerShare) RELEASE(universeLock); } - + dcm = DummyCustomMessage{ CUSTOM_MESSAGE_OP_END_DISTRIBUTE_DIVIDENDS }; + logger.logCustomMessage(dcm); return true; } diff --git a/src/contract_core/qpi_collection_impl.h b/src/contract_core/qpi_collection_impl.h index 4bca5ee2b..2fbf9cedf 100644 --- a/src/contract_core/qpi_collection_impl.h +++ b/src/contract_core/qpi_collection_impl.h @@ -615,11 +615,10 @@ namespace QPI } // Init buffers - auto* _povsBuffer = reinterpret_cast(::__scratchpad()); + auto* _povsBuffer = reinterpret_cast(::__scratchpad(sizeof(_povs) + sizeof(_povOccupationFlags))); auto* _povOccupationFlagsBuffer = reinterpret_cast(_povsBuffer + L); auto* _stackBuffer = reinterpret_cast( _povOccupationFlagsBuffer + sizeof(_povOccupationFlags) / sizeof(_povOccupationFlags[0])); - setMem(::__scratchpad(), sizeof(_povs) + sizeof(_povOccupationFlags), 0); uint64 newPopulation = 0; // Go through pov hash map. For each pov that is occupied but not marked for removal, insert pov in new Collection's pov buffers and diff --git a/src/contract_core/qpi_hash_map_impl.h b/src/contract_core/qpi_hash_map_impl.h index 033dc9fbb..6266cfdb9 100644 --- a/src/contract_core/qpi_hash_map_impl.h +++ b/src/contract_core/qpi_hash_map_impl.h @@ -288,11 +288,10 @@ namespace QPI } // Init buffers - auto* _elementsBuffer = reinterpret_cast(::__scratchpad()); + auto* _elementsBuffer = reinterpret_cast(::__scratchpad(sizeof(_elements) + sizeof(_occupationFlags))); auto* _occupationFlagsBuffer = reinterpret_cast(_elementsBuffer + L); auto* _stackBuffer = reinterpret_cast( _occupationFlagsBuffer + sizeof(_occupationFlags) / sizeof(_occupationFlags[0])); - setMem(::__scratchpad(), sizeof(_elements) + sizeof(_occupationFlags), 0); uint64 newPopulation = 0; // Go through hash map. For each element that is occupied but not marked for removal, insert element in new hash map's buffers. @@ -615,11 +614,10 @@ namespace QPI } // Init buffers - auto* _keyBuffer = reinterpret_cast(::__scratchpad()); + auto* _keyBuffer = reinterpret_cast(::__scratchpad(sizeof(_keys) + sizeof(_occupationFlags))); auto* _occupationFlagsBuffer = reinterpret_cast(_keyBuffer + L); auto* _stackBuffer = reinterpret_cast( _occupationFlagsBuffer + sizeof(_occupationFlags) / sizeof(_occupationFlags[0])); - setMem(::__scratchpad(), sizeof(_keys) + sizeof(_occupationFlags), 0); uint64 newPopulation = 0; // Go through hash map. For each element that is occupied but not marked for removal, insert element in new hash map's buffers. diff --git a/src/contract_core/qpi_ipo_impl.h b/src/contract_core/qpi_ipo_impl.h index 5287f4c92..b2cc02dfa 100644 --- a/src/contract_core/qpi_ipo_impl.h +++ b/src/contract_core/qpi_ipo_impl.h @@ -12,7 +12,7 @@ QPI::sint64 QPI::QpiContextProcedureCall::bidInIPO(unsigned int IPOContractIndex return -1; } - if (system.epoch >= contractDescriptions[IPOContractIndex].constructionEpoch) // IPO is finished. + if (system.epoch != (contractDescriptions[IPOContractIndex].constructionEpoch - 1)) // IPO has not started yet or is finished. { return -1; } @@ -30,7 +30,7 @@ QPI::sint64 QPI::QpiContextProcedureCall::bidInIPO(unsigned int IPOContractIndex // Returns the ID of the entity who has made this IPO bid or NULL_ID if the ipoContractIndex or ipoBidIndex are invalid. QPI::id QPI::QpiContextFunctionCall::ipoBidId(QPI::uint32 ipoContractIndex, QPI::uint32 ipoBidIndex) const { - if (ipoContractIndex >= contractCount || system.epoch >= contractDescriptions[ipoContractIndex].constructionEpoch || ipoBidIndex >= NUMBER_OF_COMPUTORS) + if (ipoContractIndex >= contractCount || system.epoch != (contractDescriptions[ipoContractIndex].constructionEpoch - 1) || ipoBidIndex >= NUMBER_OF_COMPUTORS) { return NULL_ID; } @@ -51,7 +51,7 @@ QPI::sint64 QPI::QpiContextFunctionCall::ipoBidPrice(QPI::uint32 ipoContractInde return -1; } - if (system.epoch >= contractDescriptions[ipoContractIndex].constructionEpoch) + if (system.epoch != (contractDescriptions[ipoContractIndex].constructionEpoch - 1)) { return -2; } diff --git a/src/contract_core/qpi_mining_impl.h b/src/contract_core/qpi_mining_impl.h new file mode 100644 index 000000000..be4cf796c --- /dev/null +++ b/src/contract_core/qpi_mining_impl.h @@ -0,0 +1,27 @@ +#pragma once + +#include "contracts/qpi.h" +#include "score.h" + +static ScoreFunction< + NUMBER_OF_INPUT_NEURONS, + NUMBER_OF_OUTPUT_NEURONS, + NUMBER_OF_TICKS*2, + NUMBER_OF_NEIGHBORS, + POPULATION_THRESHOLD, + NUMBER_OF_MUTATIONS, + SOLUTION_THRESHOLD_DEFAULT, + 1 +>* score_qpi = nullptr; // NOTE: SC is single-threaded + +m256i QPI::QpiContextFunctionCall::computeMiningFunction(const m256i miningSeed, const m256i publicKey, const m256i nonce) const +{ + // Score's currentRandomSeed is initialized to zero by setMem(score_qpi, sizeof(*score_qpi), 0) + // If the mining seed changes, we must reinitialize it + if (miningSeed != score_qpi->currentRandomSeed) + { + score_qpi->initMiningData(miningSeed); + } + (*score_qpi)(0, publicKey, miningSeed, nonce); + return score_qpi->getLastOutput(0); +} diff --git a/src/contract_core/qpi_proposal_voting.h b/src/contract_core/qpi_proposal_voting.h index c5dafc96a..e1ccb00d3 100644 --- a/src/contract_core/qpi_proposal_voting.h +++ b/src/contract_core/qpi_proposal_voting.h @@ -11,14 +11,14 @@ namespace QPI // Maximum number of proposals (may be lower than number of proposers = IDs with right to propose and lower than num. of voters) static constexpr uint16 maxProposals = proposalSlotCount; - // Maximum number of voters - static constexpr uint32 maxVoters = NUMBER_OF_COMPUTORS; + // Maximum number of voters / votes (each computor has one vote) + static constexpr uint32 maxVotes = NUMBER_OF_COMPUTORS; // Check if proposer has right to propose (and is not NULL_ID) bool isValidProposer(const QpiContextFunctionCall& qpi, const id& proposerId) const { - // Check if proposer is currently a computor (voter index is computor index here) - return getVoterIndex(qpi, proposerId) < maxVoters; + // Check if proposer is currently a computor (vote index is computor index here) + return getVoteIndex(qpi, proposerId) < maxVotes; } // Get new proposal slot (each proposer may have at most one). @@ -76,32 +76,40 @@ namespace QPI return INVALID_PROPOSAL_INDEX; } - // Get voter index for given ID or INVALID_VOTER_INDEX if has no right to vote - // Voter index is computor index - uint32 getVoterIndex(const QpiContextFunctionCall& qpi, const id& voterId) const + // Get first vote index for given ID or INVALID_VOTE_INDEX if voterId has no right to vote. + // Vote index is computor index. + uint32 getVoteIndex(const QpiContextFunctionCall& qpi, const id& voterId, uint16 proposalIndex = 0) const { // NULL_ID is invalid if (isZero(voterId)) - return INVALID_VOTER_INDEX; + return INVALID_VOTE_INDEX; - for (uint16 compIdx = 0; compIdx < maxVoters; ++compIdx) + for (uint16 compIdx = 0; compIdx < maxVotes; ++compIdx) { if (qpi.computor(compIdx) == voterId) return compIdx; } - return INVALID_VOTER_INDEX; + return INVALID_VOTE_INDEX; } - // Return voter ID for given voter index or NULL_ID on error - id getVoterId(const QpiContextFunctionCall& qpi, uint16 voterIndex) const + // Get count of votes of a voter specified by vote index (return 0 on error). + uint32 getVoteCount(const QpiContextFunctionCall& qpi, uint32 voteIndex, uint16 proposalIndex = 0) const { - if (voterIndex >= maxVoters) + if (voteIndex >= maxVotes) + return 0; + + return 1; + } + + // Return voter ID for given vote index or NULL_ID on error + id getVoterId(const QpiContextFunctionCall& qpi, uint32 voteIndex, uint16 proposalIndex = 0) const + { + if (voteIndex >= maxVotes) return NULL_ID; - return qpi.computor(voterIndex); + return qpi.computor(voteIndex); } protected: - // TODO: maybe replace by hash map? // needs to be initialized with zeros id currentProposalProposers[maxProposals]; }; @@ -116,9 +124,217 @@ namespace QPI } }; - template + // Option for ProposerAndVoterHandlingT in ProposalVoting that allows both voting and setting proposals for contract shareholders only. + // A shareholder can have multiple votes and each may be set individually. Voting rights are assigned to current shareholders when a proposal + // is created or overwritten and cannot be sold or transferred afterwards. + template struct ProposalAndVotingByShareholders { + // Maximum number of proposals (may be lower than number of proposers = IDs with right to propose and lower than num. of voters) + static constexpr uint16 maxProposals = proposalSlotCount; + + // Maximum number of votes (676 shares per contract) + static constexpr uint32 maxVotes = NUMBER_OF_COMPUTORS; + + // Check if proposer has right to propose (and is not NULL_ID) + bool isValidProposer(const QpiContextFunctionCall& qpi, const id& proposerId) const + { + return qpi.numberOfShares({ NULL_ID, contractAssetName }, AssetOwnershipSelect::byOwner(proposerId), AssetPossessionSelect::byPossessor(proposerId)) > 0; + }; + + // Setup proposal in proposal index. Asset possession at this point in time defines the right to vote. + void setupNewProposal(const QpiContextFunctionCall& qpi, const id& proposerId, uint16 proposalIdx) + { + if (proposalIdx >= maxProposals || isZero(proposerId)) + return; + + currentProposalProposers[proposalIdx] = proposerId; + + // prepare temporary array to gather shareholder info + struct Shareholder + { + id possessor; + sint64 shares; + }; + Shareholder* shareholders = reinterpret_cast(__scratchpad(sizeof(Shareholder) * maxVotes)); + int lastShareholderIdx = -1; + + // gather shareholder info in sorted array + for (AssetPossessionIterator iter({ NULL_ID, contractAssetName }); !iter.reachedEnd(); iter.next()) + { + if (iter.numberOfPossessedShares() > 0) + { + // search sorted array backwards + // (iter will provide possessors mostly in increasing order leading to low number of search + // and move iterations) + const id& possessor = iter.possessor(); + int idx = lastShareholderIdx; + while (idx >= 0 && !(shareholders[idx].possessor < possessor)) + { + --idx; + } + ++idx; + + // update array: idx is the position to insert at with ID[idx] >= NewID + if (idx <= lastShareholderIdx && shareholders[idx].possessor == possessor) + { + // possessor is already in array -> increase number of shares + shareholders[idx].shares += iter.numberOfPossessedShares(); + } + else + { + // possessor is not in array yet -> add it to the right place (after moving items if needed) + for (int idxMove = lastShareholderIdx; idxMove >= idx; --idxMove) + { + shareholders[idxMove + 1] = shareholders[idxMove]; + } + shareholders[idx].possessor = possessor; + shareholders[idx].shares = iter.numberOfPossessedShares(); + ++lastShareholderIdx; + } + } + } + +#ifndef NDEBUG + // sanity check of array (sorted, has expected size, and 676 shares in total) + ASSERT(lastShareholderIdx >= 0); + ASSERT(lastShareholderIdx < maxVotes); + sint64 totalShares = 0; + for (int idx = 0; idx < lastShareholderIdx; ++idx) + { + ASSERT(shareholders[idx].possessor < shareholders[idx + 1].possessor); + ASSERT(shareholders[idx].shares > 0); + totalShares += shareholders[idx].shares; + } + ASSERT(shareholders[lastShareholderIdx].shares > 0); + totalShares += shareholders[lastShareholderIdx].shares; + ASSERT(totalShares == maxVotes); +#endif + + // build sorted array of votes (one entry per share) + int voteIdx = 0; + for (int shareholderIdx = 0; shareholderIdx <= lastShareholderIdx; ++shareholderIdx) + { + const Shareholder& shareholder = shareholders[shareholderIdx]; + for (int shareIdx = 0; shareIdx < shareholder.shares; ++shareIdx) + { + currentProposalShareholders[proposalIdx][voteIdx] = shareholder.possessor; + ++voteIdx; + } + } + ASSERT(voteIdx == maxVotes); + } + + // Get new proposal slot (each proposer may have at most one). + // Returns proposal index or INVALID_PROPOSAL_INDEX on error. + // CAUTION: Only pass valid proposers! + uint16 getNewProposalIndex(const QpiContextFunctionCall& qpi, const id& proposerId) + { + // Reuse slot if proposer has existing proposal + uint16 idx = getExistingProposalIndex(qpi, proposerId); + if (idx < maxProposals) + { + setupNewProposal(qpi, proposerId, idx); + return idx; + } + + // Otherwise, try to find empty slot + for (idx = 0; idx < maxProposals; ++idx) + { + if (isZero(currentProposalProposers[idx])) + { + setupNewProposal(qpi, proposerId, idx); + return idx; + } + } + + // No empty slot -> fail + return INVALID_PROPOSAL_INDEX; + } + + void freeProposalByIndex(const QpiContextFunctionCall& qpi, uint16 proposalIndex) + { + if (proposalIndex < maxProposals) + { + currentProposalProposers[proposalIndex] = NULL_ID; + setMem(currentProposalShareholders[proposalIndex], sizeof(id) * maxVotes, 0); + } + } + + // Return proposer ID for given proposal index or NULL_ID if there is no proposal + id getProposerId(const QpiContextFunctionCall& qpi, uint16 proposalIndex) const + { + if (proposalIndex >= maxProposals) + return NULL_ID; + return currentProposalProposers[proposalIndex]; + } + + // Get new index of existing used proposal of proposer if any; only pass valid proposers! + // Returns proposal index or INVALID_PROPOSAL_INDEX if there is no proposal for given proposer. + uint16 getExistingProposalIndex(const QpiContextFunctionCall& qpi, const id& proposerId) const + { + if (isZero(proposerId)) + return INVALID_PROPOSAL_INDEX; + for (uint16 i = 0; i < maxProposals; ++i) + { + if (currentProposalProposers[i] == proposerId) + return i; + } + return INVALID_PROPOSAL_INDEX; + } + + // Return vote index for given ID or INVALID_VOTE_INDEX if ID has no right to vote. If the voter has multiple + // votes, this returns the first index. All votes of a voter are stored consecutively. + uint32 getVoteIndex(const QpiContextFunctionCall& qpi, const id& voterId, uint16 proposalIndex) const + { + // NULL_ID is invalid + if (isZero(voterId) || proposalIndex >= maxProposals) + return INVALID_VOTE_INDEX; + + // Search for first vote index with voterId + // Note: This may be speeded up a bit because the array is sorted, but it is required to return the first element + // in a set of duplicates. + for (uint16 voteIdx = 0; voteIdx < maxVotes; ++voteIdx) + { + if (currentProposalShareholders[proposalIndex][voteIdx] == voterId) + return voteIdx; + } + + return INVALID_VOTE_INDEX; + } + + // Get count of votes of a voter specified by its first vote index (return 0 on error). + uint32 getVoteCount(const QpiContextFunctionCall& qpi, uint32 voteIndex, uint16 proposalIndex) const + { + if (voteIndex >= maxVotes || proposalIndex >= maxProposals) + return 0; + + const id* shareholders = currentProposalShareholders[proposalIndex]; + uint32 count = 1; + const id& voterId = shareholders[voteIndex]; + for (uint32 idx = voteIndex + 1; idx < maxVotes; ++idx) + { + if (shareholders[idx] != voterId) + break; + ++count; + } + + return count; + } + + // Return voter ID for given vote index or NULL_ID on error + id getVoterId(const QpiContextFunctionCall& qpi, uint32 voteIndex, uint16 proposalIndex) const + { + if (voteIndex >= maxVotes || proposalIndex >= maxProposals) + return NULL_ID; + return currentProposalShareholders[proposalIndex][voteIndex]; + } + + + protected: + // needs to be initialized with zeros + id currentProposalProposers[maxProposals]; + id currentProposalShareholders[maxProposals][NUMBER_OF_COMPUTORS]; }; // Check if given type is valid (supported by most comprehensive ProposalData class). @@ -130,6 +346,7 @@ namespace QPI switch (cls) { case ProposalTypes::Class::GeneralOptions: + case ProposalTypes::Class::MultiVariables: valid = (options >= 2 && options <= 8); break; case ProposalTypes::Class::Transfer: @@ -156,7 +373,7 @@ namespace QPI // Used internally by ProposalVoting to store a proposal with all votes. // Supports all vote types. - template + template struct ProposalWithAllVoteData : public ProposalDataType { // Select type for storage (sint64 if scalar votes are supported, uint8 otherwise). @@ -164,42 +381,42 @@ namespace QPI typedef __VoteStorageTypeSelector::type VoteStorageType; // Vote storage - VoteStorageType votes[numOfVoters]; + VoteStorageType votes[numOfVotes]; // Set proposal and reset all votes bool set(const ProposalDataType& proposal) { if (!supportScalarVotes && proposal.type == ProposalTypes::VariableScalarMean) return false; - + copyMemory(*(ProposalDataType*)this, proposal); if (!supportScalarVotes) { - // option voting only (1 byte per voter) + // option voting only (1 byte per vote) ASSERT(proposal.type != ProposalTypes::VariableScalarMean); constexpr uint8 noVoteValue = 0xff; setMemory(votes, noVoteValue); } else { - // scalar voting supported (sint64 per voter) + // scalar voting supported (sint64 per vote) // (cast should not be needed but is to get rid of warning) - for (uint32 i = 0; i < numOfVoters; ++i) + for (uint32 i = 0; i < numOfVotes; ++i) votes[i] = static_cast(NO_VOTE_VALUE); } return true; } - // Set vote value (as used in ProposalSingleVoteData) of given voter if voter and value are valid - bool setVoteValue(uint32 voterIndex, sint64 voteValue) + // Set vote value (as used in ProposalSingleVoteData) of given index if index and value are valid + bool setVoteValue(uint32 voteIndex, sint64 voteValue) { bool ok = false; - if (voterIndex < numOfVoters) + if (voteIndex < numOfVotes) { if (voteValue == NO_VOTE_VALUE) { - votes[voterIndex] = (supportScalarVotes) ? NO_VOTE_VALUE : 0xff; + votes[voteIndex] = (supportScalarVotes) ? NO_VOTE_VALUE : 0xff; ok = true; } else @@ -213,7 +430,7 @@ namespace QPI if ((voteValue >= this->variableScalar.minValue && voteValue <= this->variableScalar.maxValue)) { // (cast should not be needed but is to get rid of warning) - votes[voterIndex] = static_cast(voteValue); + votes[voteIndex] = static_cast(voteValue); ok = true; } } @@ -224,7 +441,7 @@ namespace QPI int numOptions = ProposalTypes::optionCount(this->type); if (voteValue >= 0 && voteValue < numOptions) { - votes[voterIndex] = static_cast(voteValue); + votes[voteIndex] = static_cast(voteValue); ok = true; } } @@ -233,23 +450,23 @@ namespace QPI return ok; } - // Get vote value of given voter as used in ProposalSingleVoteData - sint64 getVoteValue(uint32 voterIndex) const + // Get vote value of given vote as used in ProposalSingleVoteData + sint64 getVoteValue(uint32 voteIndex) const { sint64 vv = NO_VOTE_VALUE; - if (voterIndex < numOfVoters) + if (voteIndex < numOfVotes) { if (supportScalarVotes) { // stored in sint64 -> set directly - vv = votes[voterIndex]; + vv = votes[voteIndex]; } else { // stored in uint8 -> set if valid vote (not no-vote value 0xff) - if (votes[voterIndex] != 0xff) + if (votes[voteIndex] != 0xff) { - vv = votes[voterIndex]; + vv = votes[voteIndex]; } } } @@ -259,11 +476,11 @@ namespace QPI // Used internally by ProposalVoting to store a proposal with all votes // Template specialization if only yes/no is supported (saves storage space in votes) - template - struct ProposalWithAllVoteData : public ProposalDataYesNo + template + struct ProposalWithAllVoteData : public ProposalDataYesNo { - // Vote storage (2 bit per voter) - uint8 votes[(2 * numOfVoters + 7) / 8]; + // Vote storage (2 bit per vote) + uint8 votes[(2 * numOfVotes + 7) / 8]; // Set proposal and reset all votes bool set(const ProposalDataYesNo& proposal) @@ -273,23 +490,23 @@ namespace QPI copyMemory(*(ProposalDataYesNo*)this, proposal); - // option voting only (2 bit per voter) + // option voting only (2 bit per vote) constexpr uint8 noVoteValue = 0xff; setMemory(votes, noVoteValue); return true; } - // Set vote value (as used in ProposalSingleVoteData) of given voter if voter and value are valid - bool setVoteValue(uint32 voterIndex, sint64 voteValue) + // Set vote value (as used in ProposalSingleVoteData) of given index if index and value are valid + bool setVoteValue(uint32 voteIndex, sint64 voteValue) { bool ok = false; - if (voterIndex < numOfVoters) + if (voteIndex < numOfVotes) { if (voteValue == NO_VOTE_VALUE) { - uint8 bits = (3 << ((voterIndex & 3) * 2)); - votes[voterIndex >> 2] |= bits; + uint8 bits = (3 << ((voteIndex & 3) * 2)); + votes[voteIndex >> 2] |= bits; ok = true; } else @@ -297,10 +514,10 @@ namespace QPI uint16 numOptions = ProposalTypes::optionCount(this->type); if (voteValue >= 0 && voteValue < numOptions) { - uint8 bitMask = (3 << ((voterIndex & 3) * 2)); - uint8 bitNum = (uint8(voteValue) << ((voterIndex & 3) * 2)); - votes[voterIndex >> 2] &= ~bitMask; - votes[voterIndex >> 2] |= bitNum; + uint8 bitMask = (3 << ((voteIndex & 3) * 2)); + uint8 bitNum = (uint8(voteValue) << ((voteIndex & 3) * 2)); + votes[voteIndex >> 2] &= ~bitMask; + votes[voteIndex >> 2] |= bitNum; ok = true; } } @@ -308,14 +525,14 @@ namespace QPI return ok; } - // Get vote value of given voter as used in ProposalSingleVoteData - sint64 getVoteValue(uint32 voterIndex) const + // Get vote value of given vote as used in ProposalSingleVoteData + sint64 getVoteValue(uint32 voteIndex) const { sint64 vv = NO_VOTE_VALUE; - if (voterIndex < numOfVoters) + if (voteIndex < numOfVotes) { // stored in uint8 -> set if valid vote (not no-vote value 0xff) - uint8 value = (votes[voterIndex >> 2] >> ((voterIndex & 3) * 2)) & 3; + uint8 value = (votes[voteIndex >> 2] >> ((voteIndex & 3) * 2)) & 3; if (value != 3) { vv = value; @@ -367,7 +584,7 @@ namespace QPI // all occupied slots are used in current epoch? -> error if (proposalIndex >= pv.maxProposals) return INVALID_PROPOSAL_INDEX; - + // remove oldest proposal clearProposal(proposalIndex); @@ -429,11 +646,115 @@ namespace QPI if (vote.proposalTick != proposal.tick) return false; - // Return voter index (which may be INVALID_VOTER_INDEX if voter has no right to vote) - unsigned int voterIndex = pv.proposersAndVoters.getVoterIndex(qpi, voter); + // Return vote index (which may be INVALID_VOTE_INDEX if voter has no right to vote) + unsigned int voteIndex = pv.proposersAndVoters.getVoteIndex(qpi, voter, vote.proposalIndex); + if (voteIndex == INVALID_VOTE_INDEX) + return false; + + // Get count of votes that this voter can cast + unsigned int voteCount = pv.proposersAndVoters.getVoteCount(qpi, voteIndex, vote.proposalIndex); + ASSERT(voteCount >= 1); - // Set vote value (checking that voter index and value are valid) - return proposal.setVoteValue(voterIndex, vote.voteValue); + // Set vote value(s) (shareholder has one vote per share / computor has one vote only) + bool okay = true; + for (unsigned int i = 0; i < voteCount; ++i) + { + // Set vote value (checking that vote index and value are valid) + okay = proposal.setVoteValue(voteIndex + i, vote.voteValue); + if (!okay) + break; + } + return okay; + } + + template + bool QpiContextProposalProcedureCall::vote( + const id& voter, + const ProposalMultiVoteDataV1& vote + ) + { + ProposalVotingType& pv = const_cast(this->pv); + const QpiContextFunctionCall& qpi = this->qpi; + + if (vote.proposalIndex >= pv.maxProposals) + return false; + + // Check that vote matches proposal + auto& proposal = pv.proposals[vote.proposalIndex]; + if (vote.proposalType != proposal.type) + return false; + if (qpi.epoch() != proposal.epoch) + return false; + if (vote.proposalTick != proposal.tick) + return false; + + // Return vote index (which may be INVALID_VOTE_INDEX if voter has no right to vote) + unsigned int voteIndexBegin = pv.proposersAndVoters.getVoteIndex(qpi, voter, vote.proposalIndex); + if (voteIndexBegin == INVALID_VOTE_INDEX) + return false; + + // Get count of votes that this voter can cast + unsigned int voteCountTotal = pv.proposersAndVoters.getVoteCount(qpi, voteIndexBegin, vote.proposalIndex); + ASSERT(voteCountTotal >= 1); + + // Get count of votes sent + unsigned int voteCountSent = 0; + for (unsigned int i = 0; i < vote.voteCounts.capacity(); ++i) + voteCountSent += vote.voteCounts.get(i); + + // Sent more votes than allowed? + if (voteCountSent > voteCountTotal) + return false; + + // Set all votes up to total vote count (votes not sent are set to invalid) + unsigned int voteIndex = voteIndexBegin; + const unsigned int voteIndexEnd = voteIndexBegin + voteCountTotal; + + // Compatibility case? -> count 0 means all votes with same value + bool okay = true; + if (voteCountSent == 0) + { + for (; voteIndex < voteIndexEnd; ++voteIndex) + { + // Set vote value (checking that vote index and value are valid) + okay = proposal.setVoteValue(voteIndex, vote.voteValues.get(0)); + if (!okay) + { + // On error, fill all with invalid/no votes + voteIndex = voteIndexBegin; + goto leave; + } + } + return okay; + } + + // Set multiple vote values (shareholder has multiple votes) + for (unsigned int i = 0; i < vote.voteCounts.capacity(); ++i) + { + sint64 voteValue = vote.voteValues.get(i); + uint32 voteCount = vote.voteCounts.get(i); + for (unsigned int j = 0; j < voteCount; ++j) + { + // Set vote value (checking that vote index and value are valid) + okay = proposal.setVoteValue(voteIndex, voteValue); + ++voteIndex; + if (!okay) + { + // On error, fill all with invalid/no votes + voteIndex = voteIndexBegin; + goto leave; + } + } + } + + leave: + // Set remaining votes to no vote + for (; voteIndex < voteIndexEnd; ++voteIndex) + { + proposal.setVoteValue(voteIndex, NO_VOTE_VALUE); + } + + return okay; } template @@ -443,7 +764,11 @@ namespace QPI ) const { if (proposalIndex >= pv.maxProposals || !pv.proposals[proposalIndex].epoch) + { + // proposal.type == 0 indicates error + proposal.type = 0; return false; + } const ProposalDataType& storedProposal = *static_cast(&pv.proposals[proposalIndex]); copyMemory(proposal, storedProposal); return true; @@ -452,25 +777,111 @@ namespace QPI template bool QpiContextProposalFunctionCall::getVote( uint16 proposalIndex, - uint32 voterIndex, + uint32 voteIndex, ProposalSingleVoteDataV1& vote ) const { - if (proposalIndex >= pv.maxProposals || voterIndex >= pv.maxVoters || !pv.proposals[proposalIndex].epoch) + if (proposalIndex >= pv.maxProposals || voteIndex >= pv.maxVotes || !pv.proposals[proposalIndex].epoch) + { + // vote.proposalType == 0 indicates error + vote.proposalType = 0; return false; + } vote.proposalIndex = proposalIndex; vote.proposalType = pv.proposals[proposalIndex].type; vote.proposalTick = pv.proposals[proposalIndex].tick; - vote.voteValue = pv.proposals[proposalIndex].getVoteValue(voterIndex); + vote.voteValue = pv.proposals[proposalIndex].getVoteValue(voteIndex); return true; } + template + bool QpiContextProposalFunctionCall::getVotes( + uint16 proposalIndex, + const id& voter, + ProposalMultiVoteDataV1& votes + ) const + { + // proposalType = 0 is an additional error indicator in votes (overwritten on success at the end of the function) + votes.proposalType = 0; + + if (proposalIndex >= pv.maxProposals || !pv.proposals[proposalIndex].epoch) + return false; + + auto& proposal = pv.proposals[proposalIndex]; + + // Return first vote index (which may be INVALID_VOTE_INDEX if voter has no right to vote) + unsigned int voteIndexBegin = pv.proposersAndVoters.getVoteIndex(qpi, voter, proposalIndex); + if (voteIndexBegin == INVALID_VOTE_INDEX) + return false; + + // Get count of votes that this voter can cast + unsigned int voteCountTotal = pv.proposersAndVoters.getVoteCount(qpi, voteIndexBegin, proposalIndex); + ASSERT(voteCountTotal >= 1); + + // Count votes of individual values + unsigned int voteIndex = voteIndexBegin; + const unsigned int voteIndexEnd = voteIndexBegin + voteCountTotal; + + if (proposal.type == ProposalTypes::VariableScalarMean) + { + // scalar voting -> histogram with arbitrary values + uint32 voteValueIdx = 0, uniqueVoteValues = 0; + QPI::HashMap valueIdx; + valueIdx.reset(); + votes.voteValues.setAll(0); + votes.voteCounts.setAll(0); + for (; voteIndex < voteIndexEnd; ++voteIndex) + { + sint64 voteValue = proposal.getVoteValue(voteIndex); + if (voteValue != NO_VOTE_VALUE) + { + if (!valueIdx.get(voteValue, voteValueIdx)) + { + voteValueIdx = uniqueVoteValues; + if (voteValueIdx >= votes.voteValues.capacity()) + return false; + valueIdx.set(voteValue, voteValueIdx); + votes.voteValues.set(voteValueIdx, voteValue); + ++uniqueVoteValues; + } + votes.voteCounts.set(voteValueIdx, votes.voteCounts.get(voteValueIdx) + 1); + } + } + } + else + { + // option voting -> compute histogram of option values + auto& hist = votes.voteCounts; + const uint16 optionCount = ProposalTypes::optionCount(proposal.type); + ASSERT(optionCount > 0); + ASSERT(optionCount <= hist.capacity()); + hist.setAll(0); + for (; voteIndex < voteIndexEnd; ++voteIndex) + { + sint64 value = proposal.getVoteValue(voteIndex); + if (value != NO_VOTE_VALUE && value >= 0 && value < optionCount) + { + hist.set(value, hist.get(value) + 1); + } + } + + votes.voteValues.setAll(0); + for (uint32 i = 0; i < optionCount; ++i) + votes.voteValues.set(i, i); + } + + votes.proposalIndex = proposalIndex; + votes.proposalType = proposal.type; + votes.proposalTick = proposal.tick; + + return true; + } // Compute voting summary of scalar votes - template + template bool __getVotingSummaryScalarVotes( - const ProposalWithAllVoteData& p, + const ProposalWithAllVoteData& p, ProposalSummarizedVotingDataV1& votingSummary ) { @@ -480,49 +891,49 @@ namespace QPI // scalar voting -> compute mean value of votes sint64 value; sint64 accumulation = 0; - if (p.variableScalar.maxValue > p.variableScalar.maxSupportedValue / maxVoters - || p.variableScalar.minValue < p.variableScalar.minSupportedValue / maxVoters) + if (p.variableScalar.maxValue > p.variableScalar.maxSupportedValue / maxVotes + || p.variableScalar.minValue < p.variableScalar.minSupportedValue / maxVotes) { // calculating mean in a way that avoids overflow of sint64 // algorithm based on https://stackoverflow.com/questions/56663116/how-to-calculate-average-of-int64-t sint64 acc2 = 0; - for (uint32 i = 0; i < maxVoters; ++i) + for (uint32 i = 0; i < maxVotes; ++i) { value = p.getVoteValue(i); if (value != NO_VOTE_VALUE) { - ++votingSummary.totalVotes; + ++votingSummary.totalVotesCasted; } } - if (votingSummary.totalVotes) + if (votingSummary.totalVotesCasted) { - for (uint32 i = 0; i < maxVoters; ++i) + for (uint32 i = 0; i < maxVotes; ++i) { value = p.getVoteValue(i); if (value != NO_VOTE_VALUE) { - accumulation += value / votingSummary.totalVotes; - acc2 += value % votingSummary.totalVotes; + accumulation += value / votingSummary.totalVotesCasted; + acc2 += value % votingSummary.totalVotesCasted; } } - acc2 /= votingSummary.totalVotes; + acc2 /= votingSummary.totalVotesCasted; accumulation += acc2; } } else { // compute mean the regular way (faster than above) - for (uint32 i = 0; i < maxVoters; ++i) + for (uint32 i = 0; i < maxVotes; ++i) { value = p.getVoteValue(i); if (value != NO_VOTE_VALUE) { - ++votingSummary.totalVotes; + ++votingSummary.totalVotesCasted; accumulation += value; } } - if (votingSummary.totalVotes) - accumulation /= votingSummary.totalVotes; + if (votingSummary.totalVotesCasted) + accumulation /= votingSummary.totalVotesCasted; } // make sure union is zeroed and set result @@ -533,9 +944,9 @@ namespace QPI } // Specialization of "Compute voting summary of scalar votes" for ProposalDataYesNo, which has no struct members about support scalar votes - template + template bool __getVotingSummaryScalarVotes( - const ProposalWithAllVoteData& p, + const ProposalWithAllVoteData& p, ProposalSummarizedVotingDataV1& votingSummary ) { @@ -549,15 +960,17 @@ namespace QPI ProposalSummarizedVotingDataV1& votingSummary ) const { + // totalVotesAuthorized = 0 is an additional error indicator in votes (overwritten on success at the end of the function) + votingSummary.totalVotesAuthorized = 0; + if (proposalIndex >= pv.maxProposals || !pv.proposals[proposalIndex].epoch) return false; - const ProposalWithAllVoteData& p = pv.proposals[proposalIndex]; + const ProposalWithAllVoteData& p = pv.proposals[proposalIndex]; votingSummary.proposalIndex = proposalIndex; votingSummary.optionCount = ProposalTypes::optionCount(p.type); votingSummary.proposalTick = p.tick; - votingSummary.authorizedVoters = pv.maxVoters; - votingSummary.totalVotes = 0; + votingSummary.totalVotesCasted = 0; if (p.type == ProposalTypes::VariableScalarMean) { @@ -572,17 +985,19 @@ namespace QPI ASSERT(votingSummary.optionCount <= votingSummary.optionVoteCount.capacity()); auto& hist = votingSummary.optionVoteCount; hist.setAll(0); - for (uint32 i = 0; i < pv.maxVoters; ++i) + for (uint32 i = 0; i < pv.maxVotes; ++i) { sint64 value = p.getVoteValue(i); if (value != NO_VOTE_VALUE && value >= 0 && value < votingSummary.optionCount) { - ++votingSummary.totalVotes; + ++votingSummary.totalVotesCasted; hist.set(value, hist.get(value) + 1); } } } + votingSummary.totalVotesAuthorized = pv.maxVotes; + return true; } @@ -606,22 +1021,36 @@ namespace QPI return pv.proposersAndVoters.getProposerId(qpi, proposalIndex); } - // Return voter index for given ID or INVALID_VOTER_INDEX if ID has no right to vote + // Return vote index for given ID or INVALID_VOTE_INDEX if ID has no right to vote. If the voter has multiple + // votes, this returns the first index. All votes of a voter are stored consecutively. template - uint32 QpiContextProposalFunctionCall::voterIndex( - const id& voterId + uint32 QpiContextProposalFunctionCall::voteIndex( + const id& voterId, + uint16 proposalIndex ) const { - return pv.proposersAndVoters.getVoterIndex(qpi, voterId); + return pv.proposersAndVoters.getVoteIndex(qpi, voterId, proposalIndex); } - // Return ID for given voter index or NULL_ID if index is invalid + // Return ID for given vote index or NULL_ID if index is invalid template id QpiContextProposalFunctionCall::voterId( - uint32 voterIndex + uint32 voteIndex, + uint16 proposalIndex + ) const + { + return pv.proposersAndVoters.getVoterId(qpi, voteIndex, proposalIndex); + } + + // Return count of votes of a voter if the first vote index is passed. Otherwise return the number of votes + // including this and the following indices. Returns 0 if an invalid index is passed. + template + uint32 QpiContextProposalFunctionCall::voteCount( + uint32 voteIndex, + uint16 proposalIndex ) const { - return pv.proposersAndVoters.getVoterId(qpi, voterIndex); + return pv.proposersAndVoters.getVoteCount(qpi, voteIndex, proposalIndex); } // Return next proposal index of proposals of given epoch (default: current epoch) diff --git a/src/contract_core/qpi_spectrum_impl.h b/src/contract_core/qpi_spectrum_impl.h index bfcfb46de..54ce7e377 100644 --- a/src/contract_core/qpi_spectrum_impl.h +++ b/src/contract_core/qpi_spectrum_impl.h @@ -33,14 +33,67 @@ bool QPI::QpiContextFunctionCall::getEntity(const m256i& id, QPI::Entity& entity } } -// Return reference to fee reserve of contract for changing its value (data stored in state of contract 0) -static long long& contractFeeReserve(unsigned int contractIndex) +// Return the amount in the fee reserve of the specified contract (data stored in state of contract 0). +static long long getContractFeeReserve(unsigned int contractIndex) { + contractStateLock[0].acquireRead(); + long long reserveAmount = ((Contract0State*)contractStates[0])->contractFeeReserves[contractIndex]; + contractStateLock[0].releaseRead(); + + return reserveAmount; +} + +// Set the amount in the fee reserve of the specified contract to a new value (data stored in state of contract 0). +// This also sets the contractStateChangeFlag of contract 0. +static void setContractFeeReserve(unsigned int contractIndex, long long newValue) +{ + contractStateLock[0].acquireWrite(); + contractStateChangeFlags[0] |= 1ULL; + ((Contract0State*)contractStates[0])->contractFeeReserves[contractIndex] = newValue; + contractStateLock[0].releaseWrite(); +} + +// Add the given amount to the amount in the fee reserve of the specified contract (data stored in state of contract 0). +// This also sets the contractStateChangeFlag of contract 0. +static void addToContractFeeReserve(unsigned int contractIndex, unsigned long long addAmount) +{ + contractStateLock[0].acquireWrite(); contractStateChangeFlags[0] |= 1ULL; - return ((Contract0State*)contractStates[0])->contractFeeReserves[contractIndex]; + if (addAmount > static_cast(INT64_MAX)) + addAmount = INT64_MAX; + ((Contract0State*)contractStates[0])->contractFeeReserves[contractIndex] = + math_lib::sadd(((Contract0State*)contractStates[0])->contractFeeReserves[contractIndex], static_cast(addAmount)); + contractStateLock[0].releaseWrite(); +} + +// Subtract the given amount from the amount in the fee reserve of the specified contract (data stored in state of contract 0). +// This also sets the contractStateChangeFlag of contract 0. +static void subtractFromContractFeeReserve(unsigned int contractIndex, unsigned long long subtractAmount) +{ + contractStateLock[0].acquireWrite(); + contractStateChangeFlags[0] |= 1ULL; + + long long negativeAddAmount; + // The smallest representable INT64 number is INT64_MIN = - INT64_MAX - 1 + if (subtractAmount > static_cast(INT64_MAX)) + negativeAddAmount = INT64_MIN; + else + negativeAddAmount = -1LL * static_cast(subtractAmount); + + ((Contract0State*)contractStates[0])->contractFeeReserves[contractIndex] = + math_lib::sadd(((Contract0State*)contractStates[0])->contractFeeReserves[contractIndex], negativeAddAmount); + contractStateLock[0].releaseWrite(); } -long long QPI::QpiContextProcedureCall::burn(long long amount) const +long long QPI::QpiContextFunctionCall::queryFeeReserve(unsigned int contractIndex) const +{ + if (contractIndex < 1 || contractIndex >= contractCount) + contractIndex = _currentContractIndex; + + return getContractFeeReserve(contractIndex); +} + +long long QPI::QpiContextProcedureCall::burn(long long amount, unsigned int contractIndexBurnedFor) const { if (amount < 0 || amount > MAX_AMOUNT) { @@ -54,6 +107,14 @@ long long QPI::QpiContextProcedureCall::burn(long long amount) const return -amount; } + if (contractIndexBurnedFor < 1 || contractIndexBurnedFor >= contractCount) + contractIndexBurnedFor = _currentContractIndex; + + if (contractError[contractIndexBurnedFor] == ContractErrorIPOFailed) + { + return -amount; + } + const long long remainingAmount = energy(index) - amount; if (remainingAmount < 0) @@ -63,20 +124,20 @@ long long QPI::QpiContextProcedureCall::burn(long long amount) const if (decreaseEnergy(index, amount)) { - contractStateLock[0].acquireWrite(); - contractFeeReserve(_currentContractIndex) += amount; - contractStateLock[0].releaseWrite(); + addToContractFeeReserve(contractIndexBurnedFor, amount); - const Burning burning = { _currentContractId , amount }; + const Burning burning = { _currentContractId , amount, contractIndexBurnedFor }; logger.logBurning(burning); } return remainingAmount; } -long long QPI::QpiContextProcedureCall::transfer(const m256i& destination, long long amount) const +long long QPI::QpiContextProcedureCall::__transfer(const m256i& destination, long long amount, unsigned char transferType) const { - if (contractCallbacksRunning & ContractCallbackPostIncomingTransfer) + // Transfer to contract is forbidden inside POST_INCOMING_TRANSFER to prevent nested callbacks + if (contractCallbacksRunning & ContractCallbackPostIncomingTransfer + && destination.u64._0 < contractCount && !destination.u64._1 && !destination.u64._2 && !destination.u64._3) { return INVALID_AMOUNT; } @@ -107,7 +168,7 @@ long long QPI::QpiContextProcedureCall::transfer(const m256i& destination, long if (!contractActionTracker.addQuTransfer(_currentContractId, destination, amount)) __qpiAbort(ContractErrorTooManyActions); - __qpiNotifyPostIncomingTransfer(_currentContractId, destination, amount, TransferType::qpiTransfer); + __qpiNotifyPostIncomingTransfer(_currentContractId, destination, amount, transferType); const QuTransfer quTransfer = { _currentContractId , destination , amount }; logger.logQuTransfer(quTransfer); @@ -116,6 +177,11 @@ long long QPI::QpiContextProcedureCall::transfer(const m256i& destination, long return remainingAmount; } +long long QPI::QpiContextProcedureCall::transfer(const m256i& destination, long long amount) const +{ + return __transfer(destination, amount, TransferType::qpiTransfer); +} + m256i QPI::QpiContextFunctionCall::nextId(const m256i& currentId) const { int index = spectrumIndex(currentId); diff --git a/src/contract_core/qpi_ticking_impl.h b/src/contract_core/qpi_ticking_impl.h index d64f043c1..668d81bfe 100644 --- a/src/contract_core/qpi_ticking_impl.h +++ b/src/contract_core/qpi_ticking_impl.h @@ -46,13 +46,21 @@ unsigned char QPI::QpiContextFunctionCall::second() const QPI::DateAndTime QPI::QpiContextFunctionCall::now() const { - QPI::DateAndTime result; - result.year = etalonTick.year; - result.month = etalonTick.month; - result.day = etalonTick.day; - result.hour = etalonTick.hour; - result.minute = etalonTick.minute; - result.second = etalonTick.second; - result.millisecond = etalonTick.millisecond; - return result; + return QPI::DateAndTime(etalonTick.year + 2000, etalonTick.month, etalonTick.day, + etalonTick.hour, etalonTick.minute, etalonTick.second, etalonTick.millisecond); +} + +m256i QPI::QpiContextFunctionCall::getPrevSpectrumDigest() const +{ + return etalonTick.prevSpectrumDigest; +} + +m256i QPI::QpiContextFunctionCall::getPrevUniverseDigest() const +{ + return etalonTick.prevUniverseDigest; +} + +m256i QPI::QpiContextFunctionCall::getPrevComputerDigest() const +{ + return etalonTick.prevComputerDigest; } \ No newline at end of file diff --git a/src/contract_core/qpi_trivial_impl.h b/src/contract_core/qpi_trivial_impl.h index 618ace814..c5ab09401 100644 --- a/src/contract_core/qpi_trivial_impl.h +++ b/src/contract_core/qpi_trivial_impl.h @@ -4,6 +4,7 @@ #pragma once #include "../contracts/qpi.h" +#include "../contracts/math_lib.h" #include "../platform/memory.h" #include "../platform/time.h" @@ -20,6 +21,26 @@ namespace QPI copyMem(&dst, &src, sizeof(dst)); } + template + inline void copyToBuffer(T1& dst, const T2& src, bool setTailToZero) + { + static_assert(sizeof(dst) >= sizeof(src), "Destination buffer must be at least the size of the source object."); + copyMem(&dst, &src, sizeof(src)); + if (sizeof(dst) > sizeof(src) && setTailToZero) + { + uint8* tailPtr = reinterpret_cast(&dst) + sizeof(src); + const uint64 tailSize = sizeof(dst) - sizeof(src); + setMem(tailPtr, tailSize, 0); + } + } + + template + inline void copyFromBuffer(T1& dst, const T2& src) + { + static_assert(sizeof(dst) <= sizeof(src), "Destination object must be at most the size of the source buffer."); + copyMem(&dst, &src, sizeof(dst)); + } + template inline void setMemory(T& dst, uint8 value) { @@ -88,3 +109,49 @@ m256i QPI::QpiContextFunctionCall::K12(const T& data) const return digest; } + +////////// +// safety multiplying a and b and then clamp + +inline static QPI::sint64 QPI::smul(QPI::sint64 a, QPI::sint64 b) +{ + return math_lib::smul(a, b); +} + +inline static QPI::uint64 QPI::smul(QPI::uint64 a, QPI::uint64 b) +{ + return math_lib::smul(a, b); +} + +inline static QPI::sint32 QPI::smul(QPI::sint32 a, QPI::sint32 b) +{ + return math_lib::smul(a, b); +} + +inline static QPI::uint32 QPI::smul(QPI::uint32 a, QPI::uint32 b) +{ + return math_lib::smul(a, b); +} + +////////// +// safety adding a and b and then clamp + +inline static QPI::sint64 QPI::sadd(QPI::sint64 a, QPI::sint64 b) +{ + return math_lib::sadd(a, b); +} + +inline static QPI::uint64 QPI::sadd(QPI::uint64 a, QPI::uint64 b) +{ + return math_lib::sadd(a, b); +} + +inline static QPI::sint32 QPI::sadd(QPI::sint32 a, QPI::sint32 b) +{ + return math_lib::sadd(a, b); +} + +inline static QPI::uint32 QPI::sadd(QPI::uint32 a, QPI::uint32 b) +{ + return math_lib::sadd(a, b); +} diff --git a/src/contracts/ComputorControlledFund.h b/src/contracts/ComputorControlledFund.h index 4927f5089..25d0029b3 100644 --- a/src/contracts/ComputorControlledFund.h +++ b/src/contracts/ComputorControlledFund.h @@ -1,5 +1,7 @@ using namespace QPI; +constexpr uint32 CCF_MAX_SUBSCRIPTIONS = 1024; + struct CCF2 { }; @@ -13,8 +15,8 @@ struct CCF : public ContractBase // and apply for funding multiple times. typedef ProposalDataYesNo ProposalDataT; - // Anyone can set a proposal, but only computors have right vote. - typedef ProposalByAnyoneVotingByComputors<100> ProposersAndVotersT; + // Only computors can set a proposal and vote. Up to 100 proposals are supported simultaneously. + typedef ProposalAndVotingByComputors<100> ProposersAndVotersT; // Proposal and voting storage type typedef ProposalVoting ProposalVotingT; @@ -35,7 +37,56 @@ struct CCF : public ContractBase typedef Array LatestTransfersT; -private: + // Subscription proposal data (for proposals being voted on) + struct SubscriptionProposalData + { + id proposerId; // ID of the proposer (for cancellation checks) + id destination; // ID of the destination + Array url; // URL of the subscription + uint8 weeksPerPeriod; // Number of weeks between payments (e.g., 1 for weekly, 4 for monthly) + Array _padding0; // Padding for alignment + Array _padding1; // Padding for alignment + uint32 numberOfPeriods; // Total number of periods (e.g., 12 for 12 periods) + uint64 amountPerPeriod; // Amount in Qubic per period + uint32 startEpoch; // Epoch when subscription should start + }; + + // Active subscription data (for accepted subscriptions) + struct SubscriptionData + { + id destination; // ID of the destination (used as key, one per destination) + Array url; // URL of the subscription + uint8 weeksPerPeriod; // Number of weeks between payments (e.g., 1 for weekly, 4 for monthly) + Array _padding1; // Padding for alignment + Array _padding2; // Padding for alignment + uint32 numberOfPeriods; // Total number of periods (e.g., 12 for 12 periods) + uint64 amountPerPeriod; // Amount in Qubic per period + uint32 startEpoch; // Epoch when subscription started (startEpoch >= proposal approval epoch) + sint32 currentPeriod; // Current period index (0-based, 0 to numberOfPeriods-1) + }; + + // Array to store subscription proposals, one per proposal slot (indexed by proposalIndex) + typedef Array SubscriptionProposalsT; + + // Array to store active subscriptions, indexed by destination ID + typedef Array ActiveSubscriptionsT; + + // Regular payment entry (similar to LatestTransfersEntry but for subscriptions) + struct RegularPaymentEntry + { + id destination; + Array url; + sint64 amount; + uint32 tick; + sint32 periodIndex; // Which period this payment is for (0-based) + bool success; + Array _padding0; + Array _padding1; + }; + + typedef Array RegularPaymentsT; + +protected: //---------------------------------------------------------------------------- // Define state ProposalVotingT proposals; @@ -45,6 +96,13 @@ struct CCF : public ContractBase uint32 setProposalFee; + RegularPaymentsT regularPayments; + + SubscriptionProposalsT subscriptionProposals; // Subscription proposals, one per proposal slot (indexed by proposalIndex) + ActiveSubscriptionsT activeSubscriptions; // Active subscriptions, identified by destination ID + + uint8 lastRegularPaymentsNextOverwriteIdx; + //---------------------------------------------------------------------------- // Define private procedures and functions with input and output @@ -53,10 +111,33 @@ struct CCF : public ContractBase //---------------------------------------------------------------------------- // Define public procedures and functions with input and output - typedef ProposalDataT SetProposal_input; - typedef Success_output SetProposal_output; + // Extended input for SetProposal that includes optional subscription data + struct SetProposal_input + { + ProposalDataT proposal; + // Optional subscription data (only used if isSubscription is true) + bit isSubscription; // Set to true if this is a subscription proposal + uint8 weeksPerPeriod; // Number of weeks between payments (e.g., 1 for weekly, 4 for monthly) + Array _padding0; // Padding for alignment + uint32 startEpoch; // Epoch when subscription starts + uint64 amountPerPeriod; // Amount per period (in Qubic) + uint32 numberOfPeriods; // Total number of periods + }; - PUBLIC_PROCEDURE(SetProposal) + struct SetProposal_output + { + uint16 proposalIndex; + }; + + struct SetProposal_locals + { + uint32 totalEpochsForSubscription; + sint32 subIndex; + SubscriptionProposalData subscriptionProposal; + ProposalDataT proposal; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(SetProposal) { if (qpi.invocationReward() < state.setProposalFee) { @@ -65,7 +146,7 @@ struct CCF : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.okay = false; + output.proposalIndex = INVALID_PROPOSAL_INDEX; return; } else if (qpi.invocationReward() > state.setProposalFee) @@ -78,19 +159,93 @@ struct CCF : public ContractBase qpi.burn(qpi.invocationReward()); // Check requirements for proposals in this contract - if (ProposalTypes::cls(input.type) != ProposalTypes::Class::Transfer) + if (ProposalTypes::cls(input.proposal.type) != ProposalTypes::Class::Transfer) { // Only transfer proposals are allowed // -> Cancel if epoch is not 0 (which means clearing the proposal) - if (input.epoch != 0) + if (input.proposal.epoch != 0) { - output.okay = false; + output.proposalIndex = INVALID_PROPOSAL_INDEX; + return; + } + } + + // Validate subscription data if provided + if (input.isSubscription) + { + // Validate start epoch + if (input.startEpoch < qpi.epoch()) + { + output.proposalIndex = INVALID_PROPOSAL_INDEX; + return; + } + + // Calculate total epochs for this subscription + // 1 week = 1 epoch + locals.totalEpochsForSubscription = input.numberOfPeriods * input.weeksPerPeriod; + + // Check against total allowed subscription time range + if (locals.totalEpochsForSubscription > 52) + { + output.proposalIndex = INVALID_PROPOSAL_INDEX; return; } } // Try to set proposal (checks originators rights and general validity of input proposal) - output.okay = qpi(state.proposals).setProposal(qpi.originator(), input); + output.proposalIndex = qpi(state.proposals).setProposal(qpi.originator(), input.proposal); + + // Handle subscription proposals + if (output.proposalIndex != INVALID_PROPOSAL_INDEX && input.isSubscription) + { + // If proposal is being cleared (epoch 0), clear the subscription proposal + if (input.proposal.epoch == 0) + { + // Check if this is a subscription proposal that can be canceled by the proposer + if (output.proposalIndex < state.subscriptionProposals.capacity()) + { + locals.subscriptionProposal = state.subscriptionProposals.get(output.proposalIndex); + // Only allow cancellation by the proposer + // The value of below condition should be always true, but set the else condition for safety + if (locals.subscriptionProposal.proposerId == qpi.originator()) + { + // Clear the subscription proposal + setMemory(locals.subscriptionProposal, 0); + state.subscriptionProposals.set(output.proposalIndex, locals.subscriptionProposal); + } + else + { + output.proposalIndex = INVALID_PROPOSAL_INDEX; + } + } + } + else + { + // Check if there's already an active subscription for this destination + // Only the proposer can create a new subscription proposal, but any valid proposer + // can propose changes to an existing subscription (which will be handled in END_EPOCH) + // For now, we allow the proposal to be created - it will overwrite the existing subscription if accepted + + // Store subscription proposal data in the array indexed by proposalIndex + locals.subscriptionProposal.proposerId = qpi.originator(); + locals.subscriptionProposal.destination = input.proposal.transfer.destination; + copyMemory(locals.subscriptionProposal.url, input.proposal.url); + locals.subscriptionProposal.weeksPerPeriod = input.weeksPerPeriod; + locals.subscriptionProposal.numberOfPeriods = input.numberOfPeriods; + locals.subscriptionProposal.amountPerPeriod = input.amountPerPeriod; + locals.subscriptionProposal.startEpoch = input.startEpoch; + state.subscriptionProposals.set(output.proposalIndex, locals.subscriptionProposal); + } + } + else if (output.proposalIndex != INVALID_PROPOSAL_INDEX && !input.isSubscription) + { + // Clear any subscription proposal at this index if it exists + if (output.proposalIndex >= 0 && output.proposalIndex < state.subscriptionProposals.capacity()) + { + setMemory(locals.subscriptionProposal, 0); + state.subscriptionProposals.set(output.proposalIndex, locals.subscriptionProposal); + } + } } @@ -138,20 +293,61 @@ struct CCF : public ContractBase struct GetProposal_input { + id subscriptionDestination; // Destination ID to look up active subscription (optional, can be zero) uint16 proposalIndex; }; struct GetProposal_output { bit okay; - uint8 _padding0[7]; - id proposerPubicKey; + bit hasSubscriptionProposal; // True if this proposal has subscription proposal data + bit hasActiveSubscription; // True if an active subscription was found for the destination + Array _padding0; + Array _padding1; + id proposerPublicKey; ProposalDataT proposal; + SubscriptionData subscription; // Active subscription data if found + SubscriptionProposalData subscriptionProposal; // Subscription proposal data if this is a subscription proposal + }; + + struct GetProposal_locals + { + sint32 subIndex; + SubscriptionData subscriptionData; + SubscriptionProposalData subscriptionProposalData; }; - PUBLIC_FUNCTION(GetProposal) + PUBLIC_FUNCTION_WITH_LOCALS(GetProposal) { - output.proposerPubicKey = qpi(state.proposals).proposerId(input.proposalIndex); + output.proposerPublicKey = qpi(state.proposals).proposerId(input.proposalIndex); output.okay = qpi(state.proposals).getProposal(input.proposalIndex, output.proposal); + output.hasSubscriptionProposal = false; + output.hasActiveSubscription = false; + + // Check if this proposal has subscription proposal data + if (input.proposalIndex < state.subscriptionProposals.capacity()) + { + locals.subscriptionProposalData = state.subscriptionProposals.get(input.proposalIndex); + if (!isZero(locals.subscriptionProposalData.proposerId)) + { + output.subscriptionProposal = locals.subscriptionProposalData; + output.hasSubscriptionProposal = true; + } + } + + // Look up active subscription by destination ID + if (!isZero(input.subscriptionDestination)) + { + for (locals.subIndex = 0; locals.subIndex < CCF_MAX_SUBSCRIPTIONS; ++locals.subIndex) + { + locals.subscriptionData = state.activeSubscriptions.get(locals.subIndex); + if (locals.subscriptionData.destination == input.subscriptionDestination && !isZero(locals.subscriptionData.destination)) + { + output.subscription = locals.subscriptionData; + output.hasActiveSubscription = true; + break; + } + } + } } @@ -187,7 +383,7 @@ struct CCF : public ContractBase { output.okay = qpi(state.proposals).getVote( input.proposalIndex, - qpi(state.proposals).voterIndex(input.voter), + qpi(state.proposals).voteIndex(input.voter), output.vote); } @@ -218,6 +414,15 @@ struct CCF : public ContractBase } + typedef NoData GetRegularPayments_input; + typedef RegularPaymentsT GetRegularPayments_output; + + PUBLIC_FUNCTION(GetRegularPayments) + { + output = state.regularPayments; + } + + typedef NoData GetProposalFee_input; struct GetProposalFee_output { @@ -238,6 +443,7 @@ struct CCF : public ContractBase REGISTER_USER_FUNCTION(GetVotingResults, 4); REGISTER_USER_FUNCTION(GetLatestTransfers, 5); REGISTER_USER_FUNCTION(GetProposalFee, 6); + REGISTER_USER_FUNCTION(GetRegularPayments, 7); REGISTER_USER_PROCEDURE(SetProposal, 1); REGISTER_USER_PROCEDURE(Vote, 2); @@ -252,19 +458,31 @@ struct CCF : public ContractBase struct END_EPOCH_locals { - sint32 proposalIndex; + sint32 proposalIndex, subIdx; ProposalDataT proposal; ProposalSummarizedVotingDataV1 results; LatestTransfersEntry transfer; + RegularPaymentEntry regularPayment; + SubscriptionData subscription; + SubscriptionProposalData subscriptionProposal; + id proposerPublicKey; + uint32 currentEpoch; + uint32 epochsSinceStart; + uint32 epochsPerPeriod; + sint32 periodIndex; + sint32 existingSubIdx; + bit isSubscription; }; END_EPOCH_WITH_LOCALS() { + locals.currentEpoch = qpi.epoch(); + // Analyze transfer proposal results // Iterate all proposals that were open for voting in this epoch ... locals.proposalIndex = -1; - while ((locals.proposalIndex = qpi(state.proposals).nextProposalIndex(locals.proposalIndex, qpi.epoch())) >= 0) + while ((locals.proposalIndex = qpi(state.proposals).nextProposalIndex(locals.proposalIndex, locals.currentEpoch)) >= 0) { if (!qpi(state.proposals).getProposal(locals.proposalIndex, locals.proposal)) continue; @@ -277,7 +495,7 @@ struct CCF : public ContractBase continue; // The total number of votes needs to be at least the quorum - if (locals.results.totalVotes < QUORUM) + if (locals.results.totalVotesCasted < QUORUM) continue; // The transfer option (1) must have more votes than the no-transfer option (0) @@ -285,21 +503,153 @@ struct CCF : public ContractBase continue; // Option for transfer has been accepted? - if (locals.results.optionVoteCount.get(1) > QUORUM / 2) + if (locals.results.optionVoteCount.get(1) > div(QUORUM, 2U)) { - // Prepare log entry and do transfer - locals.transfer.destination = locals.proposal.transfer.destination; - locals.transfer.amount = locals.proposal.transfer.amount; - locals.transfer.tick = qpi.tick(); - copyMemory(locals.transfer.url, locals.proposal.url); - locals.transfer.success = (qpi.transfer(locals.transfer.destination, locals.transfer.amount) >= 0); - - // Add log entry - state.latestTransfers.set(state.lastTransfersNextOverwriteIdx, locals.transfer); - state.lastTransfersNextOverwriteIdx = (state.lastTransfersNextOverwriteIdx + 1) & (state.latestTransfers.capacity() - 1); + // Check if this is a subscription proposal + locals.isSubscription = false; + if (locals.proposalIndex < state.subscriptionProposals.capacity()) + { + locals.subscriptionProposal = state.subscriptionProposals.get(locals.proposalIndex); + // Check if this slot has subscription proposal data (non-zero proposerId indicates valid entry) + if (!isZero(locals.subscriptionProposal.proposerId)) + { + locals.isSubscription = true; + } + } + + if (locals.isSubscription) + { + // Handle subscription proposal acceptance + // If amountPerPeriod is 0 or numberOfPeriods is 0, delete the subscription + if (locals.subscriptionProposal.amountPerPeriod == 0 || locals.subscriptionProposal.numberOfPeriods == 0 || locals.subscriptionProposal.weeksPerPeriod == 0) + { + // Find and delete the subscription by destination ID + locals.existingSubIdx = -1; + for (locals.subIdx = 0; locals.subIdx < CCF_MAX_SUBSCRIPTIONS; ++locals.subIdx) + { + locals.subscription = state.activeSubscriptions.get(locals.subIdx); + if (locals.subscription.destination == locals.subscriptionProposal.destination && !isZero(locals.subscription.destination)) + { + // Clear the subscription entry + setMemory(locals.subscription, 0); + state.activeSubscriptions.set(locals.subIdx, locals.subscription); + break; + } + } + } + else + { + // Find existing subscription by destination ID or find a free slot + locals.existingSubIdx = -1; + for (locals.subIdx = 0; locals.subIdx < CCF_MAX_SUBSCRIPTIONS; ++locals.subIdx) + { + locals.subscription = state.activeSubscriptions.get(locals.subIdx); + if (locals.subscription.destination == locals.subscriptionProposal.destination && !isZero(locals.subscription.destination)) + { + locals.existingSubIdx = locals.subIdx; + break; + } + // Track first free slot (zero destination) + if (locals.existingSubIdx == -1 && isZero(locals.subscription.destination)) + { + locals.existingSubIdx = locals.subIdx; + } + } + + // If found existing or free slot, update/create subscription + if (locals.existingSubIdx >= 0) + { + locals.subscription.destination = locals.subscriptionProposal.destination; + copyMemory(locals.subscription.url, locals.subscriptionProposal.url); + locals.subscription.weeksPerPeriod = locals.subscriptionProposal.weeksPerPeriod; + locals.subscription.numberOfPeriods = locals.subscriptionProposal.numberOfPeriods; + locals.subscription.amountPerPeriod = locals.subscriptionProposal.amountPerPeriod; + locals.subscription.startEpoch = locals.subscriptionProposal.startEpoch; // Use the start epoch from the proposal + locals.subscription.currentPeriod = -1; // Reset to -1, will be updated when first payment is made + state.activeSubscriptions.set(locals.existingSubIdx, locals.subscription); + } + } + + // Clear the subscription proposal + setMemory(locals.subscriptionProposal, 0); + state.subscriptionProposals.set(locals.proposalIndex, locals.subscriptionProposal); + } + else + { + // Regular one-time transfer (no subscription data) + locals.transfer.destination = locals.proposal.transfer.destination; + locals.transfer.amount = locals.proposal.transfer.amount; + locals.transfer.tick = qpi.tick(); + copyMemory(locals.transfer.url, locals.proposal.url); + locals.transfer.success = (qpi.transfer(locals.transfer.destination, locals.transfer.amount) >= 0); + + // Add log entry + state.latestTransfers.set(state.lastTransfersNextOverwriteIdx, locals.transfer); + state.lastTransfersNextOverwriteIdx = (state.lastTransfersNextOverwriteIdx + 1) & (state.latestTransfers.capacity() - 1); + } + } + } + } + + // Process active subscriptions for regular payments + // Iterate through all active subscriptions and check if payment is due + for (locals.subIdx = 0; locals.subIdx < CCF_MAX_SUBSCRIPTIONS; ++locals.subIdx) + { + locals.subscription = state.activeSubscriptions.get(locals.subIdx); + + // Skip invalid subscriptions (zero destination indicates empty slot) + if (isZero(locals.subscription.destination) || locals.subscription.numberOfPeriods == 0) + continue; + + // Calculate epochs per period (1 week = 1 epoch) + locals.epochsPerPeriod = locals.subscription.weeksPerPeriod; + + // Calculate how many epochs have passed since subscription started + if (locals.currentEpoch < locals.subscription.startEpoch) + continue; // Subscription hasn't started yet + + locals.epochsSinceStart = locals.currentEpoch - locals.subscription.startEpoch; + + // Calculate which period we should be in (0-based: 0 = first period, 1 = second period, etc.) + // At the start of each period, we make a payment for that period + // When startEpoch = 189 and currentEpoch = 189: epochsSinceStart = 0, periodIndex = 0 (first period) + // When startEpoch = 189 and currentEpoch = 190: epochsSinceStart = 1, periodIndex = 1 (second period) + locals.periodIndex = div(locals.epochsSinceStart, locals.epochsPerPeriod); + + // Check if we need to make a payment for the current period + // currentPeriod tracks the last period for which payment was made (or -1 if none) + // We make payment at the start of each period, so when periodIndex > currentPeriod + // For the first payment: currentPeriod = -1, periodIndex = 0, so we pay for period 0 + if (locals.periodIndex > locals.subscription.currentPeriod && locals.periodIndex < (sint32)locals.subscription.numberOfPeriods) + { + // Make payment for the current period + locals.regularPayment.destination = locals.subscription.destination; + locals.regularPayment.amount = locals.subscription.amountPerPeriod; + locals.regularPayment.tick = qpi.tick(); + locals.regularPayment.periodIndex = locals.periodIndex; + copyMemory(locals.regularPayment.url, locals.subscription.url); + locals.regularPayment.success = (qpi.transfer(locals.regularPayment.destination, locals.regularPayment.amount) >= 0); + + // Update subscription current period to the period we just paid for + locals.subscription.currentPeriod = locals.periodIndex; + state.activeSubscriptions.set(locals.subIdx, locals.subscription); + + // Add log entry + state.regularPayments.set(state.lastRegularPaymentsNextOverwriteIdx, locals.regularPayment); + state.lastRegularPaymentsNextOverwriteIdx = (uint8)mod(state.lastRegularPaymentsNextOverwriteIdx + 1, state.regularPayments.capacity()); + + // Check if subscription has expired (all periods completed) + if (locals.regularPayment.success && locals.subscription.currentPeriod >= (sint32)locals.subscription.numberOfPeriods - 1) + { + // Clear the subscription by zeroing out the entry (empty slot is indicated by zero destination) + setMemory(locals.subscription, 0); + state.activeSubscriptions.set(locals.subIdx, locals.subscription); } } } } }; + + + diff --git a/src/contracts/GeneralQuorumProposal.h b/src/contracts/GeneralQuorumProposal.h index 4055e1c1b..08debc592 100644 --- a/src/contracts/GeneralQuorumProposal.h +++ b/src/contracts/GeneralQuorumProposal.h @@ -279,7 +279,9 @@ struct GQMPROP : public ContractBase struct GetProposal_output { bit okay; - uint8 _padding0[7]; + Array _padding0; + Array _padding1; + Array _padding2; id proposerPubicKey; ProposalDataT proposal; }; @@ -316,7 +318,7 @@ struct GQMPROP : public ContractBase { output.okay = qpi(state.proposals).getVote( input.proposalIndex, - qpi(state.proposals).voterIndex(input.voter), + qpi(state.proposals).voteIndex(input.voter), output.vote); } @@ -394,7 +396,7 @@ struct GQMPROP : public ContractBase if (qpi(state.proposals).getVotingSummary(locals.proposalIndex, locals.results)) { // The total number of votes needs to be at least the quorum - if (locals.results.totalVotes >= QUORUM) + if (locals.results.totalVotesCasted >= QUORUM) { // Find most voted option locals.mostVotedOptionIndex = 0; @@ -410,7 +412,7 @@ struct GQMPROP : public ContractBase } // Option for changing status quo has been accepted? (option 0 is "no change") - if (locals.mostVotedOptionIndex > 0 && locals.mostVotedOptionVotes > QUORUM / 2) + if (locals.mostVotedOptionIndex > 0 && locals.mostVotedOptionVotes > div(QUORUM, 2U)) { // Set in revenueDonation table (cannot be done in END_EPOCH, because this may overwrite entries that // are still needed unchanged for this epoch for the revenue donation which is run after END_EPOCH) diff --git a/src/contracts/MsVault.h b/src/contracts/MsVault.h index f507978f0..488fa5437 100644 --- a/src/contracts/MsVault.h +++ b/src/contracts/MsVault.h @@ -6,12 +6,15 @@ constexpr uint64 MSVAULT_INITIAL_MAX_VAULTS = 131072ULL; // 2^17 constexpr uint64 MSVAULT_MAX_VAULTS = MSVAULT_INITIAL_MAX_VAULTS * X_MULTIPLIER; // MSVAULT asset name : 23727827095802701, using assetNameFromString("MSVAULT") utility in test_util.h static constexpr uint64 MSVAULT_ASSET_NAME = 23727827095802701; +constexpr uint64 MSVAULT_MAX_ASSET_TYPES = 8; // Max number of different asset types a vault can hold constexpr uint64 MSVAULT_REGISTERING_FEE = 5000000ULL; constexpr uint64 MSVAULT_RELEASE_FEE = 100000ULL; constexpr uint64 MSVAULT_RELEASE_RESET_FEE = 1000000ULL; constexpr uint64 MSVAULT_HOLDING_FEE = 500000ULL; constexpr uint64 MSVAULT_BURN_FEE = 0ULL; // Integer percentage from 1 -> 100 +constexpr uint64 MSVAULT_VOTE_FEE_CHANGE_FEE = 10000000ULL; // Deposit fee for adjusting other fees, and refund if shareholders +constexpr uint64 MSVAULT_REVOKE_FEE = 100ULL; // [TODO]: Turn this assert ON when MSVAULT_BURN_FEE > 0 //static_assert(MSVAULT_BURN_FEE > 0, "SC requires burning qu to operate, the burn fee must be higher than 0!"); @@ -25,19 +28,46 @@ struct MSVAULT2 struct MSVAULT : public ContractBase { public: + // Procedure Status Codes --- + // 0: GENEREAL_FAILURE + // 1: SUCCESS + // 2: FAILURE_INSUFFICIENT_FEE + // 3: FAILURE_INVALID_VAULT (Invalid vault ID or vault inactive) + // 4: FAILURE_NOT_AUTHORIZED (not owner, not shareholder, etc.) + // 5: FAILURE_INVALID_PARAMS (amount is 0, destination is NULL, etc.) + // 6: FAILURE_INSUFFICIENT_BALANCE + // 7: FAILURE_LIMIT_REACHED (max vaults, max asset types, etc.) + // 8: FAILURE_TRANSFER_FAILED + // 9: PENDING_APPROVAL + + struct AssetBalance + { + Asset asset; + uint64 balance; + }; + struct Vault { id vaultName; Array owners; Array releaseAmounts; Array releaseDestinations; - uint64 balance; + uint64 qubicBalance; uint8 numberOfOwners; uint8 requiredApprovals; bit isActive; }; - struct MsVaultFeeVote + struct VaultAssetPart + { + Array assetBalances; + uint8 numberOfAssetTypes; + Array releaseAssets; + Array releaseAssetAmounts; + Array releaseAssetDestinations; + }; + + struct MsVaultFeeVote { uint64 registeringFee; uint64 releaseFee; @@ -50,15 +80,19 @@ struct MSVAULT : public ContractBase struct MSVaultLogger { uint32 _contractIndex; - // 1: Invalid vault ID or vault inactive - // 2: Caller not an owner - // 3: Invalid parameters (e.g., amount=0, destination=NULL_ID) - // 4: Release successful - // 5: Insufficient balance - // 6: Release not fully approved - // 7: Reset release requests successful - uint32 _type; - uint64 vaultId; + // _type corresponds to Procedure Status Codes + // 0: GENEREAL_FAILURE + // 1: SUCCESS + // 2: FAILURE_INSUFFICIENT_FEE + // 3: FAILURE_INVALID_VAULT + // 4: FAILURE_NOT_AUTHORIZED + // 5: FAILURE_INVALID_PARAMS + // 6: FAILURE_INSUFFICIENT_BALANCE + // 7: FAILURE_LIMIT_REACHED + // 8: FAILURE_TRANSFER_FAILED + // 9: PENDING_APPROVAL + uint32 _type; + uint64 vaultId; id ownerID; uint64 amount; id destination; @@ -130,6 +164,16 @@ struct MSVAULT : public ContractBase uint64 result; }; + struct getManagedAssetBalance_input + { + Asset asset; + id owner; + }; + struct getManagedAssetBalance_output + { + sint64 balance; + }; + // Procedures and functions' structs struct registerVault_input { @@ -139,22 +183,25 @@ struct MSVAULT : public ContractBase }; struct registerVault_output { + uint64 status; }; struct registerVault_locals { + MSVaultLogger logger; uint64 ownerCount; uint64 i; sint64 ii; uint64 j; uint64 k; uint64 count; + uint64 found; sint64 slotIndex; Vault newVault; Vault tempVault; id proposedOwner; - + Vault newQubicVault; + VaultAssetPart newAssetVault; Array tempOwners; - resetReleaseRequests_input rr_in; resetReleaseRequests_output rr_out; resetReleaseRequests_locals rr_locals; @@ -166,13 +213,16 @@ struct MSVAULT : public ContractBase }; struct deposit_output { + uint64 status; }; struct deposit_locals { + MSVaultLogger logger; Vault vault; isValidVaultId_input iv_in; isValidVaultId_output iv_out; isValidVaultId_locals iv_locals; + uint64 amountToDeposit; }; struct releaseTo_input @@ -183,6 +233,7 @@ struct MSVAULT : public ContractBase }; struct releaseTo_output { + uint64 status; }; struct releaseTo_locals { @@ -218,6 +269,7 @@ struct MSVAULT : public ContractBase }; struct resetRelease_output { + uint64 status; }; struct resetRelease_locals { @@ -240,7 +292,7 @@ struct MSVAULT : public ContractBase isValidVaultId_locals iv_locals; }; - struct voteFeeChange_input + struct voteFeeChange_input { uint64 newRegisteringFee; uint64 newReleaseFee; @@ -251,9 +303,11 @@ struct MSVAULT : public ContractBase }; struct voteFeeChange_output { + uint64 status; }; struct voteFeeChange_locals { + MSVaultLogger logger; uint64 i; uint64 sumVote; bit needNewRecord; @@ -357,7 +411,7 @@ struct MSVAULT : public ContractBase uint64 burnedAmount; }; - struct getFees_input + struct getFees_input { }; struct getFees_output @@ -392,13 +446,238 @@ struct MSVAULT : public ContractBase uint64 requiredApprovals; }; + struct getFeeVotes_input + { + }; + struct getFeeVotes_output + { + uint64 status; + uint64 numberOfFeeVotes; + Array feeVotes; + }; + struct getFeeVotes_locals + { + uint64 i; + }; + + struct getFeeVotesOwner_input + { + }; + struct getFeeVotesOwner_output + { + uint64 status; + uint64 numberOfFeeVotes; + Array feeVotesOwner; + }; + struct getFeeVotesOwner_locals + { + uint64 i; + }; + + struct getFeeVotesScore_input + { + }; + struct getFeeVotesScore_output + { + uint64 status; + uint64 numberOfFeeVotes; + Array feeVotesScore; + }; + struct getFeeVotesScore_locals + { + uint64 i; + }; + + struct getUniqueFeeVotes_input + { + }; + struct getUniqueFeeVotes_output + { + uint64 status; + uint64 numberOfUniqueFeeVotes; + Array uniqueFeeVotes; + }; + struct getUniqueFeeVotes_locals + { + uint64 i; + }; + + struct getUniqueFeeVotesRanking_input + { + }; + struct getUniqueFeeVotesRanking_output + { + uint64 status; + uint64 numberOfUniqueFeeVotes; + Array uniqueFeeVotesRanking; + }; + struct getUniqueFeeVotesRanking_locals + { + uint64 i; + }; + + struct depositAsset_input + { + uint64 vaultId; + Asset asset; + uint64 amount; + }; + struct depositAsset_output + { + uint64 status; + }; + struct depositAsset_locals + { + MSVaultLogger logger; + Vault qubicVault; // Object for the Qubic-related part of the vault + VaultAssetPart assetVault; // Object for the Asset-related part of the vault + AssetBalance ab; + sint64 assetIndex; + uint64 i; + sint64 userAssetBalance; + sint64 tempShares; + sint64 transferResult; + sint64 transferedShares; + QX::TransferShareOwnershipAndPossession_input qx_in; + QX::TransferShareOwnershipAndPossession_output qx_out; + sint64 transferredNumberOfShares; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + }; + + struct revokeAssetManagementRights_input + { + Asset asset; + sint64 numberOfShares; + }; + struct revokeAssetManagementRights_output + { + sint64 transferredNumberOfShares; + uint64 status; + }; + struct revokeAssetManagementRights_locals + { + MSVaultLogger logger; + sint64 managedBalance; + sint64 result; + }; + + struct releaseAssetTo_input + { + uint64 vaultId; + Asset asset; + uint64 amount; + id destination; + }; + struct releaseAssetTo_output + { + uint64 status; + }; + struct releaseAssetTo_locals + { + Vault qubicVault; + VaultAssetPart assetVault; + MSVaultLogger logger; + sint64 ownerIndex; + uint64 approvals; + bit releaseApproved; + AssetBalance ab; + uint64 i; + sint64 assetIndex; + isOwnerOfVault_input io_in; + isOwnerOfVault_output io_out; + isOwnerOfVault_locals io_locals; + findOwnerIndexInVault_input fi_in; + findOwnerIndexInVault_output fi_out; + findOwnerIndexInVault_locals fi_locals; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + sint64 remainingShares; + sint64 releaseResult; + }; + + struct resetAssetRelease_input + { + uint64 vaultId; + }; + struct resetAssetRelease_output + { + uint64 status; + }; + struct resetAssetRelease_locals + { + Vault qubicVault; + VaultAssetPart assetVault; + sint64 ownerIndex; + MSVaultLogger logger; + isOwnerOfVault_input io_in; + isOwnerOfVault_output io_out; + isOwnerOfVault_locals io_locals; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + findOwnerIndexInVault_input fi_in; + findOwnerIndexInVault_output fi_out; + findOwnerIndexInVault_locals fi_locals; + uint64 i; + }; + + struct getAssetReleaseStatus_input + { + uint64 vaultId; + }; + struct getAssetReleaseStatus_output + { + uint64 status; + Array assets; + Array amounts; + Array destinations; + }; + struct getAssetReleaseStatus_locals + { + Vault qubicVault; + VaultAssetPart assetVault; + uint64 i; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + }; + + struct getVaultAssetBalances_input + { + uint64 vaultId; + }; + struct getVaultAssetBalances_output + { + uint64 status; + uint64 numberOfAssetTypes; + Array assetBalances; + }; + struct getVaultAssetBalances_locals + { + uint64 i; + Vault qubicVault; + VaultAssetPart assetVault; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + }; + struct END_EPOCH_locals { uint64 i; uint64 j; - Vault v; + uint64 k; + Vault qubicVault; + VaultAssetPart assetVault; sint64 amountToDistribute; uint64 feeToBurn; + AssetBalance ab; + QX::TransferShareOwnershipAndPossession_input qx_in; + QX::TransferShareOwnershipAndPossession_output qx_out; + id qxAdress; }; protected: @@ -426,6 +705,8 @@ struct MSVAULT : public ContractBase uint64 liveDepositFee; uint64 liveBurnFee; + Array vaultAssetParts; + // Helper Functions PRIVATE_FUNCTION_WITH_LOCALS(isValidVaultId) { @@ -473,68 +754,71 @@ struct MSVAULT : public ContractBase // Procedures and functions PUBLIC_PROCEDURE_WITH_LOCALS(registerVault) { - // [TODO]: Change this to - // if (qpi.invocationReward() < state.liveRegisteringFee) - if (qpi.invocationReward() < MSVAULT_REGISTERING_FEE) + output.status = 0; + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.vaultId = -1; // Not yet created + + if (qpi.invocationReward() < (sint64)state.liveRegisteringFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } + if (qpi.invocationReward() > (sint64)state.liveRegisteringFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveRegisteringFee); + } + locals.ownerCount = 0; for (locals.i = 0; locals.i < MSVAULT_MAX_OWNERS; locals.i = locals.i + 1) { locals.proposedOwner = input.owners.get(locals.i); if (locals.proposedOwner != NULL_ID) { - locals.tempOwners.set(locals.ownerCount, locals.proposedOwner); - locals.ownerCount = locals.ownerCount + 1; + // Check for duplicates + locals.found = false; + for (locals.j = 0; locals.j < locals.ownerCount; locals.j++) + { + if (locals.tempOwners.get(locals.j) == locals.proposedOwner) + { + locals.found = true; + break; + } + } + if (!locals.found) + { + locals.tempOwners.set(locals.ownerCount, locals.proposedOwner); + locals.ownerCount++; + } } } if (locals.ownerCount <= 1) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - if (locals.ownerCount > MSVAULT_MAX_OWNERS) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - for (locals.i = locals.ownerCount; locals.i < MSVAULT_MAX_OWNERS; locals.i = locals.i + 1) - { - locals.tempOwners.set(locals.i, NULL_ID); - } - - // Check if requiredApprovals is valid: must be <= numberOfOwners, > 1 + // requiredApprovals must be > 1 and <= numberOfOwners if (input.requiredApprovals <= 1 || input.requiredApprovals > locals.ownerCount) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // Find empty slot - locals.slotIndex = -1; - for (locals.ii = 0; locals.ii < MSVAULT_MAX_VAULTS; locals.ii++) - { - locals.tempVault = state.vaults.get(locals.ii); - if (!locals.tempVault.isActive && locals.tempVault.numberOfOwners == 0 && locals.tempVault.balance == 0) - { - locals.slotIndex = locals.ii; - break; - } - } - - if (locals.slotIndex == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - for (locals.i = 0; locals.i < locals.ownerCount; locals.i++) + // Check co-ownership limits + for (locals.i = 0; locals.i < locals.ownerCount; locals.i = locals.i + 1) { locals.proposedOwner = locals.tempOwners.get(locals.i); locals.count = 0; @@ -548,101 +832,408 @@ struct MSVAULT : public ContractBase if (locals.tempVault.owners.get(locals.k) == locals.proposedOwner) { locals.count++; - if (locals.count >= MSVAULT_MAX_COOWNER) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } } } } } + if (locals.count >= MSVAULT_MAX_COOWNER) + { + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 7; // FAILURE_LIMIT_REACHED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + } + + // Find empty slot + locals.slotIndex = -1; + for (locals.ii = 0; locals.ii < MSVAULT_MAX_VAULTS; locals.ii++) + { + locals.tempVault = state.vaults.get(locals.ii); + if (!locals.tempVault.isActive && locals.tempVault.numberOfOwners == 0 && locals.tempVault.qubicBalance == 0) + { + locals.slotIndex = locals.ii; + break; + } } - locals.newVault.vaultName = input.vaultName; - locals.newVault.numberOfOwners = (uint8)locals.ownerCount; - locals.newVault.requiredApprovals = (uint8)input.requiredApprovals; - locals.newVault.balance = 0; - locals.newVault.isActive = true; + if (locals.slotIndex == -1) + { + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 7; // FAILURE_LIMIT_REACHED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } - locals.rr_in.vault = locals.newVault; - resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); - locals.newVault = locals.rr_out.vault; + // Initialize the new vault + locals.newQubicVault.vaultName = input.vaultName; + locals.newQubicVault.numberOfOwners = (uint8)locals.ownerCount; + locals.newQubicVault.requiredApprovals = (uint8)input.requiredApprovals; + locals.newQubicVault.qubicBalance = 0; + locals.newQubicVault.isActive = true; + // Set owners for (locals.i = 0; locals.i < locals.ownerCount; locals.i++) { - locals.newVault.owners.set(locals.i, locals.tempOwners.get(locals.i)); + locals.newQubicVault.owners.set(locals.i, locals.tempOwners.get(locals.i)); + } + // Clear remaining owner slots + for (locals.i = locals.ownerCount; locals.i < MSVAULT_MAX_OWNERS; locals.i++) + { + locals.newQubicVault.owners.set(locals.i, NULL_ID); } - state.vaults.set((uint64)locals.slotIndex, locals.newVault); + // Reset release requests for both Qubic and Assets + locals.rr_in.vault = locals.newQubicVault; + resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); + locals.newQubicVault = locals.rr_out.vault; - // [TODO]: Change this to - //if (qpi.invocationReward() > state.liveRegisteringFee) - //{ - // qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.liveRegisteringFee); - // } - if (qpi.invocationReward() > MSVAULT_REGISTERING_FEE) + // Init the Asset part of the vault + locals.newAssetVault.numberOfAssetTypes = 0; + for (locals.i = 0; locals.i < locals.ownerCount; locals.i++) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - MSVAULT_REGISTERING_FEE); + locals.newAssetVault.releaseAssets.set(locals.i, { NULL_ID, 0 }); + locals.newAssetVault.releaseAssetAmounts.set(locals.i, 0); + locals.newAssetVault.releaseAssetDestinations.set(locals.i, NULL_ID); } + state.vaults.set((uint64)locals.slotIndex, locals.newQubicVault); + state.vaultAssetParts.set((uint64)locals.slotIndex, locals.newAssetVault); + state.numberOfActiveVaults++; - // [TODO]: Change this to - //state.totalRevenue += state.liveRegisteringFee; - state.totalRevenue += MSVAULT_REGISTERING_FEE; + state.totalRevenue += state.liveRegisteringFee; + output.status = 1; // SUCCESS + locals.logger.vaultId = locals.slotIndex; + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); } PUBLIC_PROCEDURE_WITH_LOCALS(deposit) { - locals.iv_in.vaultId = input.vaultId; - isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + output.status = 0; // FAILURE_GENERAL - if (!locals.iv_out.result) + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.vaultId = input.vaultId; + + if (qpi.invocationReward() < (sint64)state.liveDepositFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - locals.vault = state.vaults.get(input.vaultId); - if (!locals.vault.isActive) + // calculate the actual amount to deposit into the vault + locals.amountToDeposit = qpi.invocationReward() - state.liveDepositFee; + + // make sure the deposit amount is greater than zero + if (locals.amountToDeposit == 0) { + // The user only send the exact fee amount, with nothing left to deposit + // this is an invalid operation, so we refund everything qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - locals.vault.balance += qpi.invocationReward(); - state.vaults.set(input.vaultId, locals.vault); - } - - PUBLIC_PROCEDURE_WITH_LOCALS(releaseTo) + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.vault = state.vaults.get(input.vaultId); + if (!locals.vault.isActive) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // add the collected fee to the total revenue + state.totalRevenue += state.liveDepositFee; + + // add the remaining amount to the specified vault's balance + locals.vault.qubicBalance += locals.amountToDeposit; + + state.vaults.set(input.vaultId, locals.vault); + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(revokeAssetManagementRights) + { + // This procedure allows a user to revoke asset management rights from MsVault + // and transfer them back to QX, which is the default manager for trading. + + output.status = 0; // FAILURE_GENERAL + output.transferredNumberOfShares = 0; + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.amount = input.numberOfShares; + + if (qpi.invocationReward() < (sint64)MSVAULT_REVOKE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.transferredNumberOfShares = 0; + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)MSVAULT_REVOKE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)MSVAULT_REVOKE_FEE); + } + + // must transfer a positive number of shares. + if (input.numberOfShares <= 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.transferredNumberOfShares = 0; + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // Check if MsVault actually manages the specified number of shares for the caller. + locals.managedBalance = qpi.numberOfShares( + input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX } + ); + + if (locals.managedBalance < input.numberOfShares) + { + // The user is trying to revoke more shares than are managed by MsVault. + output.transferredNumberOfShares = 0; + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + else + { + // The balance check passed. Proceed to release the management rights. + locals.result = qpi.releaseShares( + input.asset, + qpi.invocator(), // owner + qpi.invocator(), // possessor + input.numberOfShares, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + MSVAULT_REVOKE_FEE + ); + + if (locals.result < 0) + { + output.transferredNumberOfShares = 0; + output.status = 8; // FAILURE_TRANSFER_FAILED + } + else + { + output.transferredNumberOfShares = input.numberOfShares; + output.status = 1; // SUCCESS + } + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + } + + PUBLIC_PROCEDURE_WITH_LOCALS(depositAsset) { - // [TODO]: Change this to - //if (qpi.invocationReward() > state.liveReleaseFee) - //{ - // qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.liveReleaseFee); - //} - if (qpi.invocationReward() > MSVAULT_RELEASE_FEE) + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.vaultId = input.vaultId; + locals.logger.amount = input.amount; + + if (qpi.invocationReward() < (sint64)state.liveDepositFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)state.liveDepositFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveDepositFee); + } + + locals.userAssetBalance = qpi.numberOfShares(input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX }); + + if (locals.userAssetBalance < (sint64)input.amount || input.amount == 0) + { + // User does not have enough shares, or is trying to deposit zero. Abort and refund the fee. + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // check if vault id is valid and the vault is active + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + if (!locals.iv_out.result) + { + output.status = 3; // FAILURE_INVALID_VAULT + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; // invalid vault id + } + + locals.qubicVault = state.vaults.get(input.vaultId); + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + if (!locals.qubicVault.isActive) + { + output.status = 3; // FAILURE_INVALID_VAULT + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; // vault is not active + } + + // check if the vault has room for a new asset type + locals.assetIndex = -1; + for (locals.i = 0; locals.i < locals.assetVault.numberOfAssetTypes; locals.i++) + { + locals.ab = locals.assetVault.assetBalances.get(locals.i); + if (locals.ab.asset.assetName == input.asset.assetName && locals.ab.asset.issuer == input.asset.issuer) + { + locals.assetIndex = locals.i; + break; + } + } + + // if the asset is new to this vault, check if there's an empty slot. + if (locals.assetIndex == -1 && locals.assetVault.numberOfAssetTypes >= MSVAULT_MAX_ASSET_TYPES) + { + // no more new asset + output.status = 7; // FAILURE_LIMIT_REACHED + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // All checks passed, now perform the transfer of ownership. + state.totalRevenue += state.liveDepositFee; + + locals.tempShares = qpi.numberOfShares( + input.asset, + { SELF, SELF_INDEX }, + { SELF, SELF_INDEX } + ); + + locals.qx_in.assetName = input.asset.assetName; + locals.qx_in.issuer = input.asset.issuer; + locals.qx_in.numberOfShares = input.amount; + locals.qx_in.newOwnerAndPossessor = SELF; + + locals.transferResult = qpi.transferShareOwnershipAndPossession(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), input.amount, SELF); + + if (locals.transferResult < 0) + { + output.status = 8; // FAILURE_TRANSFER_FAILED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.transferedShares = qpi.numberOfShares(input.asset, { SELF, SELF_INDEX }, { SELF, SELF_INDEX }) - locals.tempShares; + + if (locals.transferedShares != (sint64)input.amount) + { + output.status = 8; // FAILURE_TRANSFER_FAILED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // If the transfer succeeds, update the vault's internal accounting. + if (locals.assetIndex != -1) + { + // Asset type exists, update balance + locals.ab = locals.assetVault.assetBalances.get(locals.assetIndex); + locals.ab.balance += input.amount; + locals.assetVault.assetBalances.set(locals.assetIndex, locals.ab); + } + else { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - MSVAULT_RELEASE_FEE); + // Add the new asset type to the vault's balance list + locals.ab.asset = input.asset; + locals.ab.balance = input.amount; + locals.assetVault.assetBalances.set(locals.assetVault.numberOfAssetTypes, locals.ab); + locals.assetVault.numberOfAssetTypes++; } - // [TODO]: Change this to - //state.totalRevenue += state.liveReleaseFee; - state.totalRevenue += MSVAULT_RELEASE_FEE; + + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(releaseTo) + { + output.status = 0; // GENEREAL_FAILURE locals.logger._contractIndex = CONTRACT_INDEX; - locals.logger._type = 0; locals.logger.vaultId = input.vaultId; locals.logger.ownerID = qpi.invocator(); locals.logger.amount = input.amount; locals.logger.destination = input.destination; + if (qpi.invocationReward() < (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseFee); + } + + state.totalRevenue += state.liveReleaseFee; + locals.iv_in.vaultId = input.vaultId; isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); if (!locals.iv_out.result) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -651,7 +1242,9 @@ struct MSVAULT : public ContractBase if (!locals.vault.isActive) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -661,21 +1254,27 @@ struct MSVAULT : public ContractBase isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); if (!locals.io_out.result) { - locals.logger._type = 2; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } if (input.amount == 0 || input.destination == NULL_ID) { - locals.logger._type = 3; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } - if (locals.vault.balance < input.amount) + if (locals.vault.qubicBalance < input.amount) { - locals.logger._type = 5; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -688,9 +1287,9 @@ struct MSVAULT : public ContractBase locals.vault.releaseAmounts.set(locals.ownerIndex, input.amount); locals.vault.releaseDestinations.set(locals.ownerIndex, input.destination); + // Check for approvals locals.approvals = 0; - locals.totalOwners = (uint64)locals.vault.numberOfOwners; - for (locals.i = 0; locals.i < locals.totalOwners; locals.i++) + for (locals.i = 0; locals.i < (uint64)locals.vault.numberOfOwners; locals.i++) { if (locals.vault.releaseAmounts.get(locals.i) == input.amount && locals.vault.releaseDestinations.get(locals.i) == input.destination) @@ -708,10 +1307,10 @@ struct MSVAULT : public ContractBase if (locals.releaseApproved) { // Still need to re-check the balance before releasing funds - if (locals.vault.balance >= input.amount) + if (locals.vault.qubicBalance >= input.amount) { qpi.transfer(input.destination, input.amount); - locals.vault.balance -= input.amount; + locals.vault.qubicBalance -= input.amount; locals.rr_in.vault = locals.vault; resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); @@ -719,51 +1318,278 @@ struct MSVAULT : public ContractBase state.vaults.set(input.vaultId, locals.vault); - locals.logger._type = 4; + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } else { - locals.logger._type = 5; + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } } else { state.vaults.set(input.vaultId, locals.vault); - locals.logger._type = 6; + output.status = 9; // PENDING_APPROVAL + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } } - PUBLIC_PROCEDURE_WITH_LOCALS(resetRelease) + PUBLIC_PROCEDURE_WITH_LOCALS(releaseAssetTo) { - // [TODO]: Change this to - //if (qpi.invocationReward() > state.liveReleaseResetFee) - //{ - // qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.liveReleaseResetFee); - //} - if (qpi.invocationReward() > MSVAULT_RELEASE_RESET_FEE) + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.vaultId = input.vaultId; + locals.logger.ownerID = qpi.invocator(); + locals.logger.amount = input.amount; + locals.logger.destination = input.destination; + + if (qpi.invocationReward() < (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseFee); + } + + state.totalRevenue += state.liveReleaseFee; + + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.qubicVault = state.vaults.get(input.vaultId); + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + + if (!locals.qubicVault.isActive) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.io_in.vault = locals.qubicVault; + locals.io_in.ownerID = qpi.invocator(); + isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); + if (!locals.io_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (locals.qubicVault.qubicBalance < MSVAULT_REVOKE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (input.amount == 0 || input.destination == NULL_ID) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // Find the asset in the vault + locals.assetIndex = -1; + for (locals.i = 0; locals.i < locals.assetVault.numberOfAssetTypes; locals.i++) + { + locals.ab = locals.assetVault.assetBalances.get(locals.i); + if (locals.ab.asset.assetName == input.asset.assetName && locals.ab.asset.issuer == input.asset.issuer) + { + locals.assetIndex = locals.i; + break; + } + } + if (locals.assetIndex == -1) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; // Asset not found + LOG_INFO(locals.logger); + return; + } + + if (locals.assetVault.assetBalances.get(locals.assetIndex).balance < input.amount) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // Record the release request + locals.fi_in.vault = locals.qubicVault; + locals.fi_in.ownerID = qpi.invocator(); + findOwnerIndexInVault(qpi, state, locals.fi_in, locals.fi_out, locals.fi_locals); + locals.ownerIndex = locals.fi_out.index; + + locals.assetVault.releaseAssets.set(locals.ownerIndex, input.asset); + locals.assetVault.releaseAssetAmounts.set(locals.ownerIndex, input.amount); + locals.assetVault.releaseAssetDestinations.set(locals.ownerIndex, input.destination); + + // Check for approvals + locals.approvals = 0; + for (locals.i = 0; locals.i < (uint64)locals.qubicVault.numberOfOwners; locals.i++) + { + if (locals.assetVault.releaseAssetAmounts.get(locals.i) == input.amount && + locals.assetVault.releaseAssetDestinations.get(locals.i) == input.destination && + locals.assetVault.releaseAssets.get(locals.i).assetName == input.asset.assetName && + locals.assetVault.releaseAssets.get(locals.i).issuer == input.asset.issuer) + { + locals.approvals++; + } + } + + locals.releaseApproved = false; + if (locals.approvals >= (uint64)locals.qubicVault.requiredApprovals) + { + locals.releaseApproved = true; + } + + if (locals.releaseApproved) + { + // Re-check balance before transfer + if (locals.assetVault.assetBalances.get(locals.assetIndex).balance >= input.amount) + { + locals.remainingShares = qpi.transferShareOwnershipAndPossession( + input.asset.assetName, + input.asset.issuer, + SELF, // owner + SELF, // possessor + input.amount, + input.destination // new owner & possessor + ); + if (locals.remainingShares >= 0) + { + // Update internal asset balance + locals.ab = locals.assetVault.assetBalances.get(locals.assetIndex); + locals.ab.balance -= input.amount; + locals.assetVault.assetBalances.set(locals.assetIndex, locals.ab); + + // Release management rights from MsVault to QX for the recipient + locals.releaseResult = qpi.releaseShares( + input.asset, + input.destination, // new owner + input.destination, // new possessor + input.amount, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + MSVAULT_REVOKE_FEE + ); + + if (locals.releaseResult >= 0) + { + // Deduct the fee from the vault's balance upon success + locals.qubicVault.qubicBalance -= MSVAULT_REVOKE_FEE; + output.status = 1; // SUCCESS + } + else + { + // Log an error if management rights transfer fails + output.status = 8; // FAILURE_TRANSFER_FAILED + } + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + + // Reset all asset release requests + for (locals.i = 0; locals.i < MSVAULT_MAX_OWNERS; locals.i++) + { + locals.assetVault.releaseAssets.set(locals.i, { NULL_ID, 0 }); + locals.assetVault.releaseAssetAmounts.set(locals.i, 0); + locals.assetVault.releaseAssetDestinations.set(locals.i, NULL_ID); + } + } + else + { + output.status = 8; // FAILURE_TRANSFER_FAILED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + } + else + { + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + } + else { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - MSVAULT_RELEASE_RESET_FEE); + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + output.status = 9; // PENDING_APPROVAL + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); } - // [TODO]: Change this to - //state.totalRevenue += state.liveReleaseResetFee; - state.totalRevenue += MSVAULT_RELEASE_RESET_FEE; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(resetRelease) + { + output.status = 0; // GENEREAL_FAILURE locals.logger._contractIndex = CONTRACT_INDEX; - locals.logger._type = 0; locals.logger.vaultId = input.vaultId; locals.logger.ownerID = qpi.invocator(); locals.logger.amount = 0; locals.logger.destination = NULL_ID; + if (qpi.invocationReward() < (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + if (qpi.invocationReward() > (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseResetFee); + } + + state.totalRevenue += state.liveReleaseResetFee; + locals.iv_in.vaultId = input.vaultId; isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); if (!locals.iv_out.result) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -772,7 +1598,9 @@ struct MSVAULT : public ContractBase if (!locals.vault.isActive) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -782,7 +1610,9 @@ struct MSVAULT : public ContractBase isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); if (!locals.io_out.result) { - locals.logger._type = 2; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -797,121 +1627,231 @@ struct MSVAULT : public ContractBase state.vaults.set(input.vaultId, locals.vault); - locals.logger._type = 7; + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(resetAssetRelease) + { + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.vaultId = input.vaultId; + locals.logger.ownerID = qpi.invocator(); + locals.logger.amount = 0; + locals.logger.destination = NULL_ID; + + if (qpi.invocationReward() < (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + if (qpi.invocationReward() > (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseResetFee); + } + + state.totalRevenue += state.liveReleaseResetFee; + + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.qubicVault = state.vaults.get(input.vaultId); + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + + if (!locals.qubicVault.isActive) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.io_in.vault = locals.qubicVault; + locals.io_in.ownerID = qpi.invocator(); + isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); + if (!locals.io_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.fi_in.vault = locals.qubicVault; + locals.fi_in.ownerID = qpi.invocator(); + findOwnerIndexInVault(qpi, state, locals.fi_in, locals.fi_out, locals.fi_locals); + locals.ownerIndex = locals.fi_out.index; + + locals.assetVault.releaseAssets.set(locals.ownerIndex, { NULL_ID, 0 }); + locals.assetVault.releaseAssetAmounts.set(locals.ownerIndex, 0); + locals.assetVault.releaseAssetDestinations.set(locals.ownerIndex, NULL_ID); + + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } - // [TODO]: Uncomment this to enable live fee update PUBLIC_PROCEDURE_WITH_LOCALS(voteFeeChange) { - // locals.ish_in.candidate = qpi.invocator(); - // isShareHolder(qpi, state, locals.ish_in, locals.ish_out, locals.ish_locals); - // if (!locals.ish_out.result) - // { - // return; - // } - // - // qpi.transfer(qpi.invocator(), qpi.invocationReward()); - // locals.nShare = qpi.numberOfPossessedShares(MSVAULT_ASSET_NAME, id::zero(), qpi.invocator(), qpi.invocator(), MSVAULT_CONTRACT_INDEX, MSVAULT_CONTRACT_INDEX); - // - // locals.fs.registeringFee = input.newRegisteringFee; - // locals.fs.releaseFee = input.newReleaseFee; - // locals.fs.releaseResetFee = input.newReleaseResetFee; - // locals.fs.holdingFee = input.newHoldingFee; - // locals.fs.depositFee = input.newDepositFee; - // // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 - // //locals.fs.burnFee = input.burnFee; - // - // locals.needNewRecord = true; - // for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) - // { - // locals.currentAddr = state.feeVotesOwner.get(locals.i); - // locals.realScore = qpi.numberOfPossessedShares(MSVAULT_ASSET_NAME, id::zero(), locals.currentAddr, locals.currentAddr, MSVAULT_CONTRACT_INDEX, MSVAULT_CONTRACT_INDEX); - // state.feeVotesScore.set(locals.i, locals.realScore); - // if (locals.currentAddr == qpi.invocator()) - // { - // locals.needNewRecord = false; - // } - // } - // if (locals.needNewRecord) - // { - // state.feeVotes.set(state.feeVotesAddrCount, locals.fs); - // state.feeVotesOwner.set(state.feeVotesAddrCount, qpi.invocator()); - // state.feeVotesScore.set(state.feeVotesAddrCount, locals.nShare); - // state.feeVotesAddrCount = state.feeVotesAddrCount + 1; - // } - // - // locals.sumVote = 0; - // for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) - // { - // locals.sumVote = locals.sumVote + state.feeVotesScore.get(locals.i); - // } - // if (locals.sumVote < QUORUM) - // { - // return; - // } - // - // state.uniqueFeeVotesCount = 0; - // for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) - // { - // locals.currentVote = state.feeVotes.get(locals.i); - // locals.found = false; - // locals.uniqueIndex = 0; - // locals.j; - // for (locals.j = 0; locals.j < state.uniqueFeeVotesCount; locals.j = locals.j + 1) - // { - // locals.uniqueVote = state.uniqueFeeVotes.get(locals.j); - // if (locals.uniqueVote.registeringFee == locals.currentVote.registeringFee && - // locals.uniqueVote.releaseFee == locals.currentVote.releaseFee && - // locals.uniqueVote.releaseResetFee == locals.currentVote.releaseResetFee && - // locals.uniqueVote.holdingFee == locals.currentVote.holdingFee && - // locals.uniqueVote.depositFee == locals.currentVote.depositFee - // // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 - // //&& locals.uniqueVote.burnFee == locals.currentVote.burnFee - // ) - // { - // locals.found = true; - // locals.uniqueIndex = locals.j; - // break; - // } - // } - // if (locals.found) - // { - // locals.currentRank = state.uniqueFeeVotesRanking.get(locals.uniqueIndex); - // state.uniqueFeeVotesRanking.set(locals.uniqueIndex, locals.currentRank + state.feeVotesScore.get(locals.i)); - // } - // else - // { - // state.uniqueFeeVotes.set(state.uniqueFeeVotesCount, locals.currentVote); - // state.uniqueFeeVotesRanking.set(state.uniqueFeeVotesCount, state.feeVotesScore.get(locals.i)); - // state.uniqueFeeVotesCount = state.uniqueFeeVotesCount + 1; - // } - // } - // - // for (locals.i = 0; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) - // { - // if (state.uniqueFeeVotesRanking.get(locals.i) >= QUORUM) - // { - // state.liveRegisteringFee = state.uniqueFeeVotes.get(locals.i).registeringFee; - // state.liveReleaseFee = state.uniqueFeeVotes.get(locals.i).releaseFee; - // state.liveReleaseResetFee = state.uniqueFeeVotes.get(locals.i).releaseResetFee; - // state.liveHoldingFee = state.uniqueFeeVotes.get(locals.i).holdingFee; - // state.liveDepositFee = state.uniqueFeeVotes.get(locals.i).depositFee; - // // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 - // //state.liveBurnFee = state.uniqueFeeVotes.get(locals.i).burnFee; - - // state.feeVotesAddrCount = 0; - // state.uniqueFeeVotesCount = 0; - // return; - // } - // } + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + + if (qpi.invocationReward() < (sint64)MSVAULT_VOTE_FEE_CHANGE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.ish_in.candidate = qpi.invocator(); + isShareHolder(qpi, state, locals.ish_in, locals.ish_out, locals.ish_locals); + if (!locals.ish_out.result) + { + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + locals.nShare = qpi.numberOfShares({ NULL_ID, MSVAULT_ASSET_NAME }, AssetOwnershipSelect::byOwner(qpi.invocator()), AssetPossessionSelect::byPossessor(qpi.invocator())); + + locals.fs.registeringFee = input.newRegisteringFee; + locals.fs.releaseFee = input.newReleaseFee; + locals.fs.releaseResetFee = input.newReleaseResetFee; + locals.fs.holdingFee = input.newHoldingFee; + locals.fs.depositFee = input.newDepositFee; + // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 + //locals.fs.burnFee = input.burnFee; + + locals.needNewRecord = true; + for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + locals.currentAddr = state.feeVotesOwner.get(locals.i); + locals.realScore = qpi.numberOfShares({ NULL_ID, MSVAULT_ASSET_NAME }, AssetOwnershipSelect::byOwner(locals.currentAddr), AssetPossessionSelect::byPossessor(locals.currentAddr)); + state.feeVotesScore.set(locals.i, locals.realScore); + if (locals.currentAddr == qpi.invocator()) + { + locals.needNewRecord = false; + state.feeVotes.set(locals.i, locals.fs); // Update existing vote + } + } + if (locals.needNewRecord && state.feeVotesAddrCount < MSVAULT_MAX_FEE_VOTES) + { + state.feeVotes.set(state.feeVotesAddrCount, locals.fs); + state.feeVotesOwner.set(state.feeVotesAddrCount, qpi.invocator()); + state.feeVotesScore.set(state.feeVotesAddrCount, locals.nShare); + state.feeVotesAddrCount = state.feeVotesAddrCount + 1; + } + + locals.sumVote = 0; + for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + locals.sumVote = locals.sumVote + state.feeVotesScore.get(locals.i); + } + if (locals.sumVote < QUORUM) + { + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + state.uniqueFeeVotesCount = 0; + // Reset unique vote ranking + for (locals.i = 0; locals.i < MSVAULT_MAX_FEE_VOTES; locals.i = locals.i + 1) + { + state.uniqueFeeVotesRanking.set(locals.i, 0); + } + + for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + locals.currentVote = state.feeVotes.get(locals.i); + locals.found = false; + locals.uniqueIndex = 0; + for (locals.j = 0; locals.j < state.uniqueFeeVotesCount; locals.j = locals.j + 1) + { + locals.uniqueVote = state.uniqueFeeVotes.get(locals.j); + if (locals.uniqueVote.registeringFee == locals.currentVote.registeringFee && + locals.uniqueVote.releaseFee == locals.currentVote.releaseFee && + locals.uniqueVote.releaseResetFee == locals.currentVote.releaseResetFee && + locals.uniqueVote.holdingFee == locals.currentVote.holdingFee && + locals.uniqueVote.depositFee == locals.currentVote.depositFee + // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 + //&& locals.uniqueVote.burnFee == locals.currentVote.burnFee + ) + { + locals.found = true; + locals.uniqueIndex = locals.j; + break; + } + } + if (locals.found) + { + locals.currentRank = state.uniqueFeeVotesRanking.get(locals.uniqueIndex); + state.uniqueFeeVotesRanking.set(locals.uniqueIndex, locals.currentRank + state.feeVotesScore.get(locals.i)); + } + else if (state.uniqueFeeVotesCount < MSVAULT_MAX_FEE_VOTES) + { + state.uniqueFeeVotes.set(state.uniqueFeeVotesCount, locals.currentVote); + state.uniqueFeeVotesRanking.set(state.uniqueFeeVotesCount, state.feeVotesScore.get(locals.i)); + state.uniqueFeeVotesCount = state.uniqueFeeVotesCount + 1; + } + } + + for (locals.i = 0; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) + { + if (state.uniqueFeeVotesRanking.get(locals.i) >= QUORUM) + { + state.liveRegisteringFee = state.uniqueFeeVotes.get(locals.i).registeringFee; + state.liveReleaseFee = state.uniqueFeeVotes.get(locals.i).releaseFee; + state.liveReleaseResetFee = state.uniqueFeeVotes.get(locals.i).releaseResetFee; + state.liveHoldingFee = state.uniqueFeeVotes.get(locals.i).holdingFee; + state.liveDepositFee = state.uniqueFeeVotes.get(locals.i).depositFee; + // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 + //state.liveBurnFee = state.uniqueFeeVotes.get(locals.i).burnFee; + + state.feeVotesAddrCount = 0; + state.uniqueFeeVotesCount = 0; + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + } + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); } PUBLIC_FUNCTION_WITH_LOCALS(getVaults) { output.numberOfVaults = 0ULL; locals.count = 0ULL; - for (locals.i = 0ULL; locals.i < MSVAULT_MAX_VAULTS; locals.i++) + for (locals.i = 0ULL; locals.i < MSVAULT_MAX_VAULTS && locals.count < MSVAULT_MAX_COOWNER; locals.i++) { locals.v = state.vaults.get(locals.i); if (locals.v.isActive) @@ -956,6 +1896,33 @@ struct MSVAULT : public ContractBase output.status = 1ULL; } + PUBLIC_FUNCTION_WITH_LOCALS(getAssetReleaseStatus) + { + output.status = 0ULL; + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + return; // output.status = false + } + + locals.qubicVault = state.vaults.get(input.vaultId); + if (!locals.qubicVault.isActive) + { + return; // output.status = false + } + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + + for (locals.i = 0; locals.i < (uint64)locals.qubicVault.numberOfOwners; locals.i++) + { + output.assets.set(locals.i, locals.assetVault.releaseAssets.get(locals.i)); + output.amounts.set(locals.i, locals.assetVault.releaseAssetAmounts.get(locals.i)); + output.destinations.set(locals.i, locals.assetVault.releaseAssetDestinations.get(locals.i)); + } + output.status = 1ULL; + } + PUBLIC_FUNCTION_WITH_LOCALS(getBalanceOf) { output.status = 0ULL; @@ -972,7 +1939,32 @@ struct MSVAULT : public ContractBase { return; // output.status = false } - output.balance = locals.vault.balance; + output.balance = locals.vault.qubicBalance; + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getVaultAssetBalances) + { + output.status = 0ULL; + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + return; // output.status = false + } + + locals.qubicVault = state.vaults.get(input.vaultId); + if (!locals.qubicVault.isActive) + { + return; // output.status = false + } + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + output.numberOfAssetTypes = locals.assetVault.numberOfAssetTypes; + for (locals.i = 0; locals.i < locals.assetVault.numberOfAssetTypes; locals.i++) + { + output.assetBalances.set(locals.i, locals.assetVault.assetBalances.get(locals.i)); + } output.status = 1ULL; } @@ -1003,23 +1995,19 @@ struct MSVAULT : public ContractBase output.totalDistributedToShareholders = state.totalDistributedToShareholders; // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 //output.burnedAmount = state.burnedAmount; + output.burnedAmount = 0; } PUBLIC_FUNCTION(getFees) { - output.registeringFee = MSVAULT_REGISTERING_FEE; - output.releaseFee = MSVAULT_RELEASE_FEE; - output.releaseResetFee = MSVAULT_RELEASE_RESET_FEE; - output.holdingFee = MSVAULT_HOLDING_FEE; - output.depositFee = 0ULL; - // [TODO]: Change this to: - //output.registeringFee = state.liveRegisteringFee; - //output.releaseFee = state.liveReleaseFee; - //output.releaseResetFee = state.liveReleaseResetFee; - //output.holdingFee = state.liveHoldingFee; - //output.depositFee = state.liveDepositFee; + output.registeringFee = state.liveRegisteringFee; + output.releaseFee = state.liveReleaseFee; + output.releaseResetFee = state.liveReleaseResetFee; + output.holdingFee = state.liveHoldingFee; + output.depositFee = state.liveDepositFee; // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 //output.burnFee = state.liveBurnFee; + output.burnFee = MSVAULT_BURN_FEE; } PUBLIC_FUNCTION_WITH_LOCALS(getVaultOwners) @@ -1053,17 +2041,93 @@ struct MSVAULT : public ContractBase output.status = 1ULL; } - // [TODO]: Uncomment this to enable live fee update PUBLIC_FUNCTION_WITH_LOCALS(isShareHolder) { - // if (qpi.numberOfPossessedShares(MSVAULT_ASSET_NAME, id::zero(), input.candidate, input.candidate, MSVAULT_CONTRACT_INDEX, MSVAULT_CONTRACT_INDEX) > 0) - // { - // output.result = 1ULL; - // } - // else - // { - // output.result = 0ULL; - // } + if (qpi.numberOfShares({ NULL_ID, MSVAULT_ASSET_NAME }, AssetOwnershipSelect::byOwner(input.candidate), + AssetPossessionSelect::byPossessor(input.candidate)) > 0) + { + output.result = 1ULL; + } + else + { + output.result = 0ULL; + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(getFeeVotes) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + output.feeVotes.set(locals.i, state.feeVotes.get(locals.i)); + } + + output.numberOfFeeVotes = state.feeVotesAddrCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getFeeVotesOwner) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + output.feeVotesOwner.set(locals.i, state.feeVotesOwner.get(locals.i)); + } + output.numberOfFeeVotes = state.feeVotesAddrCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getFeeVotesScore) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + output.feeVotesScore.set(locals.i, state.feeVotesScore.get(locals.i)); + } + output.numberOfFeeVotes = state.feeVotesAddrCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getUniqueFeeVotes) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) + { + output.uniqueFeeVotes.set(locals.i, state.uniqueFeeVotes.get(locals.i)); + } + output.numberOfUniqueFeeVotes = state.uniqueFeeVotesCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION(getManagedAssetBalance) + { + // Get management rights balance the owner transferred to MsVault + output.balance = qpi.numberOfShares( + input.asset, + { input.owner, SELF_INDEX }, + { input.owner, SELF_INDEX } + ); + } + + PUBLIC_FUNCTION_WITH_LOCALS(getUniqueFeeVotesRanking) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) + { + output.uniqueFeeVotesRanking.set(locals.i, state.uniqueFeeVotesRanking.get(locals.i)); + } + output.numberOfUniqueFeeVotes = state.uniqueFeeVotesCount; + + output.status = 1ULL; } INITIALIZE() @@ -1082,47 +2146,77 @@ struct MSVAULT : public ContractBase END_EPOCH_WITH_LOCALS() { + locals.qxAdress = id(QX_CONTRACT_INDEX, 0, 0, 0); for (locals.i = 0ULL; locals.i < MSVAULT_MAX_VAULTS; locals.i++) { - locals.v = state.vaults.get(locals.i); - if (locals.v.isActive) + locals.qubicVault = state.vaults.get(locals.i); + if (locals.qubicVault.isActive) { - // [TODO]: Change this to - //if (locals.v.balance >= state.liveHoldingFee) - //{ - // locals.v.balance -= state.liveHoldingFee; - // state.totalRevenue += state.liveHoldingFee; - // state.vaults.set(locals.i, locals.v); - //} - if (locals.v.balance >= MSVAULT_HOLDING_FEE) + if (locals.qubicVault.qubicBalance >= state.liveHoldingFee) { - locals.v.balance -= MSVAULT_HOLDING_FEE; - state.totalRevenue += MSVAULT_HOLDING_FEE; - state.vaults.set(locals.i, locals.v); + locals.qubicVault.qubicBalance -= state.liveHoldingFee; + state.totalRevenue += state.liveHoldingFee; + state.vaults.set(locals.i, locals.qubicVault); } else { // Not enough funds to pay holding fee - if (locals.v.balance > 0) + if (locals.qubicVault.qubicBalance > 0) { - state.totalRevenue += locals.v.balance; + state.totalRevenue += locals.qubicVault.qubicBalance; + } + + // Temporarily disable this code block. To transfer back the assets to QX, we need to pay 100 QUs fee. + // But the SC itself does not have enough funds to do so. We will keep it under the SC, so if the stuck asset + // happens, we just assign it back manually through patches. As long as there are fees needed for releasing + // assets back to QX, the code block below is not applicable. + // locals.assetVault = state.vaultAssetParts.get(locals.i); + // for (locals.k = 0; locals.k < locals.assetVault.numberOfAssetTypes; locals.k++) + // { + // locals.ab = locals.assetVault.assetBalances.get(locals.k); + // if (locals.ab.balance > 0) + // { + // // Prepare the transfer request to QX + // locals.qx_in.assetName = locals.ab.asset.assetName; + // locals.qx_in.issuer = locals.ab.asset.issuer; + // locals.qx_in.numberOfShares = locals.ab.balance; + // locals.qx_in.newOwnerAndPossessor = locals.qxAdress; + + // INVOKE_OTHER_CONTRACT_PROCEDURE(QX, TransferShareOwnershipAndPossession, locals.qx_in, locals.qx_out, 0); + // } + // } + + locals.qubicVault.isActive = false; + locals.qubicVault.qubicBalance = 0; + locals.qubicVault.requiredApprovals = 0; + locals.qubicVault.vaultName = NULL_ID; + locals.qubicVault.numberOfOwners = 0; + for (locals.j = 0; locals.j < MSVAULT_MAX_OWNERS; locals.j++) + { + locals.qubicVault.owners.set(locals.j, NULL_ID); + locals.qubicVault.releaseAmounts.set(locals.j, 0); + locals.qubicVault.releaseDestinations.set(locals.j, NULL_ID); + } + + // clear asset release proposals + locals.assetVault.numberOfAssetTypes = 0; + for (locals.j = 0; locals.j < MSVAULT_MAX_ASSET_TYPES; locals.j++) + { + locals.assetVault.assetBalances.set(locals.j, { { NULL_ID, 0 }, 0 }); } - locals.v.isActive = false; - locals.v.balance = 0; - locals.v.requiredApprovals = 0; - locals.v.vaultName = NULL_ID; - locals.v.numberOfOwners = 0; for (locals.j = 0; locals.j < MSVAULT_MAX_OWNERS; locals.j++) { - locals.v.owners.set(locals.j, NULL_ID); - locals.v.releaseAmounts.set(locals.j, 0); - locals.v.releaseDestinations.set(locals.j, NULL_ID); + locals.assetVault.releaseAssets.set(locals.j, { NULL_ID, 0 }); + locals.assetVault.releaseAssetAmounts.set(locals.j, 0); + locals.assetVault.releaseAssetDestinations.set(locals.j, NULL_ID); } + if (state.numberOfActiveVaults > 0) { state.numberOfActiveVaults--; } - state.vaults.set(locals.i, locals.v); + state.vaults.set(locals.i, locals.qubicVault); + state.vaultAssetParts.set(locals.i, locals.assetVault); } } } @@ -1165,5 +2259,29 @@ struct MSVAULT : public ContractBase REGISTER_USER_FUNCTION(getVaultOwners, 11); REGISTER_USER_FUNCTION(isShareHolder, 12); REGISTER_USER_PROCEDURE(voteFeeChange, 13); + REGISTER_USER_FUNCTION(getFeeVotes, 14); + REGISTER_USER_FUNCTION(getFeeVotesOwner, 15); + REGISTER_USER_FUNCTION(getFeeVotesScore, 16); + REGISTER_USER_FUNCTION(getUniqueFeeVotes, 17); + REGISTER_USER_FUNCTION(getUniqueFeeVotesRanking, 18); + // New asset-related functions and procedures + REGISTER_USER_PROCEDURE(depositAsset, 19); + REGISTER_USER_PROCEDURE(releaseAssetTo, 20); + REGISTER_USER_PROCEDURE(resetAssetRelease, 21); + REGISTER_USER_FUNCTION(getVaultAssetBalances, 22); + REGISTER_USER_FUNCTION(getAssetReleaseStatus, 23); + REGISTER_USER_FUNCTION(getManagedAssetBalance, 24); + REGISTER_USER_PROCEDURE(revokeAssetManagementRights, 25); + + } + + PRE_ACQUIRE_SHARES() + { + output.requestedFee = 0; + output.allowTransfer = true; + } + + POST_ACQUIRE_SHARES() + { } }; diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 2fe184712..9e50602a2 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -572,6 +572,14 @@ struct NOST : public ContractBase } return ; } + if (QUOTTERY::checkValidQtryDateTime(locals.firstPhaseStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.firstPhaseEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.secondPhaseStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.secondPhaseEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.thirdPhaseStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.thirdPhaseEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.listingStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.cliffEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.vestingEndDate) == 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (input.stepOfVesting == 0 || input.stepOfVesting > 12 || input.TGE > 50 || input.threshold > 50 || input.indexOfProject >= state.numberOfCreatedProject) { @@ -630,7 +638,7 @@ struct NOST : public ContractBase if (qpi.invocationReward() > NOSTROMO_QX_TOKEN_ISSUANCE_FEE) { - qpi.transfer(qpi.invocator(), NOSTROMO_QX_TOKEN_ISSUANCE_FEE - qpi.invocationReward()); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_QX_TOKEN_ISSUANCE_FEE); } locals.tmpProject = state.projects.get(input.indexOfProject); @@ -684,14 +692,6 @@ struct NOST : public ContractBase locals.maxCap = state.fundaraisings.get(input.indexOfFundraising).requiredFunds + div(state.fundaraisings.get(input.indexOfFundraising).requiredFunds * state.fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); locals.minCap = state.fundaraisings.get(input.indexOfFundraising).requiredFunds - div(state.fundaraisings.get(input.indexOfFundraising).requiredFunds * state.fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); - if (state.fundaraisings.get(input.indexOfFundraising).raisedFunds + qpi.invocationReward() > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } if (state.numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) && locals.numberOfInvestedProjects >= NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) { if (qpi.invocationReward() > 0) @@ -754,6 +754,14 @@ struct NOST : public ContractBase if (locals.i < locals.numberOfInvestedProjects) { + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) { qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); @@ -771,6 +779,14 @@ struct NOST : public ContractBase } else { + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); @@ -843,6 +859,14 @@ struct NOST : public ContractBase if (locals.i < locals.numberOfInvestedProjects) { + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) { qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); @@ -860,6 +884,14 @@ struct NOST : public ContractBase } else { + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); @@ -886,6 +918,14 @@ struct NOST : public ContractBase } else if (locals.curDate >= state.fundaraisings.get(input.indexOfFundraising).thirdPhaseStartDate && locals.curDate < state.fundaraisings.get(input.indexOfFundraising).thirdPhaseEndDate) { + if (locals.tmpFundraising.raisedFunds + qpi.invocationReward() > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } state.investors.get(qpi.invocator(), state.tmpInvestedList); state.numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); @@ -923,6 +963,14 @@ struct NOST : public ContractBase } locals.tmpFundraising.raisedFunds += qpi.invocationReward(); } + else + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (locals.minCap <= locals.tmpFundraising.raisedFunds && locals.tmpFundraising.isCreatedToken == 0) { locals.input.assetName = state.projects.get(locals.tmpFundraising.indexOfProject).tokenName; @@ -1131,7 +1179,6 @@ struct NOST : public ContractBase { // success output.transferredNumberOfShares = input.numberOfShares; - qpi.transfer(id(QX_CONTRACT_INDEX, 0, 0, 0), state.transferRightsFee); if (qpi.invocationReward() > state.transferRightsFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.transferRightsFee); diff --git a/src/contracts/QBond.h b/src/contracts/QBond.h new file mode 100644 index 000000000..9874c8d33 --- /dev/null +++ b/src/contracts/QBond.h @@ -0,0 +1,1428 @@ +using namespace QPI; + +constexpr uint64 QBOND_MAX_EPOCH_COUNT = 1024ULL; +constexpr uint64 QBOND_MBOND_PRICE = 1000000ULL; +constexpr uint64 QBOND_MAX_QUEUE_SIZE = 10ULL; +constexpr uint64 QBOND_MIN_MBONDS_TO_STAKE = 10ULL; +constexpr sint64 QBOND_MBONDS_EMISSION = 1000000000LL; +constexpr uint64 QBOND_STAKE_LIMIT_PER_EPOCH = 1000000ULL; + +constexpr uint16 QBOND_START_EPOCH = 182; +constexpr uint16 QBOND_CYCLIC_START_EPOCH = 192; +constexpr uint16 QBOND_FULL_CYCLE_EPOCHS_AMOUNT = 53; + +constexpr uint64 QBOND_STAKE_FEE_PERCENT = 50; // 0.5% +constexpr uint64 QBOND_TRADE_FEE_PERCENT = 3; // 0.03% +constexpr uint64 QBOND_MBOND_TRANSFER_FEE = 100; + +constexpr uint64 QBOND_QVAULT_DISTRIBUTION_PERCENT = 9900; // 99% + +struct QBOND2 +{ +}; + +struct QBOND : public ContractBase +{ +public: + struct StakeEntry + { + id staker; + sint64 amount; + }; + + struct MBondInfo + { + uint64 name; + sint64 stakersAmount; + sint64 totalStaked; + }; + + struct Stake_input + { + sint64 quMillions; + }; + struct Stake_output + { + }; + + struct TransferMBondOwnershipAndPossession_input + { + id newOwnerAndPossessor; + sint64 epoch; + sint64 numberOfMBonds; + }; + struct TransferMBondOwnershipAndPossession_output + { + sint64 transferredMBonds; + }; + + struct AddAskOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct AddAskOrder_output + { + sint64 addedMBondsAmount; + }; + + struct RemoveAskOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct RemoveAskOrder_output + { + sint64 removedMBondsAmount; + }; + + struct AddBidOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct AddBidOrder_output + { + sint64 addedMBondsAmount; + }; + + struct RemoveBidOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct RemoveBidOrder_output + { + sint64 removedMBondsAmount; + }; + + struct BurnQU_input + { + sint64 amount; + }; + struct BurnQU_output + { + sint64 amount; + }; + + struct UpdateCFA_input + { + id user; + bit operation; // 0 to remove, 1 to add + }; + struct UpdateCFA_output + { + bit result; + }; + + struct GetFees_input + { + }; + struct GetFees_output + { + uint64 stakeFeePercent; + uint64 tradeFeePercent; + uint64 transferFee; + }; + + struct GetEarnedFees_input + { + }; + struct GetEarnedFees_output + { + uint64 stakeFees; + uint64 tradeFees; + }; + + struct GetInfoPerEpoch_input + { + sint64 epoch; + }; + struct GetInfoPerEpoch_output + { + uint64 stakersAmount; + sint64 totalStaked; + sint64 apy; + }; + + struct GetOrders_input + { + sint64 epoch; + sint64 askOrdersOffset; + sint64 bidOrdersOffset; + }; + struct GetOrders_output + { + struct Order + { + id owner; + sint64 epoch; + sint64 numberOfMBonds; + sint64 price; + }; + + Array askOrders; + Array bidOrders; + }; + + struct GetUserOrders_input + { + id owner; + sint64 askOrdersOffset; + sint64 bidOrdersOffset; + }; + struct GetUserOrders_output + { + struct Order + { + id owner; + sint64 epoch; + sint64 numberOfMBonds; + sint64 price; + }; + + Array askOrders; + Array bidOrders; + }; + + struct GetMBondsTable_input + { + }; + struct GetMBondsTable_output + { + struct TableEntry + { + sint64 epoch; + sint64 totalStakedQBond; + sint64 totalStakedQEarn; + uint64 apy; + }; + Array info; + }; + + struct GetUserMBonds_input + { + id owner; + }; + struct GetUserMBonds_output + { + sint64 totalMBondsAmount; + struct MBondEntity + { + sint64 epoch; + sint64 amount; + uint64 apy; + }; + Array mbonds; + }; + + struct GetCFA_input + { + }; + struct GetCFA_output + { + Array commissionFreeAddresses; + }; + +protected: + Array _stakeQueue; + HashMap _epochMbondInfoMap; + HashMap _userTotalStakedMap; + HashSet _commissionFreeAddresses; + uint64 _qearnIncomeAmount; + uint64 _totalEarnedAmount; + uint64 _earnedAmountFromTrade; + uint64 _distributedAmount; + id _adminAddress; + id _devAddress; + struct _Order + { + id owner; + sint64 epoch; + sint64 numberOfMBonds; + }; + Collection<_Order, 1048576> _askOrders; + Collection<_Order, 1048576> _bidOrders; + uint8 _cyclicMbondCounter; + + struct _NumberOfReservedMBonds_input + { + id owner; + sint64 epoch; + } _numberOfReservedMBonds_input; + + struct _NumberOfReservedMBonds_output + { + sint64 amount; + } _numberOfReservedMBonds_output; + + struct _NumberOfReservedMBonds_locals + { + sint64 elementIndex; + id mbondIdentity; + _Order order; + MBondInfo tempMbondInfo; + }; + + PRIVATE_FUNCTION_WITH_LOCALS(_NumberOfReservedMBonds) + { + output.amount = 0; + if (!state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if (input.epoch >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) input.epoch; + } + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX) + { + locals.order = state._askOrders.element(locals.elementIndex); + if (locals.order.epoch == input.epoch && locals.order.owner == input.owner) + { + output.amount += locals.order.numberOfMBonds; + } + + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + } + + struct Stake_locals + { + sint64 amountInQueue; + sint64 userMBondsAmount; + sint64 tempAmount; + uint64 counter; + sint64 amountToStake; + uint64 amountAndFee; + uint64 stakeLimitPerUser; + StakeEntry tempStakeEntry; + MBondInfo tempMbondInfo; + QEARN::lock_input lock_input; + QEARN::lock_output lock_output; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(Stake) + { + locals.amountAndFee = sadd(smul((uint64) input.quMillions, QBOND_MBOND_PRICE), div(smul(smul((uint64) input.quMillions, QBOND_MBOND_PRICE), QBOND_STAKE_FEE_PERCENT), 10000ULL)); + + if (input.quMillions <= 0 + || input.quMillions >= MAX_AMOUNT + || !state._epochMbondInfoMap.get(qpi.epoch(), locals.tempMbondInfo) + || qpi.invocationReward() < 0 + || (uint64) qpi.invocationReward() < locals.amountAndFee + || locals.tempMbondInfo.totalStaked + QBOND_MIN_MBONDS_TO_STAKE > QBOND_STAKE_LIMIT_PER_EPOCH) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if ((uint64) qpi.invocationReward() > locals.amountAndFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.amountAndFee); + } + + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), div(smul((uint64) input.quMillions, QBOND_MBOND_PRICE) * QBOND_STAKE_FEE_PERCENT, 10000ULL)); + } + else + { + state._totalEarnedAmount += div(smul((uint64) input.quMillions, QBOND_MBOND_PRICE) * QBOND_STAKE_FEE_PERCENT, 10000ULL); + } + + locals.amountInQueue = input.quMillions; + for (locals.counter = 0; locals.counter < QBOND_MAX_QUEUE_SIZE; locals.counter++) + { + if (state._stakeQueue.get(locals.counter).staker != NULL_ID) + { + locals.amountInQueue += state._stakeQueue.get(locals.counter).amount; + } + else + { + locals.stakeLimitPerUser = input.quMillions; + if (locals.tempMbondInfo.totalStaked + locals.amountInQueue > QBOND_STAKE_LIMIT_PER_EPOCH) + { + locals.stakeLimitPerUser = QBOND_STAKE_LIMIT_PER_EPOCH - locals.tempMbondInfo.totalStaked - (locals.amountInQueue - input.quMillions); + qpi.transfer(qpi.invocator(), (input.quMillions - locals.stakeLimitPerUser) * QBOND_MBOND_PRICE); + } + locals.tempStakeEntry.staker = qpi.invocator(); + locals.tempStakeEntry.amount = locals.stakeLimitPerUser; + state._stakeQueue.set(locals.counter, locals.tempStakeEntry); + break; + } + } + + if (locals.amountInQueue < QBOND_MIN_MBONDS_TO_STAKE) + { + return; + } + + locals.tempStakeEntry.staker = NULL_ID; + locals.tempStakeEntry.amount = 0; + locals.amountToStake = 0; + for (locals.counter = 0; locals.counter < QBOND_MAX_QUEUE_SIZE; locals.counter++) + { + if (state._stakeQueue.get(locals.counter).staker == NULL_ID) + { + break; + } + + if (state._userTotalStakedMap.get(state._stakeQueue.get(locals.counter).staker, locals.userMBondsAmount)) + { + state._userTotalStakedMap.replace(state._stakeQueue.get(locals.counter).staker, locals.userMBondsAmount + state._stakeQueue.get(locals.counter).amount); + } + else + { + state._userTotalStakedMap.set(state._stakeQueue.get(locals.counter).staker, state._stakeQueue.get(locals.counter).amount); + } + + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, state._stakeQueue.get(locals.counter).staker, state._stakeQueue.get(locals.counter).staker, SELF_INDEX, SELF_INDEX) <= 0) + { + locals.tempMbondInfo.stakersAmount++; + } + qpi.transferShareOwnershipAndPossession(locals.tempMbondInfo.name, SELF, SELF, SELF, state._stakeQueue.get(locals.counter).amount, state._stakeQueue.get(locals.counter).staker); + locals.amountToStake += state._stakeQueue.get(locals.counter).amount; + state._stakeQueue.set(locals.counter, locals.tempStakeEntry); + } + + locals.tempMbondInfo.totalStaked += locals.amountToStake; + state._epochMbondInfoMap.replace(qpi.epoch(), locals.tempMbondInfo); + + INVOKE_OTHER_CONTRACT_PROCEDURE(QEARN, lock, locals.lock_input, locals.lock_output, locals.amountToStake * QBOND_MBOND_PRICE); + } + + struct TransferMBondOwnershipAndPossession_locals + { + MBondInfo tempMbondInfo; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(TransferMBondOwnershipAndPossession) + { + if (input.numberOfMBonds >= MAX_AMOUNT || input.numberOfMBonds <= 0 || qpi.invocationReward() < QBOND_MBOND_TRANSFER_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if (qpi.invocationReward() > QBOND_MBOND_TRANSFER_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QBOND_MBOND_TRANSFER_FEE); + } + + state._numberOfReservedMBonds_input.epoch = input.epoch; + state._numberOfReservedMBonds_input.owner = qpi.invocator(); + CALL(_NumberOfReservedMBonds, state._numberOfReservedMBonds_input, state._numberOfReservedMBonds_output); + + if (state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo) + && qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) - state._numberOfReservedMBonds_output.amount < input.numberOfMBonds) + { + output.transferredMBonds = 0; + qpi.transfer(qpi.invocator(), QBOND_MBOND_TRANSFER_FEE); + } + else + { + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, input.newOwnerAndPossessor, input.newOwnerAndPossessor, SELF_INDEX, SELF_INDEX) <= 0) + { + locals.tempMbondInfo.stakersAmount++; + } + output.transferredMBonds = qpi.transferShareOwnershipAndPossession(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), input.numberOfMBonds, input.newOwnerAndPossessor) < 0 ? 0 : input.numberOfMBonds; + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) <= 0) + { + locals.tempMbondInfo.stakersAmount--; + } + state._epochMbondInfoMap.replace((uint16)input.epoch, locals.tempMbondInfo); + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), QBOND_MBOND_TRANSFER_FEE); + } + else + { + state._totalEarnedAmount += QBOND_MBOND_TRANSFER_FEE; + } + } + } + + struct AddAskOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + sint64 nextElementIndex; + sint64 fee; + _Order tempAskOrder; + _Order tempBidOrder; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(AddAskOrder) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (input.price <= 0 || input.price >= MAX_AMOUNT || input.numberOfMBonds <= 0 || input.numberOfMBonds >= MAX_AMOUNT || !state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + output.addedMBondsAmount = 0; + return; + } + + state._numberOfReservedMBonds_input.epoch = input.epoch; + state._numberOfReservedMBonds_input.owner = qpi.invocator(); + CALL(_NumberOfReservedMBonds, state._numberOfReservedMBonds_input, state._numberOfReservedMBonds_output); + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) - state._numberOfReservedMBonds_output.amount < input.numberOfMBonds) + { + output.addedMBondsAmount = 0; + return; + } + + output.addedMBondsAmount = input.numberOfMBonds; + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if (input.epoch >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) input.epoch; + } + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price > state._bidOrders.priority(locals.elementIndex)) + { + break; + } + + locals.tempBidOrder = state._bidOrders.element(locals.elementIndex); + if (input.numberOfMBonds <= locals.tempBidOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + qpi.invocator(), + qpi.invocator(), + input.numberOfMBonds, + locals.tempBidOrder.owner); + + locals.fee = div(input.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(qpi.invocator(), input.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) - locals.fee); + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), locals.fee); + } + else + { + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + + if (input.numberOfMBonds < locals.tempBidOrder.numberOfMBonds) + { + locals.tempBidOrder.numberOfMBonds -= input.numberOfMBonds; + state._bidOrders.replace(locals.elementIndex, locals.tempBidOrder); + } + else if (input.numberOfMBonds == locals.tempBidOrder.numberOfMBonds) + { + state._bidOrders.remove(locals.elementIndex); + } + return; + } + else if (input.numberOfMBonds > locals.tempBidOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + qpi.invocator(), + qpi.invocator(), + locals.tempBidOrder.numberOfMBonds, + locals.tempBidOrder.owner); + + locals.fee = div(locals.tempBidOrder.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(qpi.invocator(), locals.tempBidOrder.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) - locals.fee); + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), locals.fee); + } + else + { + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + locals.elementIndex = state._bidOrders.remove(locals.elementIndex); + input.numberOfMBonds -= locals.tempBidOrder.numberOfMBonds; + } + } + + if (state._askOrders.population(locals.mbondIdentity) == 0) + { + locals.tempAskOrder.epoch = input.epoch; + locals.tempAskOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempAskOrder.owner = qpi.invocator(); + state._askOrders.add(locals.mbondIdentity, locals.tempAskOrder, -input.price); + return; + } + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price < -state._askOrders.priority(locals.elementIndex)) + { + locals.tempAskOrder.epoch = input.epoch; + locals.tempAskOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempAskOrder.owner = qpi.invocator(); + state._askOrders.add(locals.mbondIdentity, locals.tempAskOrder, -input.price); + break; + } + else if (input.price == -state._askOrders.priority(locals.elementIndex)) + { + if (state._askOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + locals.tempAskOrder = state._askOrders.element(locals.elementIndex); + locals.tempAskOrder.numberOfMBonds += input.numberOfMBonds; + state._askOrders.replace(locals.elementIndex, locals.tempAskOrder); + break; + } + } + + if (state._askOrders.nextElementIndex(locals.elementIndex) == NULL_INDEX) + { + locals.tempAskOrder.epoch = input.epoch; + locals.tempAskOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempAskOrder.owner = qpi.invocator(); + state._askOrders.add(locals.mbondIdentity, locals.tempAskOrder, -input.price); + break; + } + + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + } + + struct RemoveAskOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + _Order order; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(RemoveAskOrder) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.removedMBondsAmount = 0; + + if (input.price <= 0 || input.price >= MAX_AMOUNT || input.numberOfMBonds <= 0 || input.numberOfMBonds >= MAX_AMOUNT || !state._epochMbondInfoMap.get((uint16) input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if (input.epoch >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) input.epoch; + } + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price == -state._askOrders.priority(locals.elementIndex) && state._askOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + if (state._askOrders.element(locals.elementIndex).numberOfMBonds <= input.numberOfMBonds) + { + output.removedMBondsAmount = state._askOrders.element(locals.elementIndex).numberOfMBonds; + state._askOrders.remove(locals.elementIndex); + } + else + { + locals.order = state._askOrders.element(locals.elementIndex); + locals.order.numberOfMBonds -= input.numberOfMBonds; + state._askOrders.replace(locals.elementIndex, locals.order); + output.removedMBondsAmount = input.numberOfMBonds; + } + break; + } + + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + } + + struct AddBidOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + sint64 fee; + _Order tempAskOrder; + _Order tempBidOrder; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(AddBidOrder) + { + if (qpi.invocationReward() < smul(input.numberOfMBonds, input.price) + || input.price <= 0 + || input.price >= MAX_AMOUNT + || input.numberOfMBonds <= 0 + || input.numberOfMBonds >= MAX_AMOUNT + || !state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + output.addedMBondsAmount = 0; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if (qpi.invocationReward() > smul(input.numberOfMBonds, input.price)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - smul(input.numberOfMBonds, input.price)); + } + + output.addedMBondsAmount = input.numberOfMBonds; + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if (input.epoch >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) input.epoch; + } + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price < -state._askOrders.priority(locals.elementIndex)) + { + break; + } + + locals.tempAskOrder = state._askOrders.element(locals.elementIndex); + if (input.numberOfMBonds <= locals.tempAskOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + locals.tempAskOrder.owner, + locals.tempAskOrder.owner, + input.numberOfMBonds, + qpi.invocator()); + + if (state._commissionFreeAddresses.getElementIndex(locals.tempAskOrder.owner) != NULL_INDEX) + { + qpi.transfer(locals.tempAskOrder.owner, -(input.numberOfMBonds * state._askOrders.priority(locals.elementIndex))); + } + else + { + locals.fee = div(input.numberOfMBonds * -state._askOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(locals.tempAskOrder.owner, -(input.numberOfMBonds * state._askOrders.priority(locals.elementIndex)) - locals.fee); + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + + if (input.price > -state._askOrders.priority(locals.elementIndex)) + { + qpi.transfer(qpi.invocator(), input.numberOfMBonds * (input.price + state._askOrders.priority(locals.elementIndex))); // ask orders priotiry is always negative + } + + if (input.numberOfMBonds < locals.tempAskOrder.numberOfMBonds) + { + locals.tempAskOrder.numberOfMBonds -= input.numberOfMBonds; + state._askOrders.replace(locals.elementIndex, locals.tempAskOrder); + } + else if (input.numberOfMBonds == locals.tempAskOrder.numberOfMBonds) + { + state._askOrders.remove(locals.elementIndex); + } + return; + } + else if (input.numberOfMBonds > locals.tempAskOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + locals.tempAskOrder.owner, + locals.tempAskOrder.owner, + locals.tempAskOrder.numberOfMBonds, + qpi.invocator()); + + if (state._commissionFreeAddresses.getElementIndex(locals.tempAskOrder.owner) != NULL_INDEX) + { + qpi.transfer(locals.tempAskOrder.owner, -(locals.tempAskOrder.numberOfMBonds * state._askOrders.priority(locals.elementIndex))); + } + else + { + locals.fee = div(locals.tempAskOrder.numberOfMBonds * -state._askOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(locals.tempAskOrder.owner, -(locals.tempAskOrder.numberOfMBonds * state._askOrders.priority(locals.elementIndex)) - locals.fee); + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + + if (input.price > -state._askOrders.priority(locals.elementIndex)) + { + qpi.transfer(qpi.invocator(), locals.tempAskOrder.numberOfMBonds * (input.price + state._askOrders.priority(locals.elementIndex))); // ask orders priotiry is always negative + } + + locals.elementIndex = state._askOrders.remove(locals.elementIndex); + input.numberOfMBonds -= locals.tempAskOrder.numberOfMBonds; + } + } + + if (state._bidOrders.population(locals.mbondIdentity) == 0) + { + locals.tempBidOrder.epoch = input.epoch; + locals.tempBidOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempBidOrder.owner = qpi.invocator(); + state._bidOrders.add(locals.mbondIdentity, locals.tempBidOrder, input.price); + return; + } + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price > state._bidOrders.priority(locals.elementIndex)) + { + locals.tempBidOrder.epoch = input.epoch; + locals.tempBidOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempBidOrder.owner = qpi.invocator(); + state._bidOrders.add(locals.mbondIdentity, locals.tempBidOrder, input.price); + break; + } + else if (input.price == state._bidOrders.priority(locals.elementIndex)) + { + if (state._bidOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + locals.tempBidOrder = state._bidOrders.element(locals.elementIndex); + locals.tempBidOrder.numberOfMBonds += input.numberOfMBonds; + state._bidOrders.replace(locals.elementIndex, locals.tempBidOrder); + break; + } + } + + if (state._bidOrders.nextElementIndex(locals.elementIndex) == NULL_INDEX) + { + locals.tempBidOrder.epoch = input.epoch; + locals.tempBidOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempBidOrder.owner = qpi.invocator(); + state._bidOrders.add(locals.mbondIdentity, locals.tempBidOrder, input.price); + break; + } + + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + } + } + + struct RemoveBidOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + _Order order; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(RemoveBidOrder) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.removedMBondsAmount = 0; + + if (input.price <= 0 || input.price >= MAX_AMOUNT || input.numberOfMBonds <= 0 || input.numberOfMBonds >= MAX_AMOUNT || !state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if (input.epoch >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) input.epoch; + } + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price == state._bidOrders.priority(locals.elementIndex) && state._bidOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + if (state._bidOrders.element(locals.elementIndex).numberOfMBonds <= input.numberOfMBonds) + { + output.removedMBondsAmount = state._bidOrders.element(locals.elementIndex).numberOfMBonds; + state._bidOrders.remove(locals.elementIndex); + } + else + { + locals.order = state._bidOrders.element(locals.elementIndex); + locals.order.numberOfMBonds -= input.numberOfMBonds; + state._bidOrders.replace(locals.elementIndex, locals.order); + output.removedMBondsAmount = input.numberOfMBonds; + } + qpi.transfer(qpi.invocator(), output.removedMBondsAmount * input.price); + break; + } + + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + } + } + + PUBLIC_PROCEDURE(BurnQU) + { + if (input.amount <= 0 || input.amount >= MAX_AMOUNT || qpi.invocationReward() < input.amount) + { + output.amount = -1; + if (input.amount == 0) + { + output.amount = 0; + } + + if (qpi.invocationReward() > 0 && qpi.invocationReward() < MAX_AMOUNT) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + if (qpi.invocationReward() > input.amount) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount); + } + + qpi.burn(input.amount); + output.amount = input.amount; + } + + PUBLIC_PROCEDURE(UpdateCFA) + { + if (qpi.invocationReward() > 0 && qpi.invocationReward() <= MAX_AMOUNT) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state._adminAddress) + { + return; + } + + if (input.operation == 0) + { + output.result = state._commissionFreeAddresses.remove(input.user); + } + else + { + output.result = state._commissionFreeAddresses.add(input.user); + } + } + + struct GetInfoPerEpoch_locals + { + sint64 index; + QEARN::getLockInfoPerEpoch_input tempInput; + QEARN::getLockInfoPerEpoch_output tempOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetInfoPerEpoch) + { + output.totalStaked = 0; + output.stakersAmount = 0; + output.apy = 0; + + locals.index = state._epochMbondInfoMap.getElementIndex((uint16)input.epoch); + + if (locals.index == NULL_INDEX) + { + return; + } + + locals.tempInput.Epoch = (uint32) input.epoch; + CALL_OTHER_CONTRACT_FUNCTION(QEARN, getLockInfoPerEpoch, locals.tempInput, locals.tempOutput); + + output.totalStaked = state._epochMbondInfoMap.value(locals.index).totalStaked; + output.stakersAmount = state._epochMbondInfoMap.value(locals.index).stakersAmount; + output.apy = locals.tempOutput.yield; + } + + PUBLIC_FUNCTION(GetFees) + { + output.stakeFeePercent = QBOND_STAKE_FEE_PERCENT; + output.tradeFeePercent = QBOND_TRADE_FEE_PERCENT; + output.transferFee = QBOND_MBOND_TRANSFER_FEE; + } + + PUBLIC_FUNCTION(GetEarnedFees) + { + output.stakeFees = state._totalEarnedAmount - state._earnedAmountFromTrade; + output.tradeFees = state._earnedAmountFromTrade; + } + + struct GetOrders_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + sint64 arrayElementIndex; + sint64 arrayElementIndex2; + sint64 startEpoch; + sint64 endEpoch; + sint64 epochCounter; + GetOrders_output::Order tempOrder; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetOrders) + { + if (input.epoch != 0 && !state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.arrayElementIndex = 0; + locals.arrayElementIndex2 = 0; + locals.mbondIdentity = SELF; + + if (input.epoch == 0) + { + locals.startEpoch = QBOND_START_EPOCH; + locals.endEpoch = qpi.epoch(); + } + else + { + locals.startEpoch = input.epoch; + locals.endEpoch = input.epoch; + } + + for (locals.epochCounter = locals.startEpoch; locals.epochCounter <= locals.endEpoch; locals.epochCounter++) + { + if (!state._epochMbondInfoMap.get((uint16)locals.epochCounter, locals.tempMbondInfo)) + { + continue; + } + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if (locals.epochCounter >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) locals.epochCounter; + } + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX && locals.arrayElementIndex < 256) + { + if (input.askOrdersOffset > 0) + { + input.askOrdersOffset--; + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + continue; + } + + locals.tempOrder.owner = state._askOrders.element(locals.elementIndex).owner; + locals.tempOrder.epoch = state._askOrders.element(locals.elementIndex).epoch; + locals.tempOrder.numberOfMBonds = state._askOrders.element(locals.elementIndex).numberOfMBonds; + locals.tempOrder.price = -state._askOrders.priority(locals.elementIndex); + output.askOrders.set(locals.arrayElementIndex, locals.tempOrder); + locals.arrayElementIndex++; + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX && locals.arrayElementIndex2 < 256) + { + if (input.bidOrdersOffset > 0) + { + input.bidOrdersOffset--; + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + continue; + } + + locals.tempOrder.owner = state._bidOrders.element(locals.elementIndex).owner; + locals.tempOrder.epoch = state._bidOrders.element(locals.elementIndex).epoch; + locals.tempOrder.numberOfMBonds = state._bidOrders.element(locals.elementIndex).numberOfMBonds; + locals.tempOrder.price = state._bidOrders.priority(locals.elementIndex); + output.bidOrders.set(locals.arrayElementIndex2, locals.tempOrder); + locals.arrayElementIndex2++; + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + } + } + } + + struct GetUserOrders_locals + { + sint64 epoch; + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex1; + sint64 arrayElementIndex1; + sint64 elementIndex2; + sint64 arrayElementIndex2; + GetUserOrders_output::Order tempOrder; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserOrders) + { + for (locals.epoch = QBOND_START_EPOCH; locals.epoch <= qpi.epoch(); locals.epoch++) + { + if (!state._epochMbondInfoMap.get((uint16)locals.epoch, locals.tempMbondInfo)) + { + continue; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if (locals.epoch >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) locals.epoch; + } + + locals.elementIndex1 = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex1 != NULL_INDEX && locals.arrayElementIndex1 < 256) + { + if (state._askOrders.element(locals.elementIndex1).owner != input.owner) + { + locals.elementIndex1 = state._askOrders.nextElementIndex(locals.elementIndex1); + continue; + } + + if (input.askOrdersOffset > 0) + { + input.askOrdersOffset--; + locals.elementIndex1 = state._askOrders.nextElementIndex(locals.elementIndex1); + continue; + } + + locals.tempOrder.owner = input.owner; + locals.tempOrder.epoch = state._askOrders.element(locals.elementIndex1).epoch; + locals.tempOrder.numberOfMBonds = state._askOrders.element(locals.elementIndex1).numberOfMBonds; + locals.tempOrder.price = -state._askOrders.priority(locals.elementIndex1); + output.askOrders.set(locals.arrayElementIndex1, locals.tempOrder); + locals.arrayElementIndex1++; + locals.elementIndex1 = state._askOrders.nextElementIndex(locals.elementIndex1); + } + + locals.elementIndex2 = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex2 != NULL_INDEX && locals.arrayElementIndex2 < 256) + { + if (state._bidOrders.element(locals.elementIndex2).owner != input.owner) + { + locals.elementIndex2 = state._bidOrders.nextElementIndex(locals.elementIndex2); + continue; + } + + if (input.bidOrdersOffset > 0) + { + input.bidOrdersOffset--; + locals.elementIndex2 = state._bidOrders.nextElementIndex(locals.elementIndex2); + continue; + } + + locals.tempOrder.owner = input.owner; + locals.tempOrder.epoch = state._bidOrders.element(locals.elementIndex2).epoch; + locals.tempOrder.numberOfMBonds = state._bidOrders.element(locals.elementIndex2).numberOfMBonds; + locals.tempOrder.price = state._bidOrders.priority(locals.elementIndex2); + output.bidOrders.set(locals.arrayElementIndex2, locals.tempOrder); + locals.arrayElementIndex2++; + locals.elementIndex2 = state._bidOrders.nextElementIndex(locals.elementIndex2); + } + } + } + + struct GetMBondsTable_locals + { + sint64 epoch; + sint64 index; + MBondInfo tempMBondInfo; + GetMBondsTable_output::TableEntry tempTableEntry; + QEARN::getLockInfoPerEpoch_input tempInput; + QEARN::getLockInfoPerEpoch_output tempOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetMBondsTable) + { + for (locals.epoch = QBOND_START_EPOCH; locals.epoch <= qpi.epoch(); locals.epoch++) + { + if (state._epochMbondInfoMap.get((uint16)locals.epoch, locals.tempMBondInfo)) + { + locals.tempInput.Epoch = (uint32) locals.epoch; + CALL_OTHER_CONTRACT_FUNCTION(QEARN, getLockInfoPerEpoch, locals.tempInput, locals.tempOutput); + locals.tempTableEntry.epoch = locals.epoch; + locals.tempTableEntry.totalStakedQBond = locals.tempMBondInfo.totalStaked * QBOND_MBOND_PRICE; + locals.tempTableEntry.totalStakedQEarn = locals.tempOutput.currentLockedAmount; + locals.tempTableEntry.apy = locals.tempOutput.yield; + output.info.set(locals.index, locals.tempTableEntry); + locals.index++; + } + } + } + + struct GetUserMBonds_locals + { + GetUserMBonds_output::MBondEntity tempMbondEntity; + sint64 epoch; + sint64 index; + sint64 mbondsAmount; + MBondInfo tempMBondInfo; + QEARN::getLockInfoPerEpoch_input tempInput; + QEARN::getLockInfoPerEpoch_output tempOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserMBonds) + { + output.totalMBondsAmount = 0; + if (state._userTotalStakedMap.get(input.owner, locals.mbondsAmount)) + { + output.totalMBondsAmount = locals.mbondsAmount; + } + + for (locals.epoch = QBOND_START_EPOCH; locals.epoch <= qpi.epoch(); locals.epoch++) + { + if (!state._epochMbondInfoMap.get((uint16)locals.epoch, locals.tempMBondInfo)) + { + continue; + } + + locals.mbondsAmount = qpi.numberOfPossessedShares(locals.tempMBondInfo.name, SELF, input.owner, input.owner, SELF_INDEX, SELF_INDEX); + if (locals.mbondsAmount <= 0) + { + continue; + } + + locals.tempInput.Epoch = (uint32) locals.epoch; + CALL_OTHER_CONTRACT_FUNCTION(QEARN, getLockInfoPerEpoch, locals.tempInput, locals.tempOutput); + + locals.tempMbondEntity.epoch = locals.epoch; + locals.tempMbondEntity.amount = locals.mbondsAmount; + locals.tempMbondEntity.apy = locals.tempOutput.yield; + output.mbonds.set(locals.index, locals.tempMbondEntity); + locals.index++; + } + } + + struct GetCFA_locals + { + sint64 index; + sint64 counter; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetCFA) + { + locals.index = state._commissionFreeAddresses.nextElementIndex(NULL_INDEX); + while (locals.index != NULL_INDEX) + { + output.commissionFreeAddresses.set(locals.counter, state._commissionFreeAddresses.key(locals.index)); + locals.counter++; + locals.index = state._commissionFreeAddresses.nextElementIndex(locals.index); + } + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(Stake, 1); + REGISTER_USER_PROCEDURE(TransferMBondOwnershipAndPossession, 2); + REGISTER_USER_PROCEDURE(AddAskOrder, 3); + REGISTER_USER_PROCEDURE(RemoveAskOrder, 4); + REGISTER_USER_PROCEDURE(AddBidOrder, 5); + REGISTER_USER_PROCEDURE(RemoveBidOrder, 6); + REGISTER_USER_PROCEDURE(BurnQU, 7); + REGISTER_USER_PROCEDURE(UpdateCFA, 8); + + REGISTER_USER_FUNCTION(GetFees, 1); + REGISTER_USER_FUNCTION(GetEarnedFees, 2); + REGISTER_USER_FUNCTION(GetInfoPerEpoch, 3); + REGISTER_USER_FUNCTION(GetOrders, 4); + REGISTER_USER_FUNCTION(GetUserOrders, 5); + REGISTER_USER_FUNCTION(GetMBondsTable, 6); + REGISTER_USER_FUNCTION(GetUserMBonds, 7); + REGISTER_USER_FUNCTION(GetCFA, 8); + } + + INITIALIZE() + { + state._devAddress = ID(_B, _O, _N, _D, _D, _J, _N, _U, _H, _O, _G, _Y, _L, _A, _A, _A, _C, _V, _X, _C, _X, _F, _G, _F, _R, _C, _S, _D, _C, _U, _W, _C, _Y, _U, _N, _K, _M, _P, _G, _O, _I, _F, _E, _P, _O, _E, _M, _Y, _T, _L, _Q, _L, _F, _C, _S, _B); + state._adminAddress = ID(_B, _O, _N, _D, _A, _A, _F, _B, _U, _G, _H, _E, _L, _A, _N, _X, _G, _H, _N, _L, _M, _S, _U, _I, _V, _B, _K, _B, _H, _A, _Y, _E, _Q, _S, _Q, _B, _V, _P, _V, _N, _B, _H, _L, _F, _J, _I, _A, _Z, _F, _Q, _C, _W, _W, _B, _V, _E); + state._commissionFreeAddresses.add(state._adminAddress); + } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } + + struct BEGIN_EPOCH_locals + { + sint8 chunk; + uint64 currentName; + StakeEntry emptyEntry; + sint64 totalReward; + sint64 rewardPerMBond; + Asset tempAsset; + MBondInfo tempMbondInfo; + AssetOwnershipIterator assetIt; + id mbondIdentity; + sint64 elementIndex; + uint64 counter; + _Order tempOrder; + }; + + BEGIN_EPOCH_WITH_LOCALS() + { + if (state._qearnIncomeAmount > 0 && state._epochMbondInfoMap.get((uint16) (qpi.epoch() - 53), locals.tempMbondInfo)) + { + locals.totalReward = state._qearnIncomeAmount - locals.tempMbondInfo.totalStaked * QBOND_MBOND_PRICE; + locals.rewardPerMBond = QPI::div(locals.totalReward, locals.tempMbondInfo.totalStaked); + + locals.tempAsset.assetName = locals.tempMbondInfo.name; + locals.tempAsset.issuer = SELF; + locals.assetIt.begin(locals.tempAsset); + while (!locals.assetIt.reachedEnd()) + { + if (locals.assetIt.owner() == SELF) + { + locals.assetIt.next(); + continue; + } + qpi.transfer(locals.assetIt.owner(), (QBOND_MBOND_PRICE + locals.rewardPerMBond) * locals.assetIt.numberOfOwnedShares()); + + if (qpi.epoch() - 53 < QBOND_CYCLIC_START_EPOCH) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + locals.assetIt.owner(), + locals.assetIt.owner(), + locals.assetIt.numberOfOwnedShares(), + NULL_ID); + } + else + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + locals.assetIt.owner(), + locals.assetIt.owner(), + locals.assetIt.numberOfOwnedShares(), + SELF); + } + + locals.assetIt.next(); + } + state._qearnIncomeAmount = 0; + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + if ((uint16) (qpi.epoch() - 53) >= QBOND_CYCLIC_START_EPOCH) + { + locals.mbondIdentity.u16._3 = (uint16) (qpi.epoch() - 53); + } + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + locals.elementIndex = state._askOrders.remove(locals.elementIndex); + } + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + locals.tempOrder = state._bidOrders.element(locals.elementIndex); + qpi.transfer(locals.tempOrder.owner, locals.tempOrder.numberOfMBonds * state._bidOrders.priority(locals.elementIndex)); + locals.elementIndex = state._bidOrders.remove(locals.elementIndex); + } + } + + if (state._cyclicMbondCounter >= QBOND_FULL_CYCLE_EPOCHS_AMOUNT) + { + state._cyclicMbondCounter = 1; + } + else + { + state._cyclicMbondCounter++; + } + + if (qpi.epoch() == QBOND_CYCLIC_START_EPOCH) + { + state._cyclicMbondCounter = 1; + for (locals.counter = 1; locals.counter <= QBOND_FULL_CYCLE_EPOCHS_AMOUNT; locals.counter++) + { + locals.currentName = 1145979469ULL; // MBND + + locals.chunk = (sint8) (48 + div(locals.counter, 10ULL)); + locals.currentName |= (uint64)locals.chunk << (4 * 8); + + locals.chunk = (sint8) (48 + mod(locals.counter, 10ULL)); + locals.currentName |= (uint64)locals.chunk << (5 * 8); + + qpi.issueAsset(locals.currentName, SELF, 0, QBOND_MBONDS_EMISSION, 0); + } + } + + locals.currentName = 1145979469ULL; // MBND + locals.chunk = (sint8) (48 + div(state._cyclicMbondCounter, (uint8) 10)); + locals.currentName |= (uint64)locals.chunk << (4 * 8); + + locals.chunk = (sint8) (48 + mod(state._cyclicMbondCounter, (uint8) 10)); + locals.currentName |= (uint64)locals.chunk << (5 * 8); + + locals.tempMbondInfo.name = locals.currentName; + locals.tempMbondInfo.totalStaked = 0; + locals.tempMbondInfo.stakersAmount = 0; + state._epochMbondInfoMap.set(qpi.epoch(), locals.tempMbondInfo); + + locals.emptyEntry.staker = NULL_ID; + locals.emptyEntry.amount = 0; + state._stakeQueue.setAll(locals.emptyEntry); + } + + struct POST_INCOMING_TRANSFER_locals + { + MBondInfo tempMbondInfo; + }; + + POST_INCOMING_TRANSFER_WITH_LOCALS() + { + if (input.sourceId == id(QEARN_CONTRACT_INDEX, 0, 0, 0) && state._epochMbondInfoMap.get(qpi.epoch() - 52, locals.tempMbondInfo)) + { + state._qearnIncomeAmount = input.amount; + } + } + + struct END_EPOCH_locals + { + sint64 availableMbonds; + MBondInfo tempMbondInfo; + sint64 counter; + StakeEntry tempStakeEntry; + sint64 amountToQvault; + sint64 amountToDev; + }; + + END_EPOCH_WITH_LOCALS() + { + locals.amountToQvault = div((state._totalEarnedAmount - state._distributedAmount) * QBOND_QVAULT_DISTRIBUTION_PERCENT, 10000ULL); + locals.amountToDev = state._totalEarnedAmount - state._distributedAmount - locals.amountToQvault; + qpi.transfer(id(QVAULT_CONTRACT_INDEX, 0, 0, 0), locals.amountToQvault); + qpi.transfer(state._devAddress, locals.amountToDev); + state._distributedAmount += locals.amountToQvault; + state._distributedAmount += locals.amountToDev; + + locals.tempStakeEntry.staker = NULL_ID; + locals.tempStakeEntry.amount = 0; + for (locals.counter = 0; locals.counter < QBOND_MAX_QUEUE_SIZE; locals.counter++) + { + if (state._stakeQueue.get(locals.counter).staker == NULL_ID) + { + break; + } + + qpi.transfer(state._stakeQueue.get(locals.counter).staker, state._stakeQueue.get(locals.counter).amount * QBOND_MBOND_PRICE); + state._stakeQueue.set(locals.counter, locals.tempStakeEntry); + } + + state._commissionFreeAddresses.cleanupIfNeeded(); + state._askOrders.cleanupIfNeeded(); + state._bidOrders.cleanupIfNeeded(); + } +}; diff --git a/src/contracts/QIP.h b/src/contracts/QIP.h new file mode 100644 index 000000000..722d94817 --- /dev/null +++ b/src/contracts/QIP.h @@ -0,0 +1,519 @@ +using namespace QPI; + +constexpr uint32 QIP_MAX_NUMBER_OF_ICO = 1024; + + +enum QIPLogInfo { + QIP_success = 0, + QIP_invalidStartEpoch = 1, + QIP_invalidSaleAmount = 2, + QIP_invalidPrice = 3, + QIP_invalidPercent = 4, + QIP_invalidTransfer = 5, + QIP_overflowICO = 6, + QIP_ICONotFound = 7, + QIP_invalidAmount = 8, + QIP_invalidEpoch = 9, + QIP_insufficientInvocationReward = 10, +}; + +struct QIPLogger +{ + uint32 _contractIndex; + uint32 _type; + id dst; + sint64 amt; + sint8 _terminator; +}; + +struct QIP2 +{ +}; + +struct QIP : public ContractBase +{ +public: + struct createICO_input + { + id issuer; + id address1, address2, address3, address4, address5, address6, address7, address8, address9, address10; + uint64 assetName; + uint64 price1; + uint64 price2; + uint64 price3; + uint64 saleAmountForPhase1; + uint64 saleAmountForPhase2; + uint64 saleAmountForPhase3; + uint32 percent1, percent2, percent3, percent4, percent5, percent6, percent7, percent8, percent9, percent10; + uint32 startEpoch; + }; + struct createICO_output + { + sint32 returnCode; + }; + + struct buyToken_input + { + uint32 indexOfICO; + uint64 amount; + }; + struct buyToken_output + { + sint32 returnCode; + }; + + struct TransferShareManagementRights_input + { + Asset asset; + sint64 numberOfShares; + uint32 newManagingContractIndex; + }; + struct TransferShareManagementRights_output + { + sint64 transferredNumberOfShares; + }; + + struct getICOInfo_input + { + uint32 indexOfICO; + }; + struct getICOInfo_output + { + id creatorOfICO; + id issuer; + id address1, address2, address3, address4, address5, address6, address7, address8, address9, address10; + uint64 assetName; + uint64 price1; + uint64 price2; + uint64 price3; + uint64 saleAmountForPhase1; + uint64 saleAmountForPhase2; + uint64 saleAmountForPhase3; + uint64 remainingAmountForPhase1; + uint64 remainingAmountForPhase2; + uint64 remainingAmountForPhase3; + uint32 percent1, percent2, percent3, percent4, percent5, percent6, percent7, percent8, percent9, percent10; + uint32 startEpoch; + }; + +protected: + + struct ICOInfo + { + id creatorOfICO; + id issuer; + id address1, address2, address3, address4, address5, address6, address7, address8, address9, address10; + uint64 assetName; + uint64 price1; + uint64 price2; + uint64 price3; + uint64 saleAmountForPhase1; + uint64 saleAmountForPhase2; + uint64 saleAmountForPhase3; + uint64 remainingAmountForPhase1; + uint64 remainingAmountForPhase2; + uint64 remainingAmountForPhase3; + uint32 percent1, percent2, percent3, percent4, percent5, percent6, percent7, percent8, percent9, percent10; + uint32 startEpoch; + }; + Array icos; + + uint32 numberOfICO; + uint32 transferRightsFee; +public: + + struct getICOInfo_locals + { + ICOInfo ico; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getICOInfo) + { + locals.ico = state.icos.get(input.indexOfICO); + output.creatorOfICO = locals.ico.creatorOfICO; + output.issuer = locals.ico.issuer; + output.address1 = locals.ico.address1; + output.address2 = locals.ico.address2; + output.address3 = locals.ico.address3; + output.address4 = locals.ico.address4; + output.address5 = locals.ico.address5; + output.address6 = locals.ico.address6; + output.address7 = locals.ico.address7; + output.address8 = locals.ico.address8; + output.address9 = locals.ico.address9; + output.address10 = locals.ico.address10; + output.assetName = locals.ico.assetName; + output.price1 = locals.ico.price1; + output.price2 = locals.ico.price2; + output.price3 = locals.ico.price3; + output.saleAmountForPhase1 = locals.ico.saleAmountForPhase1; + output.saleAmountForPhase2 = locals.ico.saleAmountForPhase2; + output.saleAmountForPhase3 = locals.ico.saleAmountForPhase3; + output.remainingAmountForPhase1 = locals.ico.remainingAmountForPhase1; + output.remainingAmountForPhase2 = locals.ico.remainingAmountForPhase2; + output.remainingAmountForPhase3 = locals.ico.remainingAmountForPhase3; + output.percent1 = locals.ico.percent1; + output.percent2 = locals.ico.percent2; + output.percent3 = locals.ico.percent3; + output.percent4 = locals.ico.percent4; + output.percent5 = locals.ico.percent5; + output.percent6 = locals.ico.percent6; + output.percent7 = locals.ico.percent7; + output.percent8 = locals.ico.percent8; + output.percent9 = locals.ico.percent9; + output.percent10 = locals.ico.percent10; + output.startEpoch = locals.ico.startEpoch; + } + + struct createICO_locals + { + ICOInfo newICO; + QIPLogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(createICO) + { + if (input.startEpoch <= (uint32)qpi.epoch() + 1) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidStartEpoch; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidStartEpoch; + return; + } + if (input.saleAmountForPhase1 + input.saleAmountForPhase2 + input.saleAmountForPhase3 != qpi.numberOfPossessedShares(input.assetName, input.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX)) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidSaleAmount; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidSaleAmount; + return; + } + if (input.price1 <= 0 || input.price2 <= 0 || input.price3 <= 0) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidPrice; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidPrice; + return; + } + if (input.percent1 + input.percent2 + input.percent3 + input.percent4 + input.percent5 + input.percent6 + input.percent7 + input.percent8 + input.percent9 + input.percent10 != 95) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidPercent; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidPercent; + return; + } + if (qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, qpi.invocator(), qpi.invocator(), input.saleAmountForPhase1 + input.saleAmountForPhase2 + input.saleAmountForPhase3, SELF) < 0) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidTransfer; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidTransfer; + return; + } + if (state.numberOfICO >= QIP_MAX_NUMBER_OF_ICO) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_overflowICO; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_overflowICO; + return; + } + locals.newICO.creatorOfICO = qpi.invocator(); + locals.newICO.issuer = input.issuer; + locals.newICO.address1 = input.address1; + locals.newICO.address2 = input.address2; + locals.newICO.address3 = input.address3; + locals.newICO.address4 = input.address4; + locals.newICO.address5 = input.address5; + locals.newICO.address6 = input.address6; + locals.newICO.address7 = input.address7; + locals.newICO.address8 = input.address8; + locals.newICO.address9 = input.address9; + locals.newICO.address10 = input.address10; + locals.newICO.assetName = input.assetName; + locals.newICO.price1 = input.price1; + locals.newICO.price2 = input.price2; + locals.newICO.price3 = input.price3; + locals.newICO.saleAmountForPhase1 = input.saleAmountForPhase1; + locals.newICO.saleAmountForPhase2 = input.saleAmountForPhase2; + locals.newICO.saleAmountForPhase3 = input.saleAmountForPhase3; + locals.newICO.remainingAmountForPhase1 = input.saleAmountForPhase1; + locals.newICO.remainingAmountForPhase2 = input.saleAmountForPhase2; + locals.newICO.remainingAmountForPhase3 = input.saleAmountForPhase3; + locals.newICO.percent1 = input.percent1; + locals.newICO.percent2 = input.percent2; + locals.newICO.percent3 = input.percent3; + locals.newICO.percent4 = input.percent4; + locals.newICO.percent5 = input.percent5; + locals.newICO.percent6 = input.percent6; + locals.newICO.percent7 = input.percent7; + locals.newICO.percent8 = input.percent8; + locals.newICO.percent9 = input.percent9; + locals.newICO.percent10 = input.percent10; + locals.newICO.startEpoch = input.startEpoch; + state.icos.set(state.numberOfICO, locals.newICO); + state.numberOfICO++; + output.returnCode = QIPLogInfo::QIP_success; + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_success; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + } + + struct buyToken_locals + { + ICOInfo ico; + uint64 distributedAmount, price; + uint32 idx, percent; + QIPLogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(buyToken) + { + if (input.indexOfICO >= state.numberOfICO) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_ICONotFound; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_ICONotFound; + return; + } + locals.ico = state.icos.get(input.indexOfICO); + if (qpi.epoch() == locals.ico.startEpoch) + { + if (input.amount <= locals.ico.remainingAmountForPhase1) + { + locals.price = locals.ico.price1; + locals.percent = locals.ico.percent1; + } + else + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidAmount; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidAmount; + return; + } + } + else if (qpi.epoch() == locals.ico.startEpoch + 1) + { + if (input.amount <= locals.ico.remainingAmountForPhase2) + { + locals.price = locals.ico.price2; + locals.percent = locals.ico.percent2; + } + else + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidAmount; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidAmount; + return ; + } + } + else if (qpi.epoch() == locals.ico.startEpoch + 2) + { + if (input.amount <= locals.ico.remainingAmountForPhase3) + { + locals.price = locals.ico.price3; + locals.percent = locals.ico.percent3; + } + else + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidAmount; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidAmount; + return ; + } + } + else + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_invalidEpoch; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_invalidEpoch; + return; + } + if (input.amount * locals.price > (uint64)qpi.invocationReward()) + { + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_insufficientInvocationReward; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + output.returnCode = QIPLogInfo::QIP_insufficientInvocationReward; + return; + } + if (input.amount * locals.price <= (uint64)qpi.invocationReward()) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount * locals.price); + } + qpi.transferShareOwnershipAndPossession(locals.ico.assetName, locals.ico.issuer, SELF, SELF, input.amount, qpi.invocator()); + locals.distributedAmount = div(input.amount * locals.price * locals.ico.percent1 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent2 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent3 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent4 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent5 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent6 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent7 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent8 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent9 * 1ULL, 100ULL) + div(input.amount * locals.price * locals.ico.percent10 * 1ULL, 100ULL); + qpi.transfer(locals.ico.address1, div(input.amount * locals.price * locals.ico.percent1 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address2, div(input.amount * locals.price * locals.ico.percent2 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address3, div(input.amount * locals.price * locals.ico.percent3 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address4, div(input.amount * locals.price * locals.ico.percent4 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address5, div(input.amount * locals.price * locals.ico.percent5 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address6, div(input.amount * locals.price * locals.ico.percent6 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address7, div(input.amount * locals.price * locals.ico.percent7 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address8, div(input.amount * locals.price * locals.ico.percent8 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address9, div(input.amount * locals.price * locals.ico.percent9 * 1ULL, 100ULL)); + qpi.transfer(locals.ico.address10, div(input.amount * locals.price * locals.ico.percent10 * 1ULL, 100ULL)); + qpi.distributeDividends(div((input.amount * locals.price - locals.distributedAmount), 676ULL)); + + if (qpi.epoch() == locals.ico.startEpoch) + { + locals.ico.remainingAmountForPhase1 -= input.amount; + } + else if (qpi.epoch() == locals.ico.startEpoch + 1) + { + locals.ico.remainingAmountForPhase2 -= input.amount; + } + else if (qpi.epoch() == locals.ico.startEpoch + 2) + { + locals.ico.remainingAmountForPhase3 -= input.amount; + } + state.icos.set(input.indexOfICO, locals.ico); + output.returnCode = QIPLogInfo::QIP_success; + locals.log._contractIndex = SELF_INDEX; + locals.log._type = QIPLogInfo::QIP_success; + locals.log.dst = qpi.invocator(); + locals.log.amt = 0; + LOG_INFO(locals.log); + } + + PUBLIC_PROCEDURE(TransferShareManagementRights) + { + if (qpi.invocationReward() < state.transferRightsFee) + { + return ; + } + + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + // not enough shares available + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, + input.newManagingContractIndex, input.newManagingContractIndex, state.transferRightsFee) < 0) + { + // error + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + // success + output.transferredNumberOfShares = input.numberOfShares; + if (qpi.invocationReward() > state.transferRightsFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.transferRightsFee); + } + } + } + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_FUNCTION(getICOInfo, 1); + + REGISTER_USER_PROCEDURE(createICO, 1); + REGISTER_USER_PROCEDURE(buyToken, 2); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 3); + } + + INITIALIZE() + { + state.transferRightsFee = 100; + } + + struct BEGIN_EPOCH_locals + { + ICOInfo ico; + }; + + BEGIN_EPOCH_WITH_LOCALS() + { + if (qpi.epoch() == 196) + { + locals.ico = state.icos.get(0); + locals.ico.remainingAmountForPhase3 = qpi.numberOfPossessedShares(locals.ico.assetName, locals.ico.issuer, SELF, SELF, SELF_INDEX, SELF_INDEX); + state.icos.set(0, locals.ico); + } + } + + struct END_EPOCH_locals + { + ICOInfo ico; + uint32 idx; + }; + + END_EPOCH_WITH_LOCALS() + { + for(locals.idx = 0; locals.idx < state.numberOfICO; locals.idx++) + { + locals.ico = state.icos.get(locals.idx); + if (locals.ico.startEpoch == qpi.epoch() && locals.ico.remainingAmountForPhase1 > 0) + { + locals.ico.remainingAmountForPhase2 += locals.ico.remainingAmountForPhase1; + locals.ico.remainingAmountForPhase1 = 0; + state.icos.set(locals.idx, locals.ico); + } + if (locals.ico.startEpoch + 1 == qpi.epoch() && locals.ico.remainingAmountForPhase2 > 0) + { + locals.ico.remainingAmountForPhase3 += locals.ico.remainingAmountForPhase2; + locals.ico.remainingAmountForPhase2 = 0; + state.icos.set(locals.idx, locals.ico); + } + if (locals.ico.startEpoch + 2 == qpi.epoch() && locals.ico.remainingAmountForPhase3 > 0) + { + qpi.transferShareOwnershipAndPossession(locals.ico.assetName, locals.ico.issuer, SELF, SELF, locals.ico.remainingAmountForPhase3, locals.ico.creatorOfICO); + state.icos.set(locals.idx, state.icos.get(state.numberOfICO - 1)); + state.numberOfICO--; + } + } + } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } + +}; \ No newline at end of file diff --git a/src/contracts/QRaffle.h b/src/contracts/QRaffle.h new file mode 100644 index 000000000..114de9551 --- /dev/null +++ b/src/contracts/QRaffle.h @@ -0,0 +1,1484 @@ +using namespace QPI; + +constexpr uint64 QRAFFLE_REGISTER_AMOUNT = 1000000000ull; +constexpr uint64 QRAFFLE_QXMR_REGISTER_AMOUNT = 100000000ull; +constexpr uint64 QRAFFLE_MAX_QRE_AMOUNT = 1000000000ull; +constexpr uint64 QRAFFLE_ASSET_NAME = 19505638103142993; +constexpr uint64 QRAFFLE_QXMR_ASSET_NAME = 1380800593; // QXMR token asset name +constexpr uint32 QRAFFLE_LOGOUT_FEE = 50000000; +constexpr uint32 QRAFFLE_QXMR_LOGOUT_FEE = 5000000; // QXMR logout fee +constexpr uint32 QRAFFLE_TRANSFER_SHARE_FEE = 100; +constexpr uint32 QRAFFLE_BURN_FEE = 10; // percent +constexpr uint32 QRAFFLE_REGISTER_FEE = 5; // percent +constexpr uint32 QRAFFLE_FEE = 1; // percent +constexpr uint32 QRAFFLE_CHARITY_FEE = 1; // percent +constexpr uint32 QRAFFLE_SHRAEHOLDER_FEE = 3; // percent +constexpr uint32 QRAFFLE_MAX_EPOCH = 65536; +constexpr uint32 QRAFFLE_MAX_PROPOSAL_EPOCH = 128; +constexpr uint32 QRAFFLE_MAX_MEMBER = 65536; +constexpr uint32 QRAFFLE_DEFAULT_QRAFFLE_AMOUNT = 10000000ull; +constexpr uint32 QRAFFLE_MIN_QRAFFLE_AMOUNT = 1000000ull; +constexpr uint32 QRAFFLE_MAX_QRAFFLE_AMOUNT = 1000000000ull; + +constexpr sint32 QRAFFLE_SUCCESS = 0; +constexpr sint32 QRAFFLE_INSUFFICIENT_FUND = 1; +constexpr sint32 QRAFFLE_ALREADY_REGISTERED = 2; +constexpr sint32 QRAFFLE_UNREGISTERED = 3; +constexpr sint32 QRAFFLE_MAX_PROPOSAL_EPOCH_REACHED = 4; +constexpr sint32 QRAFFLE_INVALID_PROPOSAL = 5; +constexpr sint32 QRAFFLE_FAILED_TO_DEPOSIT = 6; +constexpr sint32 QRAFFLE_ALREADY_VOTED = 7; +constexpr sint32 QRAFFLE_INVALID_TOKEN_RAFFLE = 8; +constexpr sint32 QRAFFLE_INVALID_OFFSET_OR_LIMIT = 9; +constexpr sint32 QRAFFLE_INVALID_EPOCH = 10; +constexpr sint32 QRAFFLE_MAX_MEMBER_REACHED = 11; +constexpr sint32 QRAFFLE_INITIAL_REGISTER_CANNOT_LOGOUT = 12; +constexpr sint32 QRAFFLE_INSUFFICIENT_QXMR = 13; +constexpr sint32 QRAFFLE_INVALID_TOKEN_TYPE = 14; +constexpr sint32 QRAFFLE_USER_NOT_FOUND = 15; +constexpr sint32 QRAFFLE_INVALID_ENTRY_AMOUNT = 16; +constexpr sint32 QRAFFLE_EMPTY_QU_RAFFLE = 17; +constexpr sint32 QRAFFLE_EMPTY_TOKEN_RAFFLE = 18; + +enum QRAFFLELogInfo { + QRAFFLE_success = 0, + QRAFFLE_insufficientQubic = 1, + QRAFFLE_insufficientQXMR = 2, + QRAFFLE_alreadyRegistered = 3, + QRAFFLE_unregistered = 4, + QRAFFLE_maxMemberReached = 5, + QRAFFLE_maxProposalEpochReached = 6, + QRAFFLE_invalidProposal = 7, + QRAFFLE_failedToDeposit = 8, + QRAFFLE_alreadyVoted = 9, + QRAFFLE_invalidTokenRaffle = 10, + QRAFFLE_invalidOffsetOrLimit = 11, + QRAFFLE_invalidEpoch = 12, + QRAFFLE_initialRegisterCannotLogout = 13, + QRAFFLE_invalidTokenType = 14, + QRAFFLE_invalidEntryAmount = 15, + QRAFFLE_maxMemberReachedForQuRaffle = 16, + QRAFFLE_proposalNotFound = 17, + QRAFFLE_proposalAlreadyEnded = 18, + QRAFFLE_notEnoughShares = 19, + QRAFFLE_transferFailed = 20, + QRAFFLE_epochEnded = 21, + QRAFFLE_winnerSelected = 22, + QRAFFLE_revenueDistributed = 23, + QRAFFLE_tokenRaffleCreated = 24, + QRAFFLE_tokenRaffleEnded = 25, + QRAFFLE_proposalSubmitted = 26, + QRAFFLE_proposalVoted = 27, + QRAFFLE_quRaffleDeposited = 28, + QRAFFLE_tokenRaffleDeposited = 29, + QRAFFLE_shareManagementRightsTransferred = 30, + QRAFFLE_emptyQuRaffle = 31, + QRAFFLE_emptyTokenRaffle = 32 +}; + +struct QRAFFLELogger +{ + uint32 _contractIndex; + uint32 _type; // Assign a random unique (per contract) number to distinguish messages of different types + sint8 _terminator; // Only data before "_terminator" are logged +}; + +// Enhanced logger for END_EPOCH with detailed information +struct QRAFFLEEndEpochLogger +{ + uint32 _contractIndex; + uint32 _type; + uint32 _epoch; // Current epoch number + uint32 _memberCount; // Number of QuRaffle members + uint64 _totalAmount; // Total amount being processed + uint64 _winnerAmount; // Amount won by winner + uint32 _winnerIndex; // Index of the winner + sint8 _terminator; +}; + +// Enhanced logger for revenue distribution +struct QRAFFLERevenueLogger +{ + uint32 _contractIndex; + uint32 _type; + uint64 _burnAmount; // Amount burned + uint64 _charityAmount; // Amount sent to charity + uint64 _shareholderAmount; // Amount distributed to shareholders + uint64 _registerAmount; // Amount distributed to registers + uint64 _feeAmount; // Amount sent to fee address + uint64 _winnerAmount; // Amount sent to winner + sint8 _terminator; +}; + +// Enhanced logger for token raffle processing +struct QRAFFLETokenRaffleLogger +{ + uint32 _contractIndex; + uint32 _type; + uint32 _raffleIndex; // Index of the token raffle + uint64 _assetName; // Asset name of the token + uint32 _memberCount; // Number of members in this raffle + uint64 _entryAmount; // Entry amount for this raffle + uint32 _winnerIndex; // Winner index for this raffle + uint64 _winnerAmount; // Amount won in this raffle + sint8 _terminator; +}; + +// Enhanced logger for proposal processing +struct QRAFFLEProposalLogger +{ + uint32 _contractIndex; + uint32 _type; + uint32 _proposalIndex; // Index of the proposal + id _proposer; // Proposer of the proposal + uint32 _yesVotes; // Number of yes votes + uint32 _noVotes; // Number of no votes + uint64 _assetName; // Asset name if approved + uint64 _entryAmount; // Entry amount if approved + sint8 _terminator; +}; + +struct QRAFFLEEmptyTokenRaffleLogger +{ + uint32 _contractIndex; + uint32 _type; + uint32 _tokenRaffleIndex; // Index of the token raffle per epoch + sint8 _terminator; +}; + +struct QRAFFLE2 +{ +}; + +struct QRAFFLE : public ContractBase +{ +public: + struct registerInSystem_input + { + bit useQXMR; // 0 = use qubic, 1 = use QXMR tokens + }; + + struct registerInSystem_output + { + sint32 returnCode; + }; + + struct logoutInSystem_input + { + }; + + struct logoutInSystem_output + { + sint32 returnCode; + }; + + struct submitEntryAmount_input + { + uint64 amount; + }; + + struct submitEntryAmount_output + { + sint32 returnCode; + }; + + struct submitProposal_input + { + id tokenIssuer; + uint64 tokenName; + uint64 entryAmount; + }; + + struct submitProposal_output + { + sint32 returnCode; + }; + + struct voteInProposal_input + { + uint32 indexOfProposal; + bit yes; + }; + + struct voteInProposal_output + { + sint32 returnCode; + }; + + struct depositInQuRaffle_input + { + + }; + + struct depositInQuRaffle_output + { + sint32 returnCode; + }; + + struct depositInTokenRaffle_input + { + uint32 indexOfTokenRaffle; + }; + + struct depositInTokenRaffle_output + { + sint32 returnCode; + }; + + struct TransferShareManagementRights_input + { + id tokenIssuer; + uint64 tokenName; + sint64 numberOfShares; + uint32 newManagingContractIndex; + }; + + struct TransferShareManagementRights_output + { + sint64 transferredNumberOfShares; + }; + + struct getRegisters_input + { + uint32 offset; + uint32 limit; + }; + + struct getRegisters_output + { + id register1, register2, register3, register4, register5, register6, register7, register8, register9, register10, register11, register12, register13, register14, register15, register16, register17, register18, register19, register20; + sint32 returnCode; + }; + + struct getAnalytics_input + { + }; + + struct getAnalytics_output + { + uint64 currentQuRaffleAmount; + uint64 totalBurnAmount; + uint64 totalCharityAmount; + uint64 totalShareholderAmount; + uint64 totalRegisterAmount; + uint64 totalFeeAmount; + uint64 totalWinnerAmount; + uint64 largestWinnerAmount; + uint32 numberOfRegisters; + uint32 numberOfProposals; + uint32 numberOfQuRaffleMembers; + uint32 numberOfActiveTokenRaffle; + uint32 numberOfEndedTokenRaffle; + uint32 numberOfEntryAmountSubmitted; + sint32 returnCode; + }; + + struct getActiveProposal_input + { + uint32 indexOfProposal; + }; + + struct getActiveProposal_output + { + id tokenIssuer; + id proposer; + uint64 tokenName; + uint64 entryAmount; + uint32 nYes; + uint32 nNo; + sint32 returnCode; + }; + + struct getEndedTokenRaffle_input + { + uint32 indexOfRaffle; + }; + + struct getEndedTokenRaffle_output + { + id epochWinner; + id tokenIssuer; + uint64 tokenName; + uint64 entryAmount; + uint32 numberOfMembers; + uint32 winnerIndex; + uint32 epoch; + sint32 returnCode; + }; + + struct getEpochRaffleIndexes_input + { + uint32 epoch; + }; + + struct getEpochRaffleIndexes_output + { + uint32 StartIndex; + uint32 EndIndex; + sint32 returnCode; + }; + + struct getEndedQuRaffle_input + { + uint32 epoch; + }; + + struct getEndedQuRaffle_output + { + id epochWinner; + uint64 receivedAmount; + uint64 entryAmount; + uint32 numberOfMembers; + uint32 winnerIndex; + sint32 returnCode; + }; + + struct getActiveTokenRaffle_input + { + uint32 indexOfTokenRaffle; + }; + + struct getActiveTokenRaffle_output + { + id tokenIssuer; + uint64 tokenName; + uint64 entryAmount; + uint32 numberOfMembers; + sint32 returnCode; + }; + + struct getQuRaffleEntryAmountPerUser_input + { + id user; + }; + + struct getQuRaffleEntryAmountPerUser_output + { + uint64 entryAmount; + sint32 returnCode; + }; + + struct getQuRaffleEntryAverageAmount_input + { + }; + + struct getQuRaffleEntryAverageAmount_output + { + uint64 entryAverageAmount; + sint32 returnCode; + }; + +protected: + + HashMap registers; + + struct ProposalInfo { + Asset token; + id proposer; + uint64 entryAmount; + uint32 nYes; + uint32 nNo; + }; + Array proposals; + + struct VotedId { + id user; + bit status; + }; + HashMap , QRAFFLE_MAX_PROPOSAL_EPOCH> voteStatus; + Array tmpVoteStatus; + Array numberOfVotedInProposal; + Array quRaffleMembers; + + struct ActiveTokenRaffleInfo { + Asset token; + uint64 entryAmount; + }; + Array activeTokenRaffle; + HashMap , QRAFFLE_MAX_PROPOSAL_EPOCH> tokenRaffleMembers; + Array numberOfTokenRaffleMembers; + Array tmpTokenRaffleMembers; + + struct QuRaffleInfo + { + id epochWinner; + uint64 receivedAmount; + uint64 entryAmount; + uint32 numberOfMembers; + uint32 winnerIndex; + }; + Array QuRaffles; + struct TokenRaffleInfo + { + id epochWinner; + Asset token; + uint64 entryAmount; + uint32 numberOfMembers; + uint32 winnerIndex; + uint32 epoch; + }; + Array tokenRaffle; + HashMap quRaffleEntryAmount; + HashSet shareholdersList; + + id initialRegister1, initialRegister2, initialRegister3, initialRegister4, initialRegister5; + id charityAddress, feeAddress, QXMRIssuer; + uint64 epochRevenue, epochQXMRRevenue, qREAmount, totalBurnAmount, totalCharityAmount, totalShareholderAmount, totalRegisterAmount, totalFeeAmount, totalWinnerAmount, largestWinnerAmount; + uint32 numberOfRegisters, numberOfQuRaffleMembers, numberOfEntryAmountSubmitted, numberOfProposals, numberOfActiveTokenRaffle, numberOfEndedTokenRaffle; + + struct registerInSystem_locals + { + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(registerInSystem) + { + if (state.registers.contains(qpi.invocator())) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_ALREADY_REGISTERED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; + LOG_INFO(locals.log); + return ; + } + if (state.numberOfRegisters >= QRAFFLE_MAX_MEMBER) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReached, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (input.useQXMR) + { + // refund the invocation reward if the user uses QXMR for registration + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + // Use QXMR tokens for registration + if (qpi.numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, state.QXMRIssuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QRAFFLE_QXMR_REGISTER_AMOUNT) + { + output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; + LOG_INFO(locals.log); + return ; + } + + // Transfer QXMR tokens to the contract + if (qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.QXMRIssuer, qpi.invocator(), qpi.invocator(), QRAFFLE_QXMR_REGISTER_AMOUNT, SELF) < 0) + { + output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; + LOG_INFO(locals.log); + return ; + } + state.registers.set(qpi.invocator(), 2); + } + else + { + // Use qubic for registration + if (qpi.invocationReward() < QRAFFLE_REGISTER_AMOUNT) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_INSUFFICIENT_FUND; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; + LOG_INFO(locals.log); + return ; + } + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_REGISTER_AMOUNT); + state.registers.set(qpi.invocator(), 1); + } + + state.numberOfRegisters++; + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_success, 0 }; + LOG_INFO(locals.log); + } + + struct logoutInSystem_locals + { + sint64 refundAmount; + uint8 tokenType; + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(logoutInSystem) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + if (qpi.invocator() == state.initialRegister1 || qpi.invocator() == state.initialRegister2 || qpi.invocator() == state.initialRegister3 || qpi.invocator() == state.initialRegister4 || qpi.invocator() == state.initialRegister5) + { + output.returnCode = QRAFFLE_INITIAL_REGISTER_CANNOT_LOGOUT; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_initialRegisterCannotLogout, 0 }; + LOG_INFO(locals.log); + return ; + } + if (state.registers.contains(qpi.invocator()) == 0) + { + output.returnCode = QRAFFLE_UNREGISTERED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; + LOG_INFO(locals.log); + return ; + } + + state.registers.get(qpi.invocator(), locals.tokenType); + + if (locals.tokenType == 1) + { + // Use qubic for logout + locals.refundAmount = QRAFFLE_REGISTER_AMOUNT - QRAFFLE_LOGOUT_FEE; + qpi.transfer(qpi.invocator(), locals.refundAmount); + state.epochRevenue += QRAFFLE_LOGOUT_FEE; + } + else if (locals.tokenType == 2) + { + // Use QXMR tokens for logout + locals.refundAmount = QRAFFLE_QXMR_REGISTER_AMOUNT - QRAFFLE_QXMR_LOGOUT_FEE; + + // Check if contract has enough QXMR tokens + if (qpi.numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, state.QXMRIssuer, SELF, SELF, SELF_INDEX, SELF_INDEX) < locals.refundAmount) + { + output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; + LOG_INFO(locals.log); + return ; + } + + // Transfer QXMR tokens back to user + if (qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.QXMRIssuer, SELF, SELF, locals.refundAmount, qpi.invocator()) < 0) + { + output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; + LOG_INFO(locals.log); + return ; + } + + state.epochQXMRRevenue += QRAFFLE_QXMR_LOGOUT_FEE; + } + + state.registers.removeByKey(qpi.invocator()); + state.numberOfRegisters--; + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_success, 0 }; + LOG_INFO(locals.log); + } + + struct submitEntryAmount_locals + { + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(submitEntryAmount) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + if (input.amount < QRAFFLE_MIN_QRAFFLE_AMOUNT || input.amount > QRAFFLE_MAX_QRAFFLE_AMOUNT) + { + output.returnCode = QRAFFLE_INVALID_ENTRY_AMOUNT; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidEntryAmount, 0 }; + LOG_INFO(locals.log); + return ; + } + if (state.registers.contains(qpi.invocator()) == 0) + { + output.returnCode = QRAFFLE_UNREGISTERED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; + LOG_INFO(locals.log); + return ; + } + if (state.quRaffleEntryAmount.contains(qpi.invocator()) == 0) + { + state.numberOfEntryAmountSubmitted++; + } + state.quRaffleEntryAmount.set(qpi.invocator(), input.amount); + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_success, 0 }; + LOG_INFO(locals.log); + } + + struct submitProposal_locals + { + ProposalInfo proposal; + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(submitProposal) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + if (state.registers.contains(qpi.invocator()) == 0) + { + output.returnCode = QRAFFLE_UNREGISTERED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; + LOG_INFO(locals.log); + return ; + } + if (state.numberOfProposals >= QRAFFLE_MAX_PROPOSAL_EPOCH) + { + output.returnCode = QRAFFLE_MAX_PROPOSAL_EPOCH_REACHED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxProposalEpochReached, 0 }; + LOG_INFO(locals.log); + return ; + } + locals.proposal.token.issuer = input.tokenIssuer; + locals.proposal.token.assetName = input.tokenName; + locals.proposal.entryAmount = input.entryAmount; + locals.proposal.proposer = qpi.invocator(); + state.proposals.set(state.numberOfProposals, locals.proposal); + state.numberOfProposals++; + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalSubmitted, 0 }; + LOG_INFO(locals.log); + } + + struct voteInProposal_locals + { + ProposalInfo proposal; + VotedId votedId; + uint32 i; + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(voteInProposal) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + if (state.registers.contains(qpi.invocator()) == 0) + { + output.returnCode = QRAFFLE_UNREGISTERED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; + LOG_INFO(locals.log); + return ; + } + if (input.indexOfProposal >= state.numberOfProposals) + { + output.returnCode = QRAFFLE_INVALID_PROPOSAL; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalNotFound, 0 }; + LOG_INFO(locals.log); + return ; + } + locals.proposal = state.proposals.get(input.indexOfProposal); + state.voteStatus.get(input.indexOfProposal, state.tmpVoteStatus); + for (locals.i = 0; locals.i < state.numberOfVotedInProposal.get(input.indexOfProposal); locals.i++) + { + if (state.tmpVoteStatus.get(locals.i).user == qpi.invocator()) + { + if (state.tmpVoteStatus.get(locals.i).status == input.yes) + { + output.returnCode = QRAFFLE_ALREADY_VOTED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyVoted, 0 }; + LOG_INFO(locals.log); + return ; + } + else + { + if (input.yes) + { + locals.proposal.nYes++; + locals.proposal.nNo--; + } + else + { + locals.proposal.nNo++; + locals.proposal.nYes--; + } + state.proposals.set(input.indexOfProposal, locals.proposal); + } + + locals.votedId.user = qpi.invocator(); + locals.votedId.status = input.yes; + state.tmpVoteStatus.set(locals.i, locals.votedId); + state.voteStatus.set(input.indexOfProposal, state.tmpVoteStatus); + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; + LOG_INFO(locals.log); + return ; + } + } + if (input.yes) + { + locals.proposal.nYes++; + } + else + { + locals.proposal.nNo++; + } + state.proposals.set(input.indexOfProposal, locals.proposal); + + locals.votedId.user = qpi.invocator(); + locals.votedId.status = input.yes; + state.tmpVoteStatus.set(state.numberOfVotedInProposal.get(input.indexOfProposal), locals.votedId); + state.voteStatus.set(input.indexOfProposal, state.tmpVoteStatus); + state.numberOfVotedInProposal.set(input.indexOfProposal, state.numberOfVotedInProposal.get(input.indexOfProposal) + 1); + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; + LOG_INFO(locals.log); + } + + struct depositInQuRaffle_locals + { + uint32 i; + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(depositInQuRaffle) + { + if (state.numberOfQuRaffleMembers >= QRAFFLE_MAX_MEMBER) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReachedForQuRaffle, 0 }; + LOG_INFO(locals.log); + return ; + } + if (qpi.invocationReward() < (sint64)state.qREAmount) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_INSUFFICIENT_FUND; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; + LOG_INFO(locals.log); + return ; + } + for (locals.i = 0 ; locals.i < state.numberOfQuRaffleMembers; locals.i++) + { + if (state.quRaffleMembers.get(locals.i) == qpi.invocator()) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_ALREADY_REGISTERED; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; + LOG_INFO(locals.log); + return ; + } + } + qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.qREAmount); + state.quRaffleMembers.set(state.numberOfQuRaffleMembers, qpi.invocator()); + state.numberOfQuRaffleMembers++; + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_quRaffleDeposited, 0 }; + LOG_INFO(locals.log); + } + + struct depositInTokenRaffle_locals + { + uint32 i; + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(depositInTokenRaffle) + { + if (qpi.invocationReward() < QRAFFLE_TRANSFER_SHARE_FEE) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_INSUFFICIENT_FUND; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; + LOG_INFO(locals.log); + return ; + } + if (input.indexOfTokenRaffle >= state.numberOfActiveTokenRaffle) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenRaffle, 0 }; + LOG_INFO(locals.log); + return ; + } + if (qpi.transferShareOwnershipAndPossession(state.activeTokenRaffle.get(input.indexOfTokenRaffle).token.assetName, state.activeTokenRaffle.get(input.indexOfTokenRaffle).token.issuer, qpi.invocator(), qpi.invocator(), state.activeTokenRaffle.get(input.indexOfTokenRaffle).entryAmount, SELF) < 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.returnCode = QRAFFLE_FAILED_TO_DEPOSIT; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_transferFailed, 0 }; + LOG_INFO(locals.log); + return ; + } + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE); + state.tokenRaffleMembers.get(input.indexOfTokenRaffle, state.tmpTokenRaffleMembers); + state.tmpTokenRaffleMembers.set(state.numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle), qpi.invocator()); + state.numberOfTokenRaffleMembers.set(input.indexOfTokenRaffle, state.numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle) + 1); + state.tokenRaffleMembers.set(input.indexOfTokenRaffle, state.tmpTokenRaffleMembers); + output.returnCode = QRAFFLE_SUCCESS; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_tokenRaffleDeposited, 0 }; + LOG_INFO(locals.log); + } + + struct TransferShareManagementRights_locals + { + Asset asset; + QRAFFLELogger log; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) + { + if (qpi.invocationReward() < QRAFFLE_TRANSFER_SHARE_FEE) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; + LOG_INFO(locals.log); + return ; + } + + if (qpi.numberOfPossessedShares(input.tokenName, input.tokenIssuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + // not enough shares available + output.transferredNumberOfShares = 0; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_notEnoughShares, 0 }; + LOG_INFO(locals.log); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + locals.asset.assetName = input.tokenName; + locals.asset.issuer = input.tokenIssuer; + if (qpi.releaseShares(locals.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, + input.newManagingContractIndex, input.newManagingContractIndex, QRAFFLE_TRANSFER_SHARE_FEE) < 0) + { + // error + output.transferredNumberOfShares = 0; + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_transferFailed, 0 }; + LOG_INFO(locals.log); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + // success + output.transferredNumberOfShares = input.numberOfShares; + if (qpi.invocationReward() > QRAFFLE_TRANSFER_SHARE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE); + } + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_shareManagementRightsTransferred, 0 }; + LOG_INFO(locals.log); + } + } + } + + struct getRegisters_locals + { + id user; + sint64 idx; + uint32 i; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getRegisters) + { + if (input.limit > 20) + { + output.returnCode = QRAFFLE_INVALID_OFFSET_OR_LIMIT; + return ; + } + if (input.offset + input.limit > state.numberOfRegisters) + { + output.returnCode = QRAFFLE_INVALID_OFFSET_OR_LIMIT; + return ; + } + locals.idx = state.registers.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.user = state.registers.key(locals.idx); + if (locals.i >= input.offset && locals.i < input.offset + input.limit) + { + if (locals.i - input.offset == 0) + { + output.register1 = locals.user; + } + else if (locals.i - input.offset == 1) + { + output.register2 = locals.user; + } + else if (locals.i - input.offset == 2) + { + output.register3 = locals.user; + } + else if (locals.i - input.offset == 3) + { + output.register4 = locals.user; + } + else if (locals.i - input.offset == 4) + { + output.register5 = locals.user; + } + else if (locals.i - input.offset == 5) + { + output.register6 = locals.user; + } + else if (locals.i - input.offset == 6) + { + output.register7 = locals.user; + } + else if (locals.i - input.offset == 7) + { + output.register8 = locals.user; + } + else if (locals.i - input.offset == 8) + { + output.register9 = locals.user; + } + else if (locals.i - input.offset == 9) + { + output.register10 = locals.user; + } + else if (locals.i - input.offset == 10) + { + output.register11 = locals.user; + } + else if (locals.i - input.offset == 11) + { + output.register12 = locals.user; + } + else if (locals.i - input.offset == 12) + { + output.register13 = locals.user; + } + else if (locals.i - input.offset == 13) + { + output.register14 = locals.user; + } + else if (locals.i - input.offset == 14) + { + output.register15 = locals.user; + } + else if (locals.i - input.offset == 15) + { + output.register16 = locals.user; + } + else if (locals.i - input.offset == 16) + { + output.register17 = locals.user; + } + else if (locals.i - input.offset == 17) + { + output.register18 = locals.user; + } + else if (locals.i - input.offset == 18) + { + output.register19 = locals.user; + } + else if (locals.i - input.offset == 19) + { + output.register20 = locals.user; + } + } + if (locals.i >= input.offset + input.limit) + { + break; + } + locals.i++; + locals.idx = state.registers.nextElementIndex(locals.idx); + } + output.returnCode = QRAFFLE_SUCCESS; + } + + PUBLIC_FUNCTION(getAnalytics) + { + output.currentQuRaffleAmount = state.qREAmount; + output.totalBurnAmount = state.totalBurnAmount; + output.totalCharityAmount = state.totalCharityAmount; + output.totalShareholderAmount = state.totalShareholderAmount; + output.totalRegisterAmount = state.totalRegisterAmount; + output.totalFeeAmount = state.totalFeeAmount; + output.totalWinnerAmount = state.totalWinnerAmount; + output.largestWinnerAmount = state.largestWinnerAmount; + output.numberOfRegisters = state.numberOfRegisters; + output.numberOfProposals = state.numberOfProposals; + output.numberOfQuRaffleMembers = state.numberOfQuRaffleMembers; + output.numberOfActiveTokenRaffle = state.numberOfActiveTokenRaffle; + output.numberOfEndedTokenRaffle = state.numberOfEndedTokenRaffle; + output.numberOfEntryAmountSubmitted = state.numberOfEntryAmountSubmitted; + output.returnCode = QRAFFLE_SUCCESS; + } + + PUBLIC_FUNCTION(getActiveProposal) + { + if (input.indexOfProposal >= state.numberOfProposals) + { + output.returnCode = QRAFFLE_INVALID_PROPOSAL; + return ; + } + output.tokenName = state.proposals.get(input.indexOfProposal).token.assetName; + output.tokenIssuer = state.proposals.get(input.indexOfProposal).token.issuer; + output.proposer = state.proposals.get(input.indexOfProposal).proposer; + output.entryAmount = state.proposals.get(input.indexOfProposal).entryAmount; + output.nYes = state.proposals.get(input.indexOfProposal).nYes; + output.nNo = state.proposals.get(input.indexOfProposal).nNo; + output.returnCode = QRAFFLE_SUCCESS; + } + + PUBLIC_FUNCTION(getEndedTokenRaffle) + { + if (input.indexOfRaffle >= state.numberOfEndedTokenRaffle) + { + output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; + return ; + } + output.epochWinner = state.tokenRaffle.get(input.indexOfRaffle).epochWinner; + output.tokenName = state.tokenRaffle.get(input.indexOfRaffle).token.assetName; + output.tokenIssuer = state.tokenRaffle.get(input.indexOfRaffle).token.issuer; + output.entryAmount = state.tokenRaffle.get(input.indexOfRaffle).entryAmount; + output.numberOfMembers = state.tokenRaffle.get(input.indexOfRaffle).numberOfMembers; + output.winnerIndex = state.tokenRaffle.get(input.indexOfRaffle).winnerIndex; + output.epoch = state.tokenRaffle.get(input.indexOfRaffle).epoch; + output.returnCode = QRAFFLE_SUCCESS; + } + + struct getEpochRaffleIndexes_locals + { + sint32 i; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getEpochRaffleIndexes) + { + if (input.epoch > qpi.epoch()) + { + output.returnCode = QRAFFLE_INVALID_EPOCH; + return ; + } + if (input.epoch == qpi.epoch()) + { + output.StartIndex = 0; + output.EndIndex = state.numberOfActiveTokenRaffle; + return ; + } + for (locals.i = 0; locals.i < (sint32)state.numberOfEndedTokenRaffle; locals.i++) + { + if (state.tokenRaffle.get(locals.i).epoch == input.epoch) + { + output.StartIndex = locals.i; + break; + } + } + for (locals.i = (sint32)state.numberOfEndedTokenRaffle - 1; locals.i >= 0; locals.i--) + { + if (state.tokenRaffle.get(locals.i).epoch == input.epoch) + { + output.EndIndex = locals.i; + break; + } + } + output.returnCode = QRAFFLE_SUCCESS; + } + + PUBLIC_FUNCTION(getEndedQuRaffle) + { + output.epochWinner = state.QuRaffles.get(input.epoch).epochWinner; + output.receivedAmount = state.QuRaffles.get(input.epoch).receivedAmount; + output.entryAmount = state.QuRaffles.get(input.epoch).entryAmount; + output.numberOfMembers = state.QuRaffles.get(input.epoch).numberOfMembers; + output.winnerIndex = state.QuRaffles.get(input.epoch).winnerIndex; + output.returnCode = QRAFFLE_SUCCESS; + } + + PUBLIC_FUNCTION(getActiveTokenRaffle) + { + if (input.indexOfTokenRaffle >= state.numberOfActiveTokenRaffle) + { + output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; + return ; + } + output.tokenName = state.activeTokenRaffle.get(input.indexOfTokenRaffle).token.assetName; + output.tokenIssuer = state.activeTokenRaffle.get(input.indexOfTokenRaffle).token.issuer; + output.entryAmount = state.activeTokenRaffle.get(input.indexOfTokenRaffle).entryAmount; + output.numberOfMembers = state.numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle); + output.returnCode = QRAFFLE_SUCCESS; + } + + PUBLIC_FUNCTION(getQuRaffleEntryAmountPerUser) + { + if (state.quRaffleEntryAmount.contains(input.user) == 0) + { + output.entryAmount = 0; + output.returnCode = QRAFFLE_USER_NOT_FOUND; + } + else + { + state.quRaffleEntryAmount.get(input.user, output.entryAmount); + output.returnCode = QRAFFLE_SUCCESS; + } + } + + struct getQuRaffleEntryAverageAmount_locals + { + uint64 entryAmount; + uint64 totalEntryAmount; + sint64 idx; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getQuRaffleEntryAverageAmount) + { + locals.idx = state.quRaffleEntryAmount.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.totalEntryAmount += state.quRaffleEntryAmount.value(locals.idx); + locals.idx = state.quRaffleEntryAmount.nextElementIndex(locals.idx); + } + if (state.numberOfEntryAmountSubmitted > 0) + { + output.entryAverageAmount = div(locals.totalEntryAmount, state.numberOfEntryAmountSubmitted); + } + else + { + output.entryAverageAmount = 0; + } + output.returnCode = QRAFFLE_SUCCESS; + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_FUNCTION(getRegisters, 1); + REGISTER_USER_FUNCTION(getAnalytics, 2); + REGISTER_USER_FUNCTION(getActiveProposal, 3); + REGISTER_USER_FUNCTION(getEndedTokenRaffle, 4); + REGISTER_USER_FUNCTION(getEndedQuRaffle, 5); + REGISTER_USER_FUNCTION(getActiveTokenRaffle, 6); + REGISTER_USER_FUNCTION(getEpochRaffleIndexes, 7); + REGISTER_USER_FUNCTION(getQuRaffleEntryAmountPerUser, 8); + REGISTER_USER_FUNCTION(getQuRaffleEntryAverageAmount, 9); + + REGISTER_USER_PROCEDURE(registerInSystem, 1); + REGISTER_USER_PROCEDURE(logoutInSystem, 2); + REGISTER_USER_PROCEDURE(submitEntryAmount, 3); + REGISTER_USER_PROCEDURE(submitProposal, 4); + REGISTER_USER_PROCEDURE(voteInProposal, 5); + REGISTER_USER_PROCEDURE(depositInQuRaffle, 6); + REGISTER_USER_PROCEDURE(depositInTokenRaffle, 7); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 8); + } + + INITIALIZE() + { + state.qREAmount = QRAFFLE_DEFAULT_QRAFFLE_AMOUNT; + state.charityAddress = ID(_D, _P, _Q, _R, _L, _S, _Z, _S, _S, _C, _X, _I, _Y, _F, _I, _Q, _G, _B, _F, _B, _X, _X, _I, _S, _D, _D, _E, _B, _E, _G, _Q, _N, _W, _N, _T, _Q, _U, _E, _I, _F, _S, _C, _U, _W, _G, _H, _V, _X, _J, _P, _L, _F, _G, _M, _Y, _D); + state.initialRegister1 = ID(_I, _L, _N, _J, _X, _V, _H, _A, _U, _X, _D, _G, _G, _B, _T, _T, _U, _O, _I, _T, _O, _Q, _G, _P, _A, _Y, _U, _C, _F, _T, _N, _C, _P, _X, _D, _K, _O, _C, _P, _U, _O, _C, _D, _O, _T, _P, _U, _W, _X, _B, _I, _G, _R, _V, _Q, _D); + state.initialRegister2 = ID(_L, _S, _D, _A, _A, _C, _L, _X, _X, _G, _I, _P, _G, _G, _L, _S, _O, _C, _L, _M, _V, _A, _Y, _L, _N, _T, _G, _D, _V, _B, _N, _O, _S, _S, _Y, _E, _Q, _D, _R, _K, _X, _D, _Y, _W, _B, _C, _G, _J, _I, _K, _C, _M, _Z, _K, _M, _F); + state.initialRegister3 = ID(_G, _H, _G, _R, _L, _W, _S, _X, _Z, _X, _W, _D, _A, _A, _O, _M, _T, _X, _Q, _Y, _U, _P, _R, _L, _P, _N, _K, _C, _W, _G, _H, _A, _E, _F, _I, _R, _J, _I, _Z, _A, _K, _C, _A, _U, _D, _G, _N, _M, _C, _D, _E, _Q, _R, _O, _Q, _B); + state.initialRegister4 = ID(_E, _U, _O, _N, _A, _Z, _J, _U, _A, _G, _V, _D, _C, _E, _I, _B, _A, _H, _J, _E, _T, _G, _U, _U, _H, _M, _N, _D, _J, _C, _S, _E, _T, _T, _Q, _V, _G, _Y, _F, _H, _M, _D, _P, _X, _T, _A, _L, _D, _Y, _U, _V, _E, _P, _F, _C, _A); + state.initialRegister5 = ID(_S, _L, _C, _J, _C, _C, _U, _X, _G, _K, _N, _V, _A, _D, _F, _B, _E, _A, _Y, _V, _L, _S, _O, _B, _Z, _P, _A, _B, _H, _K, _S, _G, _M, _H, _W, _H, _S, _H, _G, _G, _B, _A, _P, _J, _W, _F, _V, _O, _K, _Z, _J, _P, _F, _L, _X, _D); + state.QXMRIssuer = ID(_Q, _X, _M, _R, _T, _K, _A, _I, _I, _G, _L, _U, _R, _E, _P, _I, _Q, _P, _C, _M, _H, _C, _K, _W, _S, _I, _P, _D, _T, _U, _Y, _F, _C, _F, _N, _Y, _X, _Q, _L, _T, _E, _C, _S, _U, _J, _V, _Y, _E, _M, _M, _D, _E, _L, _B, _M, _D); + state.feeAddress = ID(_H, _H, _R, _L, _C, _Z, _Q, _V, _G, _O, _M, _G, _X, _G, _F, _P, _H, _T, _R, _H, _H, _D, _W, _A, _E, _U, _X, _C, _N, _D, _L, _Z, _S, _Z, _J, _R, _M, _O, _R, _J, _K, _A, _I, _W, _S, _U, _Y, _R, _N, _X, _I, _H, _H, _O, _W, _D); + + state.registers.set(state.initialRegister1, 0); + state.registers.set(state.initialRegister2, 0); + state.registers.set(state.initialRegister3, 0); + state.registers.set(state.initialRegister4, 0); + state.registers.set(state.initialRegister5, 0); + state.numberOfRegisters = 5; + } + + struct END_EPOCH_locals + { + ProposalInfo proposal; + QuRaffleInfo qraffle; + TokenRaffleInfo tRaffle; + ActiveTokenRaffleInfo acTokenRaffle; + AssetPossessionIterator iter; + Asset QraffleAsset; + id digest, winner, shareholder; + sint64 idx; + uint64 sumOfEntryAmountSubmitted, r, winnerRevenue, burnAmount, charityRevenue, shareholderRevenue, registerRevenue, fee, oneShareholderRev; + uint32 i, j, winnerIndex; + QRAFFLELogger log; + QRAFFLEEmptyTokenRaffleLogger emptyTokenRafflelog; + QRAFFLEEndEpochLogger endEpochLog; + QRAFFLERevenueLogger revenueLog; + QRAFFLETokenRaffleLogger tokenRaffleLog; + QRAFFLEProposalLogger proposalLog; + }; + + END_EPOCH_WITH_LOCALS() + { + locals.oneShareholderRev = div(state.epochRevenue, 676); + qpi.distributeDividends(locals.oneShareholderRev); + state.epochRevenue -= locals.oneShareholderRev * 676; + + locals.digest = qpi.getPrevSpectrumDigest(); + locals.r = (qpi.numberOfTickTransactions() + 1) * locals.digest.u64._0 + (qpi.second()) * locals.digest.u64._1 + locals.digest.u64._2; + locals.winnerIndex = (uint32)mod(locals.r, state.numberOfQuRaffleMembers * 1ull); + locals.winner = state.quRaffleMembers.get(locals.winnerIndex); + + // Get QRAFFLE asset shareholders + locals.QraffleAsset.assetName = QRAFFLE_ASSET_NAME; + locals.QraffleAsset.issuer = NULL_ID; + locals.iter.begin(locals.QraffleAsset); + while (!locals.iter.reachedEnd()) + { + locals.shareholder = locals.iter.possessor(); + if (state.shareholdersList.contains(locals.shareholder) == 0) + { + state.shareholdersList.add(locals.shareholder); + } + + locals.iter.next(); + } + + if (state.numberOfQuRaffleMembers > 0) + { + // Calculate fee distributions + locals.burnAmount = div(state.qREAmount * state.numberOfQuRaffleMembers * QRAFFLE_BURN_FEE, 100); + locals.charityRevenue = div(state.qREAmount * state.numberOfQuRaffleMembers * QRAFFLE_CHARITY_FEE, 100); + locals.shareholderRevenue = div(state.qREAmount * state.numberOfQuRaffleMembers * QRAFFLE_SHRAEHOLDER_FEE, 100); + locals.registerRevenue = div(state.qREAmount * state.numberOfQuRaffleMembers * QRAFFLE_REGISTER_FEE, 100); + locals.fee = div(state.qREAmount * state.numberOfQuRaffleMembers * QRAFFLE_FEE, 100); + locals.winnerRevenue = state.qREAmount * state.numberOfQuRaffleMembers - locals.burnAmount - locals.charityRevenue - div(locals.shareholderRevenue, 676) * 676 - div(locals.registerRevenue, state.numberOfRegisters) * state.numberOfRegisters - locals.fee; + + // Log detailed revenue distribution information + locals.revenueLog = QRAFFLERevenueLogger{ + QRAFFLE_CONTRACT_INDEX, + QRAFFLE_revenueDistributed, + locals.burnAmount, + locals.charityRevenue, + div(locals.shareholderRevenue, 676) * 676, + div(locals.registerRevenue, state.numberOfRegisters) * state.numberOfRegisters, + locals.fee, + locals.winnerRevenue, + 0 + }; + LOG_INFO(locals.revenueLog); + + // Execute transfers and log each distribution + qpi.transfer(locals.winner, locals.winnerRevenue); + qpi.burn(locals.burnAmount); + qpi.transfer(state.charityAddress, locals.charityRevenue); + qpi.distributeDividends(div(locals.shareholderRevenue, 676)); + qpi.transfer(state.feeAddress, locals.fee); + + // Update total amounts and log largest winner update + state.totalBurnAmount += locals.burnAmount; + state.totalCharityAmount += locals.charityRevenue; + state.totalShareholderAmount += div(locals.shareholderRevenue, 676) * 676; + state.totalRegisterAmount += div(locals.registerRevenue, state.numberOfRegisters) * state.numberOfRegisters; + state.totalFeeAmount += locals.fee; + state.totalWinnerAmount += locals.winnerRevenue; + if (locals.winnerRevenue > state.largestWinnerAmount) + { + state.largestWinnerAmount = locals.winnerRevenue; + } + + locals.idx = state.registers.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + qpi.transfer(state.registers.key(locals.idx), div(locals.registerRevenue, state.numberOfRegisters)); + locals.idx = state.registers.nextElementIndex(locals.idx); + } + + // Store QuRaffle results and log completion with detailed information + locals.qraffle.epochWinner = locals.winner; + locals.qraffle.receivedAmount = locals.winnerRevenue; + locals.qraffle.entryAmount = state.qREAmount; + locals.qraffle.numberOfMembers = state.numberOfQuRaffleMembers; + locals.qraffle.winnerIndex = locals.winnerIndex; + state.QuRaffles.set(qpi.epoch(), locals.qraffle); + + // Log QuRaffle completion with detailed information + locals.endEpochLog = QRAFFLEEndEpochLogger{ + QRAFFLE_CONTRACT_INDEX, + QRAFFLE_revenueDistributed, + qpi.epoch(), + state.numberOfQuRaffleMembers, + state.qREAmount * state.numberOfQuRaffleMembers, + locals.winnerRevenue, + locals.winnerIndex, + 0 + }; + LOG_INFO(locals.endEpochLog); + + if (state.epochQXMRRevenue >= 676) + { + locals.idx = state.shareholdersList.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.shareholder = state.shareholdersList.key(locals.idx); + qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.QXMRIssuer, SELF, SELF, div(state.epochQXMRRevenue, 676) * qpi.numberOfShares(locals.QraffleAsset, AssetOwnershipSelect::byOwner(locals.shareholder), AssetPossessionSelect::byPossessor(locals.shareholder)), locals.shareholder); + locals.idx = state.shareholdersList.nextElementIndex(locals.idx); + } + state.epochQXMRRevenue -= div(state.epochQXMRRevenue, 676) * 676; + } + } + else + { + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_emptyQuRaffle, 0 }; + LOG_INFO(locals.log); + } + + // Process each active token raffle and log + for (locals.i = 0 ; locals.i < state.numberOfActiveTokenRaffle; locals.i++) + { + if (state.numberOfTokenRaffleMembers.get(locals.i) > 0) + { + locals.winnerIndex = (uint32)mod(locals.r, state.numberOfTokenRaffleMembers.get(locals.i) * 1ull); + state.tokenRaffleMembers.get(locals.i, state.tmpTokenRaffleMembers); + locals.winner = state.tmpTokenRaffleMembers.get(locals.winnerIndex); + + locals.acTokenRaffle = state.activeTokenRaffle.get(locals.i); + + // Calculate token raffle fee distributions + locals.burnAmount = div(locals.acTokenRaffle.entryAmount * state.numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_BURN_FEE, 100); + locals.charityRevenue = div(locals.acTokenRaffle.entryAmount * state.numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_CHARITY_FEE, 100); + locals.shareholderRevenue = div(locals.acTokenRaffle.entryAmount * state.numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_SHRAEHOLDER_FEE, 100); + locals.registerRevenue = div(locals.acTokenRaffle.entryAmount * state.numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_REGISTER_FEE, 100); + locals.fee = div(locals.acTokenRaffle.entryAmount * state.numberOfTokenRaffleMembers.get(locals.i) * QRAFFLE_FEE, 100); + locals.winnerRevenue = locals.acTokenRaffle.entryAmount * state.numberOfTokenRaffleMembers.get(locals.i) - locals.burnAmount - locals.charityRevenue - div(locals.shareholderRevenue, 676) * 676 - div(locals.registerRevenue, state.numberOfRegisters) * state.numberOfRegisters - locals.fee; + + // Execute token transfers and log each + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.winnerRevenue, locals.winner); + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.burnAmount, NULL_ID); + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.charityRevenue, state.charityAddress); + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.fee, state.feeAddress); + + locals.idx = state.shareholdersList.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.shareholder = state.shareholdersList.key(locals.idx); + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, div(locals.shareholderRevenue, 676) * qpi.numberOfShares(locals.acTokenRaffle.token, AssetOwnershipSelect::byOwner(locals.shareholder), AssetPossessionSelect::byPossessor(locals.shareholder)), locals.shareholder); + locals.idx = state.shareholdersList.nextElementIndex(locals.idx); + } + + locals.idx = state.registers.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, div(locals.registerRevenue, state.numberOfRegisters), state.registers.key(locals.idx)); + locals.idx = state.registers.nextElementIndex(locals.idx); + } + + locals.tRaffle.epochWinner = locals.winner; + locals.tRaffle.token.assetName = locals.acTokenRaffle.token.assetName; + locals.tRaffle.token.issuer = locals.acTokenRaffle.token.issuer; + locals.tRaffle.entryAmount = locals.acTokenRaffle.entryAmount; + locals.tRaffle.numberOfMembers = state.numberOfTokenRaffleMembers.get(locals.i); + locals.tRaffle.winnerIndex = locals.winnerIndex; + locals.tRaffle.epoch = qpi.epoch(); + state.tokenRaffle.set(state.numberOfEndedTokenRaffle, locals.tRaffle); + + // Log token raffle ended with detailed information + locals.tokenRaffleLog = QRAFFLETokenRaffleLogger{ + QRAFFLE_CONTRACT_INDEX, + QRAFFLE_tokenRaffleEnded, + state.numberOfEndedTokenRaffle++, + locals.acTokenRaffle.token.assetName, + state.numberOfTokenRaffleMembers.get(locals.i), + locals.acTokenRaffle.entryAmount, + locals.winnerIndex, + locals.winnerRevenue, + 0 + }; + LOG_INFO(locals.tokenRaffleLog); + + state.numberOfTokenRaffleMembers.set(locals.i, 0); + } + else + { + locals.emptyTokenRafflelog = QRAFFLEEmptyTokenRaffleLogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_emptyTokenRaffle, locals.i, 0 }; + LOG_INFO(locals.emptyTokenRafflelog); + } + } + + // Calculate new qREAmount and log + locals.log = QRAFFLELogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_revenueDistributed, 0 }; + LOG_INFO(locals.log); + + locals.sumOfEntryAmountSubmitted = 0; + locals.idx = state.quRaffleEntryAmount.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) + { + locals.sumOfEntryAmountSubmitted += state.quRaffleEntryAmount.value(locals.idx); + locals.idx = state.quRaffleEntryAmount.nextElementIndex(locals.idx); + } + if (state.numberOfEntryAmountSubmitted > 0) + { + state.qREAmount = div(locals.sumOfEntryAmountSubmitted, state.numberOfEntryAmountSubmitted); + } + else + { + state.qREAmount = QRAFFLE_DEFAULT_QRAFFLE_AMOUNT; + } + + state.numberOfActiveTokenRaffle = 0; + + // Process approved proposals and create new token raffles + for (locals.i = 0 ; locals.i < state.numberOfProposals; locals.i++) + { + locals.proposal = state.proposals.get(locals.i); + + // Log proposal processing with detailed information + locals.proposalLog = QRAFFLEProposalLogger{ + QRAFFLE_CONTRACT_INDEX, + QRAFFLE_proposalSubmitted, + locals.i, + locals.proposal.proposer, + locals.proposal.nYes, + locals.proposal.nNo, + locals.proposal.token.assetName, + locals.proposal.entryAmount, + 0 + }; + LOG_INFO(locals.proposalLog); + + if (locals.proposal.nYes > locals.proposal.nNo) + { + locals.acTokenRaffle.token.assetName = locals.proposal.token.assetName; + locals.acTokenRaffle.token.issuer = locals.proposal.token.issuer; + locals.acTokenRaffle.entryAmount = locals.proposal.entryAmount; + + state.activeTokenRaffle.set(state.numberOfActiveTokenRaffle++, locals.acTokenRaffle); + } + } + + state.numberOfVotedInProposal.setAll(0); + state.tokenRaffleMembers.reset(); + state.quRaffleEntryAmount.reset(); + state.shareholdersList.reset(); + state.voteStatus.reset(); + state.numberOfEntryAmountSubmitted = 0; + state.numberOfProposals = 0; + state.numberOfQuRaffleMembers = 0; + state.registers.cleanupIfNeeded(); + } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } +}; diff --git a/src/contracts/QUtil.h b/src/contracts/QUtil.h index d9e957bb0..fe39068a5 100644 --- a/src/contracts/QUtil.h +++ b/src/contracts/QUtil.h @@ -14,12 +14,15 @@ using namespace QPI; */ // Return code for logger and return struct + +constexpr uint64 QUTIL_CONTRACT_ASSET_NAME = 327647778129; + constexpr uint64 QUTIL_STM1_SUCCESS = 0; constexpr uint64 QUTIL_STM1_INVALID_AMOUNT_NUMBER = 1; constexpr uint64 QUTIL_STM1_WRONG_FUND = 2; constexpr uint64 QUTIL_STM1_TRIGGERED = 3; constexpr uint64 QUTIL_STM1_SEND_FUND = 4; -constexpr sint64 QUTIL_STM1_INVOCATION_FEE = 10LL; // fee to be burned and make the SC running +constexpr sint64 QUTIL_STM1_INVOCATION_FEE = 10LL; // fee to be burned and make the SC running (initial value) // Voting-specific constants constexpr uint64 QUTIL_POLL_TYPE_QUBIC = 1; @@ -29,33 +32,38 @@ constexpr uint64 QUTIL_MAX_VOTERS_PER_POLL = 131072; constexpr uint64 QUTIL_TOTAL_VOTERS = QUTIL_MAX_POLL * QUTIL_MAX_VOTERS_PER_POLL; constexpr uint64 QUTIL_MAX_OPTIONS = 64; // Maximum voting options (0 to 63) constexpr uint64 QUTIL_MAX_ASSETS_PER_POLL = 16; // Maximum assets per poll -constexpr sint64 QUTIL_VOTE_FEE = 100LL; // Fee for voting, burnt 100% -constexpr sint64 QUTIL_POLL_CREATION_FEE = 10000000LL; // Fee for poll creation to prevent spam +constexpr sint64 QUTIL_VOTE_FEE = 100LL; // Fee for voting, burnt 100% (initial value) +constexpr sint64 QUTIL_POLL_CREATION_FEE = 10000000LL; // Fee for poll creation to prevent spam (initial value) constexpr uint16 QUTIL_POLL_GITHUB_URL_MAX_SIZE = 256; // Max String Length for Poll's Github URLs -constexpr uint64 QUTIL_MAX_NEW_POLL = QUTIL_MAX_POLL / 4; // Max number of new poll per epoch +constexpr uint64 QUTIL_MAX_NEW_POLL = div(QUTIL_MAX_POLL, 4ULL); // Max number of new poll per epoch // Voting log types enum -const uint64 QutilLogTypePollCreated = 5; // Poll created successfully -const uint64 QutilLogTypeInsufficientFundsForPoll = 6; // Insufficient funds for poll creation -const uint64 QutilLogTypeInvalidPollType = 7; // Invalid poll type -const uint64 QutilLogTypeInvalidNumAssetsQubic = 8; // Invalid number of assets for Qubic poll -const uint64 QutilLogTypeInvalidNumAssetsAsset = 9; // Invalid number of assets for Asset poll -const uint64 QutilLogTypeVoteCast = 10; // Vote cast successfully -const uint64 QutilLogTypeInsufficientFundsForVote = 11; // Insufficient funds for voting -const uint64 QutilLogTypeInvalidPollId = 12; // Invalid poll ID -const uint64 QutilLogTypePollInactive = 13; // Poll is inactive -const uint64 QutilLogTypeInsufficientBalance = 14; // Insufficient voter balance -const uint64 QutilLogTypeInvalidOption = 15; // Invalid voting option -const uint64 QutilLogTypeInvalidPollIdResult = 16; // Invalid poll ID in GetCurrentResult -const uint64 QutilLogTypePollInactiveResult = 17; // Poll inactive in GetCurrentResult -const uint64 QutilLogTypeNoPollsByCreator = 18; // No polls found in GetPollsByCreator -const uint64 QutilLogTypePollCancelled = 19; // Poll cancelled successfully -const uint64 QutilLogTypeNotAuthorized = 20; // Not authorized to cancel the poll -const uint64 QutilLogTypeInsufficientFundsForCancel = 21; // Not have enough funds for poll calcellation -const uint64 QutilLogTypeMaxPollsReached = 22; // Max epoch per epoch reached - -struct QUtilLogger +constexpr uint64 QUTILLogTypePollCreated = 5; // Poll created successfully +constexpr uint64 QUTILLogTypeInsufficientFundsForPoll = 6; // Insufficient funds for poll creation +constexpr uint64 QUTILLogTypeInvalidPollType = 7; // Invalid poll type +constexpr uint64 QUTILLogTypeInvalidNumAssetsQubic = 8; // Invalid number of assets for Qubic poll +constexpr uint64 QUTILLogTypeInvalidNumAssetsAsset = 9; // Invalid number of assets for Asset poll +constexpr uint64 QUTILLogTypeVoteCast = 10; // Vote cast successfully +constexpr uint64 QUTILLogTypeInsufficientFundsForVote = 11; // Insufficient funds for voting +constexpr uint64 QUTILLogTypeInvalidPollId = 12; // Invalid poll ID +constexpr uint64 QUTILLogTypePollInactive = 13; // Poll is inactive +constexpr uint64 QUTILLogTypeInsufficientBalance = 14; // Insufficient voter balance +constexpr uint64 QUTILLogTypeInvalidOption = 15; // Invalid voting option +constexpr uint64 QUTILLogTypeInvalidPollIdResult = 16; // Invalid poll ID in GetCurrentResult +constexpr uint64 QUTILLogTypePollInactiveResult = 17; // Poll inactive in GetCurrentResult +constexpr uint64 QUTILLogTypeNoPollsByCreator = 18; // No polls found in GetPollsByCreator +constexpr uint64 QUTILLogTypePollCancelled = 19; // Poll cancelled successfully +constexpr uint64 QUTILLogTypeNotAuthorized = 20; // Not authorized to cancel the poll +constexpr uint64 QUTILLogTypeInsufficientFundsForCancel = 21; // Not have enough funds for poll calcellation +constexpr uint64 QUTILLogTypeMaxPollsReached = 22; // Max epoch per epoch reached + +// Fee per shareholder for DistributeQuToShareholders() (initial value) +constexpr sint64 QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER = 5; + +constexpr uint32 QUTIL_STMB_LOG_TYPE = 100001; // for bob to index + +struct QUTILLogger { uint32 contractId; // to distinguish bw SCs uint32 padding; @@ -64,11 +72,34 @@ struct QUtilLogger sint64 amt; uint32 logtype; // Other data go here - char _terminator; // Only data before "_terminator" are logged + sint8 _terminator; // Only data before "_terminator" are logged }; +struct QUTILSendToManyBenchmarkLog +{ + uint32 contractId; // to distinguish bw SCs + uint32 logType; + id startId; + sint64 dstCount; + sint8 _terminator; // Only data before "_terminator" are logged +}; + +// Deactivate logger for delay function +#if 0 +struct QUTILDFLogger +{ + uint32 contractId; // to distinguish bw SCs + uint32 padding; + id dfNonce; + id dfPubkey; + id dfMiningSeed; + id result; + sint8 _terminator; // Only data before "_terminator" are logged +}; +#endif + // poll and voter structs -struct QUtilPoll { +struct QUTILPoll { id poll_name; uint64 poll_type; // QUTIL_POLL_TYPE_QUBIC or QUTIL_POLL_TYPE_ASSET uint64 min_amount; // Minimum Qubic/asset amount for eligibility @@ -78,7 +109,7 @@ struct QUtilPoll { uint64 num_assets; // Number of assets in allowed assets }; -struct QUtilVoter { +struct QUTILVoter { id address; uint64 amount; uint64 chosen_option; // Limited to 0-63 by vote procedure @@ -98,14 +129,37 @@ struct QUTIL : public ContractBase sint64 total; // Voting state - Array polls; - Array voters; // 1d array for all voters + Array polls; + Array voters; // 1d array for all voters Array poll_ids; Array voter_counts; // tracks number of voters per poll Array, QUTIL_MAX_POLL> poll_links; // github links for polls uint64 current_poll_id; uint64 new_polls_this_epoch; + // DF function variables + m256i dfMiningSeed; + m256i dfCurrentState; + + // Fees + sint64 smt1InvocationFee; + sint64 pollCreationFee; + sint64 pollVoteFee; + sint64 distributeQuToShareholderFeePerShareholder; + sint64 shareholderProposalFee; + + // Placeholder for future fees, preventing that state has to be extended for every new fee + sint64 _futureFeePlaceholder0; + sint64 _futureFeePlaceholder1; + sint64 _futureFeePlaceholder2; + sint64 _futureFeePlaceholder3; + sint64 _futureFeePlaceholder4; + sint64 _futureFeePlaceholder5; + + // Provide storage for shareholder proposal state (supporting up to 8 simultaneous proposals) + DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(8, QUTIL_CONTRACT_ASSET_NAME); + + // Get Qubic Balance struct get_qubic_balance_input { id address; @@ -128,7 +182,7 @@ struct QUTIL : public ContractBase struct get_asset_balance_locals { }; - // Get QUtilVoter Balance Helper + // Get QUTILVoter Balance Helper struct get_voter_balance_input { uint64 poll_idx; id address; @@ -150,7 +204,7 @@ struct QUTIL : public ContractBase get_asset_balance_locals gab_locals; }; - // Swap QUtilVoter to the end of the array helper + // Swap QUTILVoter to the end of the array helper struct swap_voter_to_end_input { uint64 poll_idx; uint64 i; @@ -161,7 +215,7 @@ struct QUTIL : public ContractBase struct swap_voter_to_end_locals { uint64 voter_index_i; uint64 voter_index_end; - QUtilVoter temp_voter; + QUTILVoter temp_voter; }; public: @@ -181,7 +235,7 @@ struct QUTIL : public ContractBase }; struct SendToManyV1_locals { - QUtilLogger logger; + QUTILLogger logger; }; struct GetSendToManyV1Fee_input @@ -209,7 +263,9 @@ struct QUTIL : public ContractBase id currentId; sint64 t; uint64 useNext; - QUtilLogger logger; + uint64 totalNumTransfers; + QUTILLogger logger; + QUTILSendToManyBenchmarkLog logBenchmark; }; struct BurnQubic_input @@ -221,6 +277,24 @@ struct QUTIL : public ContractBase sint64 amount; }; + struct BurnQubicForContract_input + { + uint32 contractIndexBurnedFor; + }; + struct BurnQubicForContract_output + { + sint64 amount; + }; + + struct QueryFeeReserve_input + { + uint32 contractIndex; + }; + struct QueryFeeReserve_output + { + sint64 reserveAmount; + }; + typedef Asset GetTotalNumberOfAssetShares_input; typedef sint64 GetTotalNumberOfAssetShares_output; @@ -240,10 +314,10 @@ struct QUTIL : public ContractBase struct CreatePoll_locals { uint64 idx; - QUtilPoll new_poll; - QUtilVoter default_voter; + QUTILPoll new_poll; + QUTILVoter default_voter; uint64 i; - QUtilLogger logger; + QUTILLogger logger; }; struct Vote_input @@ -271,11 +345,11 @@ struct QUTIL : public ContractBase swap_voter_to_end_locals sve_locals; uint64 i; uint64 voter_index; - QUtilVoter temp_voter; + QUTILVoter temp_voter; uint64 real_vote; uint64 end_idx; uint64 max_balance; - QUtilLogger logger; + QUTILLogger logger; }; struct CancelPoll_input @@ -291,8 +365,8 @@ struct QUTIL : public ContractBase struct CancelPoll_locals { uint64 idx; - QUtilPoll current_poll; - QUtilLogger logger; + QUTILPoll current_poll; + QUTILLogger logger; }; struct GetCurrentResult_input @@ -310,10 +384,10 @@ struct QUTIL : public ContractBase uint64 idx; uint64 poll_type; uint64 effective_amount; - QUtilVoter voter; + QUTILVoter voter; uint64 i; uint64 voter_index; - QUtilLogger logger; + QUTILLogger logger; }; struct GetPollsByCreator_input @@ -328,7 +402,7 @@ struct QUTIL : public ContractBase struct GetPollsByCreator_locals { uint64 idx; - QUtilLogger logger; + QUTILLogger logger; }; struct GetCurrentPollId_input @@ -352,29 +426,38 @@ struct QUTIL : public ContractBase }; struct GetPollInfo_output { - uint64 found; // 1 if exists, 0 ig not - QUtilPoll poll_info; + uint64 found; // 1 if exists, 0 if not + QUTILPoll poll_info; Array poll_link; }; struct GetPollInfo_locals { uint64 idx; - QUtilPoll default_poll; // default values if not found + QUTILPoll default_poll; // default values if not found }; - struct END_EPOCH_locals + typedef NoData GetFees_input; + struct GetFees_output { - uint64 i; - QUtilPoll current_poll; + sint64 smt1InvocationFee; + sint64 pollCreationFee; + sint64 pollVoteFee; + sint64 distributeQuToShareholderFeePerShareholder; + sint64 shareholderProposalFee; + + // Placeholder for future fees, preventing incompatibilities for some extensions + sint64 _futureFeePlaceholder0; + sint64 _futureFeePlaceholder1; + sint64 _futureFeePlaceholder2; + sint64 _futureFeePlaceholder3; + sint64 _futureFeePlaceholder4; + sint64 _futureFeePlaceholder5; }; - struct BEGIN_EPOCH_locals + struct END_EPOCH_locals { uint64 i; - uint64 j; - QUtilPoll default_poll; - QUtilVoter default_voter; - Array zero_link; + QUTILPoll current_poll; }; /**************************************/ @@ -432,7 +515,7 @@ struct QUTIL : public ContractBase state.voters.set(locals.voter_index_end, locals.temp_voter); } - // Calculate QUtilVoter Index + // Calculate QUTILVoter Index inline static uint64 calculate_voter_index(uint64 poll_idx, uint64 voter_idx) { return poll_idx * QUTIL_MAX_VOTERS_PER_POLL + voter_idx; @@ -440,30 +523,25 @@ struct QUTIL : public ContractBase static inline bit check_github_prefix(const Array& github_link) { - return github_link.get(0) == 'h' && - github_link.get(1) == 't' && - github_link.get(2) == 't' && - github_link.get(3) == 'p' && - github_link.get(4) == 's' && - github_link.get(5) == ':' && - github_link.get(6) == '/' && - github_link.get(7) == '/' && - github_link.get(8) == 'g' && - github_link.get(9) == 'i' && - github_link.get(10) == 't' && - github_link.get(11) == 'h' && - github_link.get(12) == 'u' && - github_link.get(13) == 'b' && - github_link.get(14) == '.' && - github_link.get(15) == 'c' && - github_link.get(16) == 'o' && - github_link.get(17) == 'm' && - github_link.get(18) == '/' && - github_link.get(19) == 'q' && - github_link.get(20) == 'u' && - github_link.get(21) == 'b' && - github_link.get(22) == 'i' && - github_link.get(23) == 'c'; + return github_link.get(0) == 104 && // 'h' + github_link.get(1) == 116 && // 't' + github_link.get(2) == 116 && // 't' + github_link.get(3) == 112 && // 'p' + github_link.get(4) == 115 && // 's' + github_link.get(5) == 58 && // ':' + github_link.get(6) == 47 && // '/' + github_link.get(7) == 47 && // '/' + github_link.get(8) == 103 && // 'g' + github_link.get(9) == 105 && // 'i' + github_link.get(10) == 116 && // 't' + github_link.get(11) == 104 && // 'h' + github_link.get(12) == 117 && // 'u' + github_link.get(13) == 98 && // 'b' + github_link.get(14) == 46 && // '.' + github_link.get(15) == 99 && // 'c' + github_link.get(16) == 111 && // 'o' + github_link.get(17) == 109 && // 'm' + github_link.get(18) == 47; // '/' } /**************************************/ @@ -474,7 +552,7 @@ struct QUTIL : public ContractBase */ PUBLIC_FUNCTION(GetSendToManyV1Fee) { - output.fee = QUTIL_STM1_INVOCATION_FEE; + output.fee = state.smt1InvocationFee; } /** @@ -485,24 +563,80 @@ struct QUTIL : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(SendToManyV1) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; LOG_INFO(locals.logger); - state.total = input.amt0 + input.amt1 + input.amt2 + input.amt3 + input.amt4 + input.amt5 + input.amt6 + input.amt7 + input.amt8 + input.amt9 + input.amt10 + input.amt11 + input.amt12 + input.amt13 + input.amt14 + input.amt15 + input.amt16 + input.amt17 + input.amt18 + input.amt19 + input.amt20 + input.amt21 + input.amt22 + input.amt23 + input.amt24 + QUTIL_STM1_INVOCATION_FEE; - // invalid amount (<0), return fund and exit - if ((input.amt0 < 0) || (input.amt1 < 0) || (input.amt2 < 0) || (input.amt3 < 0) || (input.amt4 < 0) || (input.amt5 < 0) || (input.amt6 < 0) || (input.amt7 < 0) || (input.amt8 < 0) || (input.amt9 < 0) || (input.amt10 < 0) || (input.amt11 < 0) || (input.amt12 < 0) || (input.amt13 < 0) || (input.amt14 < 0) || (input.amt15 < 0) || (input.amt16 < 0) || (input.amt17 < 0) || (input.amt18 < 0) || (input.amt19 < 0) || (input.amt20 < 0) || (input.amt21 < 0) || (input.amt22 < 0) || (input.amt23 < 0) || (input.amt24 < 0)) - { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; + + // invalid amount (<0 or >= MAX_AMOUNT), return fund and exit + if ((input.amt0 < 0) || (input.amt0 >= MAX_AMOUNT) + || (input.amt1 < 0) || (input.amt1 >= MAX_AMOUNT) + || (input.amt2 < 0) || (input.amt2 >= MAX_AMOUNT) + || (input.amt3 < 0) || (input.amt3 >= MAX_AMOUNT) + || (input.amt4 < 0) || (input.amt4 >= MAX_AMOUNT) + || (input.amt5 < 0) || (input.amt5 >= MAX_AMOUNT) + || (input.amt6 < 0) || (input.amt6 >= MAX_AMOUNT) + || (input.amt7 < 0) || (input.amt7 >= MAX_AMOUNT) + || (input.amt8 < 0) || (input.amt8 >= MAX_AMOUNT) + || (input.amt9 < 0) || (input.amt9 >= MAX_AMOUNT) + || (input.amt10 < 0) || (input.amt10 >= MAX_AMOUNT) + || (input.amt11 < 0) || (input.amt11 >= MAX_AMOUNT) + || (input.amt12 < 0) || (input.amt12 >= MAX_AMOUNT) + || (input.amt13 < 0) || (input.amt13 >= MAX_AMOUNT) + || (input.amt14 < 0) || (input.amt14 >= MAX_AMOUNT) + || (input.amt15 < 0) || (input.amt15 >= MAX_AMOUNT) + || (input.amt16 < 0) || (input.amt16 >= MAX_AMOUNT) + || (input.amt17 < 0) || (input.amt17 >= MAX_AMOUNT) + || (input.amt18 < 0) || (input.amt18 >= MAX_AMOUNT) + || (input.amt19 < 0) || (input.amt19 >= MAX_AMOUNT) + || (input.amt20 < 0) || (input.amt20 >= MAX_AMOUNT) + || (input.amt21 < 0) || (input.amt21 >= MAX_AMOUNT) + || (input.amt22 < 0) || (input.amt22 >= MAX_AMOUNT) + || (input.amt23 < 0) || (input.amt23 >= MAX_AMOUNT) + || (input.amt24 < 0) || (input.amt24 >= MAX_AMOUNT)) + { + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; output.returnCode = QUTIL_STM1_INVALID_AMOUNT_NUMBER; LOG_INFO(locals.logger); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + return; } + + // Make sure that the sum of all amounts does not overflow and is equal to qpi.invocationReward() + state.total = qpi.invocationReward(); + state.total -= input.amt0; if (state.total < 0) goto exit; + state.total -= input.amt1; if (state.total < 0) goto exit; + state.total -= input.amt2; if (state.total < 0) goto exit; + state.total -= input.amt3; if (state.total < 0) goto exit; + state.total -= input.amt4; if (state.total < 0) goto exit; + state.total -= input.amt5; if (state.total < 0) goto exit; + state.total -= input.amt6; if (state.total < 0) goto exit; + state.total -= input.amt7; if (state.total < 0) goto exit; + state.total -= input.amt8; if (state.total < 0) goto exit; + state.total -= input.amt9; if (state.total < 0) goto exit; + state.total -= input.amt10; if (state.total < 0) goto exit; + state.total -= input.amt11; if (state.total < 0) goto exit; + state.total -= input.amt12; if (state.total < 0) goto exit; + state.total -= input.amt13; if (state.total < 0) goto exit; + state.total -= input.amt14; if (state.total < 0) goto exit; + state.total -= input.amt15; if (state.total < 0) goto exit; + state.total -= input.amt16; if (state.total < 0) goto exit; + state.total -= input.amt17; if (state.total < 0) goto exit; + state.total -= input.amt18; if (state.total < 0) goto exit; + state.total -= input.amt19; if (state.total < 0) goto exit; + state.total -= input.amt20; if (state.total < 0) goto exit; + state.total -= input.amt21; if (state.total < 0) goto exit; + state.total -= input.amt22; if (state.total < 0) goto exit; + state.total -= input.amt23; if (state.total < 0) goto exit; + state.total -= input.amt24; if (state.total < 0) goto exit; + state.total -= state.smt1InvocationFee; if (state.total < 0) goto exit; + // insufficient or too many qubic transferred, return fund and exit (we don't want to return change) - if (qpi.invocationReward() != state.total) + if (state.total != 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_WRONG_FUND }; + exit: + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_WRONG_FUND }; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_WRONG_FUND; if (qpi.invocationReward() > 0) @@ -514,158 +648,158 @@ struct QUTIL : public ContractBase if (input.dst0 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst0, input.amt0, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst0, input.amt0, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst0, input.amt0); } if (input.dst1 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst1, input.amt1, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst1, input.amt1, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst1, input.amt1); } if (input.dst2 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst2, input.amt2, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst2, input.amt2, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst2, input.amt2); } if (input.dst3 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst3, input.amt3, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst3, input.amt3, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst3, input.amt3); } if (input.dst4 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst4, input.amt4, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst4, input.amt4, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst4, input.amt4); } if (input.dst5 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst5, input.amt5, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst5, input.amt5, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst5, input.amt5); } if (input.dst6 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst6, input.amt6, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst6, input.amt6, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst6, input.amt6); } if (input.dst7 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst7, input.amt7, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst7, input.amt7, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst7, input.amt7); } if (input.dst8 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst8, input.amt8, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst8, input.amt8, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst8, input.amt8); } if (input.dst9 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst9, input.amt9, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst9, input.amt9, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst9, input.amt9); } if (input.dst10 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst10, input.amt10, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst10, input.amt10, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst10, input.amt10); } if (input.dst11 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst11, input.amt11, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst11, input.amt11, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst11, input.amt11); } if (input.dst12 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst12, input.amt12, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst12, input.amt12, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst12, input.amt12); } if (input.dst13 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst13, input.amt13, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst13, input.amt13, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst13, input.amt13); } if (input.dst14 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst14, input.amt14, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst14, input.amt14, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst14, input.amt14); } if (input.dst15 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst15, input.amt15, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst15, input.amt15, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst15, input.amt15); } if (input.dst16 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst16, input.amt16, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst16, input.amt16, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst16, input.amt16); } if (input.dst17 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst17, input.amt17, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst17, input.amt17, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst17, input.amt17); } if (input.dst18 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst18, input.amt18, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst18, input.amt18, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst18, input.amt18); } if (input.dst19 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst19, input.amt19, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst19, input.amt19, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst19, input.amt19); } if (input.dst20 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst20, input.amt20, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst20, input.amt20, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst20, input.amt20); } if (input.dst21 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst21, input.amt21, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst21, input.amt21, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst21, input.amt21); } if (input.dst22 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst22, input.amt22, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst22, input.amt22, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst22, input.amt22); } if (input.dst23 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst23, input.amt23, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst23, input.amt23, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst23, input.amt23); } if (input.dst24 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst24, input.amt24, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst24, input.amt24, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst24, input.amt24); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, state.total, QUTIL_STM1_SUCCESS }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_SUCCESS}; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_SUCCESS; - qpi.burn(QUTIL_STM1_INVOCATION_FEE); + qpi.burn(state.smt1InvocationFee); } /** @@ -677,46 +811,54 @@ struct QUTIL : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(SendToManyBenchmark) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; LOG_INFO(locals.logger); output.total = 0; // Number of addresses and transfers is > 0 and total transfers do not exceed limit (including 2 transfers from invocator to contract and contract to invocator) - if (input.dstCount <= 0 || input.numTransfersEach <= 0 || input.dstCount * input.numTransfersEach + 2 > CONTRACT_ACTION_TRACKER_SIZE) + locals.totalNumTransfers = smul((uint64)input.dstCount, (uint64)input.numTransfersEach); + if (input.dstCount <= 0 || input.numTransfersEach <= 0 || locals.totalNumTransfers > CONTRACT_ACTION_TRACKER_SIZE - 2) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_INVALID_AMOUNT_NUMBER; return; } // Check the fund is enough - if (qpi.invocationReward() < input.dstCount * input.numTransfersEach) + if ((uint64)qpi.invocationReward() < locals.totalNumTransfers) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_INVALID_AMOUNT_NUMBER; return; } - // Loop through the number of addresses and do the transfers locals.currentId = qpi.invocator(); locals.useNext = 1; + + locals.logBenchmark.startId = qpi.invocator(); + locals.logBenchmark.logType = QUTIL_STMB_LOG_TYPE; + locals.logBenchmark.dstCount = input.dstCount; + LOG_INFO(locals.logBenchmark); + + LOG_PAUSE(); + while (output.dstCount < input.dstCount) { if (locals.useNext == 1) locals.currentId = qpi.nextId(locals.currentId); else locals.currentId = qpi.prevId(locals.currentId); - if (locals.currentId == m256i::zero()) + if (locals.currentId == id::zero()) { locals.currentId = qpi.invocator(); locals.useNext = 1 - locals.useNext; @@ -730,6 +872,7 @@ struct QUTIL : public ContractBase output.total += 1; } } + LOG_RESUME(); // Return the change if there is any if (output.total < qpi.invocationReward()) @@ -737,14 +880,14 @@ struct QUTIL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - output.total); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, output.total, QUTIL_STM1_SUCCESS }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, output.total, QUTIL_STM1_SUCCESS }; LOG_INFO(locals.logger); } /** * Practicing burning qubic in the QChurch * @param the amount of qubic to burn - * @return the amount of qubic has burned, < 0 if failed to burn + * @return the amount of qubic that was burned, < 0 if failed to burn */ PUBLIC_PROCEDURE(BurnQubic) { @@ -767,13 +910,54 @@ struct QUTIL : public ContractBase } if (qpi.invocationReward() > input.amount) // send more than qu to burn { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount); // return the changes + qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount); // return the change + } + // if qpi.burn() succeeds, it returns the remaining amount of QUTIL's Qu balance (>= 0), otherwise some value < 0 + output.amount = qpi.burn(input.amount); + if (output.amount >= 0) + output.amount = input.amount; + else + { + qpi.transfer(qpi.invocator(), input.amount); // refund in case of failure to burn + output.amount = -1; } - qpi.burn(input.amount); - output.amount = input.amount; return; } + /** + * Burn the qubic passed as invocation reward for the contract specified in the input + * @param the contract index to burn for + * @return the amount of qubic that was burned, < 0 if failed to burn + */ + PUBLIC_PROCEDURE(BurnQubicForContract) + { + if (qpi.invocationReward() <= 0) // not sending enough qu to burn + { + output.amount = -1; + return; + } + // if qpi.burn() succeeds, it returns the remaining amount of QUTIL's Qu balance (>= 0), otherwise some value < 0 + output.amount = qpi.burn(qpi.invocationReward(), input.contractIndexBurnedFor); + if (output.amount >= 0) + output.amount = qpi.invocationReward(); + else + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); // refund in case of failure to burn + output.amount = -1; + } + return; + } + + /** + * Query the amount of qubic in the fee reserve of the specified contract + * @param the contract index to query + * @return the amount of qubic in the reserve + */ + PUBLIC_FUNCTION(QueryFeeReserve) + { + output.reserveAmount = qpi.queryFeeReserve(input.contractIndex); + } + /** * Create a new poll with min amount, and GitHub link, and list of allowed assets */ @@ -782,16 +966,16 @@ struct QUTIL : public ContractBase // max new poll exceeded if (state.new_polls_this_epoch >= QUTIL_MAX_NEW_POLL) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeMaxPollsReached }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeMaxPollsReached }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } // insufficient fund - if (qpi.invocationReward() < QUTIL_POLL_CREATION_FEE) + if (qpi.invocationReward() < state.pollCreationFee) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QutilLogTypeInsufficientFundsForPoll }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTILLogTypeInsufficientFundsForPoll }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -800,7 +984,7 @@ struct QUTIL : public ContractBase // invalid poll type if (input.poll_type != QUTIL_POLL_TYPE_QUBIC && input.poll_type != QUTIL_POLL_TYPE_ASSET) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollType }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollType }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -809,7 +993,7 @@ struct QUTIL : public ContractBase // invalid number of assets in Qubic poll if (input.poll_type == QUTIL_POLL_TYPE_QUBIC && input.num_assets != 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidNumAssetsQubic }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidNumAssetsQubic }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -818,7 +1002,7 @@ struct QUTIL : public ContractBase // invalid number of assets in Asset poll if (input.poll_type == QUTIL_POLL_TYPE_ASSET && (input.num_assets == 0 || input.num_assets > QUTIL_MAX_ASSETS_PER_POLL)) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidNumAssetsAsset }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidNumAssetsAsset }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -826,14 +1010,14 @@ struct QUTIL : public ContractBase if (!check_github_prefix(input.github_link)) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollType }; // reusing existing log type for invalid GitHub link + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollType }; // reusing existing log type for invalid GitHub link LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - qpi.transfer(qpi.invocator(), qpi.invocationReward() - QUTIL_POLL_CREATION_FEE); - qpi.burn(QUTIL_POLL_CREATION_FEE); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.pollCreationFee); + qpi.burn(state.pollCreationFee); locals.idx = mod(state.current_poll_id, QUTIL_MAX_POLL); locals.new_poll.poll_name = input.poll_name; @@ -862,7 +1046,7 @@ struct QUTIL : public ContractBase state.new_polls_this_epoch++; - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, QUTIL_POLL_CREATION_FEE, QutilLogTypePollCreated }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, state.pollCreationFee, QUTILLogTypePollCreated }; LOG_INFO(locals.logger); } @@ -872,26 +1056,32 @@ struct QUTIL : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(Vote) { output.success = false; - if (qpi.invocationReward() < QUTIL_VOTE_FEE) + if (qpi.invocationReward() < state.pollVoteFee) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QutilLogTypeInsufficientFundsForVote }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTILLogTypeInsufficientFundsForVote }; LOG_INFO(locals.logger); return; } - qpi.transfer(qpi.invocator(), qpi.invocationReward() - QUTIL_VOTE_FEE); - qpi.burn(QUTIL_VOTE_FEE); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.pollVoteFee); + qpi.burn(state.pollVoteFee); + + if (qpi.invocator() != input.address) + { + // bad query + return; + } locals.idx = mod(input.poll_id, QUTIL_MAX_POLL); if (state.poll_ids.get(locals.idx) != input.poll_id) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollId }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollId }; LOG_INFO(locals.logger); return; } if (state.polls.get(locals.idx).is_active == 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypePollInactive }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypePollInactive }; LOG_INFO(locals.logger); return; } @@ -905,13 +1095,13 @@ struct QUTIL : public ContractBase if (locals.max_balance < state.polls.get(locals.idx).min_amount || locals.max_balance < input.amount) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInsufficientBalance }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInsufficientBalance }; LOG_INFO(locals.logger); return; } if (input.chosen_option >= QUTIL_MAX_OPTIONS) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidOption }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidOption }; LOG_INFO(locals.logger); return; } @@ -923,14 +1113,14 @@ struct QUTIL : public ContractBase if (state.voters.get(locals.voter_index).address == input.address) { // Update existing voter - state.voters.set(locals.voter_index, QUtilVoter{ input.address, input.amount, input.chosen_option }); + state.voters.set(locals.voter_index, QUTILVoter{ input.address, input.amount, input.chosen_option }); output.success = true; break; } else if (state.voters.get(locals.voter_index).address == NULL_ID) { // Add new voter in empty slot - state.voters.set(locals.voter_index, QUtilVoter{ input.address, input.amount, input.chosen_option }); + state.voters.set(locals.voter_index, QUTILVoter{ input.address, input.amount, input.chosen_option }); state.voter_counts.set(locals.idx, state.voter_counts.get(locals.idx) + 1); output.success = true; break; @@ -985,7 +1175,7 @@ struct QUTIL : public ContractBase if (locals.max_balance < state.polls.get(locals.idx).min_amount) { // Mark as invalid by setting address to NULL_ID - state.voters.set(locals.voter_index, QUtilVoter{ NULL_ID, 0, 0 }); + state.voters.set(locals.voter_index, QUTILVoter{ NULL_ID, 0, 0 }); // Swap with the last valid voter while (locals.end_idx > locals.i && state.voters.get(calculate_voter_index(locals.idx, locals.end_idx)).address == NULL_ID) { @@ -1016,16 +1206,16 @@ struct QUTIL : public ContractBase if (output.success) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, QUTIL_VOTE_FEE, QutilLogTypeVoteCast }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, state.pollVoteFee, QUTILLogTypeVoteCast }; LOG_INFO(locals.logger); } } PUBLIC_PROCEDURE_WITH_LOCALS(CancelPoll) { - if (qpi.invocationReward() < QUTIL_POLL_CREATION_FEE) + if (qpi.invocationReward() < state.pollCreationFee) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QutilLogTypeInsufficientFundsForCancel }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTILLogTypeInsufficientFundsForCancel }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); output.success = false; @@ -1036,7 +1226,7 @@ struct QUTIL : public ContractBase if (state.poll_ids.get(locals.idx) != input.poll_id) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollId }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollId }; LOG_INFO(locals.logger); output.success = false; qpi.transfer(qpi.invocator(), qpi.invocationReward()); @@ -1047,7 +1237,7 @@ struct QUTIL : public ContractBase if (locals.current_poll.creator != qpi.invocator()) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeNotAuthorized }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeNotAuthorized }; LOG_INFO(locals.logger); output.success = false; qpi.transfer(qpi.invocator(), qpi.invocationReward()); @@ -1056,7 +1246,7 @@ struct QUTIL : public ContractBase if (locals.current_poll.is_active == 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypePollInactive }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypePollInactive }; LOG_INFO(locals.logger); output.success = false; qpi.transfer(qpi.invocator(), qpi.invocationReward()); @@ -1066,12 +1256,12 @@ struct QUTIL : public ContractBase locals.current_poll.is_active = 0; state.polls.set(locals.idx, locals.current_poll); - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypePollCancelled }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypePollCancelled }; LOG_INFO(locals.logger); output.success = true; - qpi.transfer(qpi.invocator(), qpi.invocationReward() - QUTIL_POLL_CREATION_FEE); - qpi.burn(QUTIL_POLL_CREATION_FEE); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.pollCreationFee); + qpi.burn(state.pollCreationFee); } /** @@ -1082,8 +1272,6 @@ struct QUTIL : public ContractBase locals.idx = mod(input.poll_id, QUTIL_MAX_POLL); if (state.poll_ids.get(locals.idx) != input.poll_id) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollIdResult }; - LOG_INFO(locals.logger); return; } output.is_active = state.polls.get(locals.idx).is_active; @@ -1107,17 +1295,12 @@ struct QUTIL : public ContractBase output.count = 0; for (locals.idx = 0; locals.idx < QUTIL_MAX_POLL; locals.idx++) { - if (state.polls.get(locals.idx).is_active != 0 && state.polls.get(locals.idx).creator == input.creator) + if (state.polls.get(locals.idx).creator != NULL_ID && state.polls.get(locals.idx).creator == input.creator) { output.poll_ids.set(output.count, state.poll_ids.get(locals.idx)); output.count++; } } - if (output.count == 0) - { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeNoPollsByCreator }; - LOG_INFO(locals.logger); - } } /** @@ -1152,6 +1335,15 @@ struct QUTIL : public ContractBase } } + PUBLIC_FUNCTION(GetFees) + { + output.smt1InvocationFee = state.smt1InvocationFee; + output.pollCreationFee = state.pollCreationFee; + output.pollVoteFee = state.pollVoteFee; + output.distributeQuToShareholderFeePerShareholder = state.distributeQuToShareholderFeePerShareholder; + output.shareholderProposalFee = state.shareholderProposalFee; + } + /** * End of epoch processing for polls, sets active polls to inactive */ @@ -1169,8 +1361,47 @@ struct QUTIL : public ContractBase } state.new_polls_this_epoch = 0; + + // Check shareholder proposals and update fees if needed + CALL(FinalizeShareholderStateVarProposals, input, output); } + INITIALIZE() + { + // init fee state variables (only called in gtest, because INITIALIZE has been added a long time after IPO) + state.smt1InvocationFee = QUTIL_STM1_INVOCATION_FEE; + state.pollCreationFee = QUTIL_POLL_CREATION_FEE; + state.pollVoteFee = QUTIL_VOTE_FEE; + state.distributeQuToShareholderFeePerShareholder = QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + state.shareholderProposalFee = 100; + } + + BEGIN_EPOCH() + { + state.dfMiningSeed = qpi.getPrevSpectrumDigest(); + } + + // Deactivate delay function + #if 0 + struct BEGIN_TICK_locals + { + m256i dfPubkey, dfNonce; + QUTILDFLogger logger; + }; + /* + * A deterministic delay function + */ + BEGIN_TICK_WITH_LOCALS() + { + locals.dfPubkey = qpi.getPrevSpectrumDigest(); + locals.dfNonce = qpi.getPrevComputerDigest(); + state.dfCurrentState = qpi.computeMiningFunction(state.dfMiningSeed, locals.dfPubkey, locals.dfNonce); + + locals.logger = QUTILDFLogger{ 0, 0, locals.dfNonce, locals.dfPubkey, state.dfMiningSeed, state.dfCurrentState}; + LOG_INFO(locals.logger); + } + #endif + /* * @return Return total number of shares that currently exist of the asset given as input */ @@ -1179,6 +1410,108 @@ struct QUTIL : public ContractBase output = qpi.numberOfShares(input); } + struct DistributeQuToShareholders_input + { + Asset asset; + }; + struct DistributeQuToShareholders_output + { + sint64 shareholders; + sint64 totalShares; + sint64 amountPerShare; + sint64 fees; + }; + struct DistributeQuToShareholders_locals + { + AssetPossessionIterator iter; + sint64 payBack; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(DistributeQuToShareholders) + { + // 1. Compute fee (increases linear with number of shareholders) + // 1.1. Count shareholders and shares + for (locals.iter.begin(input.asset); !locals.iter.reachedEnd(); locals.iter.next()) + { + if (locals.iter.numberOfPossessedShares() > 0) + { + ++output.shareholders; + output.totalShares += locals.iter.numberOfPossessedShares(); + } + } + + // 1.2. Cancel if there are no shareholders + if (output.shareholders == 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // 1.3. Compute fee (proportional to number of shareholders) + output.fees = output.shareholders * state.distributeQuToShareholderFeePerShareholder; + + // 1.4. Compute QU per share + output.amountPerShare = div(qpi.invocationReward() - output.fees, output.totalShares); + + // 1.5. Cancel if amount is not sufficient to pay fees and at least one QU per share + if (output.amountPerShare <= 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // 1.6. compute payback QU (remainder of distribution) + locals.payBack = qpi.invocationReward() - output.totalShares * output.amountPerShare - output.fees; + ASSERT(locals.payBack >= 0); + + // 2. Distribute to shareholders + for (locals.iter.begin(input.asset); !locals.iter.reachedEnd(); locals.iter.next()) + { + if (locals.iter.numberOfPossessedShares() > 0) + { + qpi.transfer(locals.iter.possessor(), locals.iter.numberOfPossessedShares() * output.amountPerShare); + } + } + + // 3. Burn fee + qpi.burn(output.fees); + + // 4. pay back QU that cannot be evenly distributed + if (locals.payBack > 0) + { + qpi.transfer(qpi.invocator(), locals.payBack); + } + } + + /**************************************/ + /* SHAREHOLDER PROPOSALS */ + /**************************************/ + + IMPLEMENT_FinalizeShareholderStateVarProposals() + { + switch (input.proposal.variableOptions.variable) + { + case 0: + state.smt1InvocationFee = input.acceptedValue; + break; + case 1: + state.pollCreationFee = input.acceptedValue; + break; + case 2: + state.pollVoteFee = input.acceptedValue; + break; + case 3: + state.distributeQuToShareholderFeePerShareholder = input.acceptedValue; + break; + case 4: + state.shareholderProposalFee = input.acceptedValue; + break; + } + } + + IMPLEMENT_DEFAULT_SHAREHOLDER_PROPOSAL_VOTING(5, state.shareholderProposalFee) + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_FUNCTION(GetSendToManyV1Fee, 1); @@ -1187,6 +1520,8 @@ struct QUTIL : public ContractBase REGISTER_USER_FUNCTION(GetPollsByCreator, 4); REGISTER_USER_FUNCTION(GetCurrentPollId, 5); REGISTER_USER_FUNCTION(GetPollInfo, 6); + REGISTER_USER_FUNCTION(GetFees, 7); + REGISTER_USER_FUNCTION(QueryFeeReserve, 8); REGISTER_USER_PROCEDURE(SendToManyV1, 1); REGISTER_USER_PROCEDURE(BurnQubic, 2); @@ -1194,5 +1529,9 @@ struct QUTIL : public ContractBase REGISTER_USER_PROCEDURE(CreatePoll, 4); REGISTER_USER_PROCEDURE(Vote, 5); REGISTER_USER_PROCEDURE(CancelPoll, 6); + REGISTER_USER_PROCEDURE(DistributeQuToShareholders, 7); + REGISTER_USER_PROCEDURE(BurnQubicForContract, 8); + + REGISTER_SHAREHOLDER_PROPOSAL_VOTING(); } }; diff --git a/src/contracts/Qbay.h b/src/contracts/Qbay.h index c352da7df..950147f34 100644 --- a/src/contracts/Qbay.h +++ b/src/contracts/Qbay.h @@ -55,7 +55,7 @@ struct QBAYLogger { uint32 _contractIndex; uint32 _type; // Assign a random unique (per contract) number to distinguish messages of different types - char _terminator; // Only data before "_terminator" are logged + sint8 _terminator; // Only data before "_terminator" are logged }; struct QBAY2 @@ -491,14 +491,6 @@ struct QBAY : public ContractBase Array NFTs; - /** - * @return Current date from core node system - */ - - inline static void getCurrentDate(const QPI::QpiContextFunctionCall& qpi, uint32& res) - { - QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), res); - } struct settingCFBAndQubicPrice_locals { @@ -966,7 +958,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1033,7 +1025,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1109,7 +1101,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1247,7 +1239,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1322,7 +1314,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.possessedNFT).endTimeOfAuction || locals.curDate <= state.NFTs.get(input.anotherNFT).endTimeOfAuction) { @@ -1411,7 +1403,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.possessedNFT).endTimeOfAuction || locals.curDate <= state.NFTs.get(input.anotherNFT).endTimeOfAuction) { @@ -1477,7 +1469,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1629,7 +1621,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1728,7 +1720,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1824,7 +1816,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.startDate <= locals.curDate || locals.endDate <= locals.startDate) { @@ -1923,7 +1915,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate < state.NFTs.get(input.NFTId).startTimeOfAuction || locals.curDate > state.NFTs.get(input.NFTId).endTimeOfAuction) { @@ -2179,7 +2171,6 @@ struct QBAY : public ContractBase { // success output.transferredNumberOfShares = input.numberOfShares; - qpi.transfer(id(QX_CONTRACT_INDEX, 0, 0, 0), state.transferRightsFee); if (qpi.invocationReward() > state.transferRightsFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.transferRightsFee); @@ -2200,7 +2191,7 @@ struct QBAY : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(getNumberOfNFTForUser) { - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); output.numberOfNFT = 0; @@ -2222,7 +2213,7 @@ struct QBAY : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(getInfoOfNFTUserPossessed) { - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.cnt = 0; @@ -2349,7 +2340,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.cnt = 0; locals._r = 0; @@ -2407,7 +2398,8 @@ struct QBAY : public ContractBase struct getUserCreatedCollection_locals { - uint32 _r, cnt, _t; + uint32 _r, cnt; + sint32 _t; }; PUBLIC_FUNCTION_WITH_LOCALS(getUserCreatedCollection) @@ -2439,7 +2431,8 @@ struct QBAY : public ContractBase struct getUserCreatedNFT_locals { - uint32 _r, cnt, _t; + uint32 _r, cnt; + sint32 _t; }; PUBLIC_FUNCTION_WITH_LOCALS(getUserCreatedNFT) @@ -2510,6 +2503,11 @@ struct QBAY : public ContractBase } + BEGIN_EPOCH() + { + state.transferRightsFee = 100; + } + struct END_EPOCH_locals { QX::TransferShareManagementRights_input transferShareManagementRights_input; diff --git a/src/contracts/Qdraw.h b/src/contracts/Qdraw.h new file mode 100644 index 000000000..46c34d111 --- /dev/null +++ b/src/contracts/Qdraw.h @@ -0,0 +1,216 @@ +using namespace QPI; + +constexpr sint64 QDRAW_TICKET_PRICE = 1000000LL; +constexpr uint64 QDRAW_MAX_PARTICIPANTS = 1024 * X_MULTIPLIER; + +struct QDRAW2 +{ +}; + +struct QDRAW : public ContractBase +{ +public: + struct buyTicket_input + { + uint64 ticketCount; + }; + struct buyTicket_output + { + }; + + struct getInfo_input + { + }; + struct getInfo_output + { + sint64 pot; + uint64 participantCount; + id lastWinner; + sint64 lastWinAmount; + uint8 lastDrawHour; + uint8 currentHour; + uint8 nextDrawHour; + }; + + + struct getParticipants_input + { + }; + struct getParticipants_output + { + uint64 participantCount; + uint64 uniqueParticipantCount; + Array participants; + Array ticketCounts; + }; + +protected: + Array _participants; + uint64 _participantCount; + sint64 _pot; + uint8 _lastDrawHour; + id _lastWinner; + sint64 _lastWinAmount; + id _owner; + + struct buyTicket_locals + { + uint64 available; + sint64 totalCost; + uint64 i; + }; + + struct getParticipants_locals + { + uint64 uniqueCount; + uint64 i; + uint64 j; + bool found; + id p; + }; + + struct BEGIN_TICK_locals + { + uint8 currentHour; + id only; + id rand; + id winner; + uint64 loopIndex; + }; + + inline static bool isMonopoly(const Array& arr, uint64 count, uint64 loopIndex) + { + if (count != QDRAW_MAX_PARTICIPANTS) + { + return false; + } + for (loopIndex = 1; loopIndex < count; ++loopIndex) + { + if (arr.get(loopIndex) != arr.get(0)) + { + return false; + } + } + return true; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(buyTicket) + { + locals.available = QDRAW_MAX_PARTICIPANTS - state._participantCount; + if (QDRAW_MAX_PARTICIPANTS == state._participantCount || input.ticketCount == 0 || input.ticketCount > locals.available) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + locals.totalCost = (sint64)input.ticketCount * (sint64)QDRAW_TICKET_PRICE; + if (qpi.invocationReward() < locals.totalCost) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + for (locals.i = 0; locals.i < input.ticketCount; ++locals.i) + { + state._participants.set(state._participantCount + locals.i, qpi.invocator()); + } + state._participantCount += input.ticketCount; + state._pot += locals.totalCost; + if (qpi.invocationReward() > locals.totalCost) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.totalCost); + } + } + + PUBLIC_FUNCTION(getInfo) + { + output.pot = state._pot; + output.participantCount = state._participantCount; + output.lastDrawHour = state._lastDrawHour; + output.currentHour = qpi.hour(); + output.nextDrawHour = (uint8)(mod(qpi.hour() + 1, 24)); + output.lastWinner = state._lastWinner; + output.lastWinAmount = state._lastWinAmount; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getParticipants) + { + locals.uniqueCount = 0; + for (locals.i = 0; locals.i < state._participantCount; ++locals.i) + { + locals.p = state._participants.get(locals.i); + locals.found = false; + for (locals.j = 0; locals.j < locals.uniqueCount; ++locals.j) + { + if (output.participants.get(locals.j) == locals.p) + { + output.ticketCounts.set(locals.j, output.ticketCounts.get(locals.j) + 1); + locals.found = true; + break; + } + } + if (!locals.found) + { + output.participants.set(locals.uniqueCount, locals.p); + output.ticketCounts.set(locals.uniqueCount, 1); + ++locals.uniqueCount; + } + } + output.participantCount = state._participantCount; + output.uniqueParticipantCount = locals.uniqueCount; + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(buyTicket, 1); + REGISTER_USER_FUNCTION(getInfo, 2); + REGISTER_USER_FUNCTION(getParticipants, 3); + } + + INITIALIZE() + { + state._participantCount = 0; + state._pot = 0; + state._lastWinAmount = 0; + state._lastWinner = NULL_ID; + state._lastDrawHour = qpi.hour(); + state._owner = ID(_Q, _D, _R, _A, _W, _U, _R, _A, _L, _C, _L, _P, _P, _E, _Q, _O, _G, _Q, _C, _U, _J, _N, _F, _B, _B, _B, _A, _A, _F, _X, _W, _Y, _Y, _M, _M, _C, _U, _C, _U, _K, _T, _C, _R, _Q, _B, _S, _M, _Z, _U, _D, _M, _V, _X, _P, _N, _F); + } + + BEGIN_TICK_WITH_LOCALS() + { + locals.currentHour = qpi.hour(); + if (locals.currentHour != state._lastDrawHour) + { + state._lastDrawHour = locals.currentHour; + if (state._participantCount > 0) + { + if (isMonopoly(state._participants, state._participantCount, locals.loopIndex)) + { + locals.only = state._participants.get(0); + qpi.burn(QDRAW_TICKET_PRICE); + qpi.transfer(locals.only, QDRAW_TICKET_PRICE); + qpi.transfer(state._owner, state._pot - QDRAW_TICKET_PRICE - QDRAW_TICKET_PRICE); + state._lastWinner = locals.only; + state._lastWinAmount = QDRAW_TICKET_PRICE; + } + else + { + locals.rand = qpi.K12(qpi.getPrevSpectrumDigest()); + locals.winner = state._participants.get(mod(locals.rand.u64._0, state._participantCount)); + qpi.transfer(locals.winner, state._pot); + state._lastWinner = locals.winner; + state._lastWinAmount = state._pot; + } + state._participantCount = 0; + state._pot = 0; + } + } + } +}; + + diff --git a/src/contracts/Qearn.h b/src/contracts/Qearn.h index 407085d31..3e0d65d0e 100644 --- a/src/contracts/Qearn.h +++ b/src/contracts/Qearn.h @@ -45,7 +45,7 @@ constexpr sint32 QEARN_UNLOCK_SUCCESS = 5; constexpr sint32 QEARN_OVERFLOW_USER = 6; constexpr sint32 QEARN_LIMIT_LOCK = 7; -enum QearnLogInfo { +enum QEARNLogInfo { QearnSuccessLocking = 0, QearnFailedTransfer = 1, QearnLimitLocking = 2, @@ -54,14 +54,14 @@ enum QearnLogInfo { QearnSuccessEarlyUnlocking = 5, QearnSuccessFullyUnlocking = 6, }; -struct QearnLogger +struct QEARNLogger { uint32 _contractIndex; id sourcePublicKey; id destinationPublicKey; sint64 amount; uint32 _type; - char _terminator; + sint8 _terminator; }; struct QEARN2 @@ -274,6 +274,90 @@ struct QEARN : public ContractBase Array statsInfo; + struct _RemoveGapsInLockerArray_input + { + }; + + struct _RemoveGapsInLockerArray_output + { + }; + + struct _RemoveGapsInLockerArray_locals + { + EpochIndexInfo tmpEpochIndex; + LockInfo INITIALIZE_USER; + uint32 _t; + sint32 st; + sint32 en; + uint32 startEpoch; + }; + + PRIVATE_PROCEDURE_WITH_LOCALS(_RemoveGapsInLockerArray) + { + // Determine the actual start epoch (ensure it's at least QEARN_INITIAL_EPOCH) + locals.startEpoch = qpi.epoch() - 52; + if (locals.startEpoch < QEARN_INITIAL_EPOCH) + { + locals.startEpoch = QEARN_INITIAL_EPOCH; + } + + // Remove all gaps in Locker array and update epochIndex + locals.tmpEpochIndex.startIndex = 0; + for(locals._t = locals.startEpoch; locals._t <= qpi.epoch(); locals._t++) + { + // This loop iteration moves all elements of one epoch to the start of its range in the Locker array. + // The startIndex is given by the end of the range of the previous epoch, the new endIndex is found in the + // gap removal process. + locals.st = locals.tmpEpochIndex.startIndex; + locals.en = state._epochIndex.get(locals._t).endIndex; + ASSERT(locals.st <= locals.en); + + while(locals.st < locals.en) + { + // try to set locals.st to first empty slot + while (state.locker.get(locals.st)._lockedAmount && locals.st < locals.en) + { + locals.st++; + } + + // try set locals.en to last non-empty slot in epoch + --locals.en; + while (!state.locker.get(locals.en)._lockedAmount && locals.st < locals.en) + { + locals.en--; + } + + // if st and en meet, there are no gaps to be closed by moving in this epoch range + if (locals.st >= locals.en) + { + // make locals.en point behind last element again + ++locals.en; + break; + } + + // move entry from locals.en to locals.st + state.locker.set(locals.st, state.locker.get(locals.en)); + + // make locals.en slot empty -> locals.en points behind last element again + locals.INITIALIZE_USER.ID = NULL_ID; + locals.INITIALIZE_USER._lockedAmount = 0; + locals.INITIALIZE_USER._lockedEpoch = 0; + state.locker.set(locals.en, locals.INITIALIZE_USER); + } + + // update epoch index + locals.tmpEpochIndex.endIndex = locals.en; + state._epochIndex.set(locals._t, locals.tmpEpochIndex); + + // set start index of next epoch to end index of current epoch + locals.tmpEpochIndex.startIndex = locals.tmpEpochIndex.endIndex; + } + + // Set end index for next epoch + locals.tmpEpochIndex.endIndex = locals.tmpEpochIndex.startIndex; + state._epochIndex.set(qpi.epoch() + 1, locals.tmpEpochIndex); + } + struct getStateOfRound_locals { uint32 firstEpoch; }; @@ -308,7 +392,7 @@ struct QEARN : public ContractBase output.currentLockedAmount = state._currentRoundInfo.get(input.Epoch)._totalLockedAmount; if(state._currentRoundInfo.get(input.Epoch)._totalLockedAmount) { - output.yield = state._currentRoundInfo.get(input.Epoch)._epochBonusAmount * 10000000ULL / state._currentRoundInfo.get(input.Epoch)._totalLockedAmount; + output.yield = div(state._currentRoundInfo.get(input.Epoch)._epochBonusAmount * 10000000ULL, state._currentRoundInfo.get(input.Epoch)._totalLockedAmount); } else { @@ -482,10 +566,11 @@ struct QEARN : public ContractBase LockInfo newLocker; RoundInfo updatedRoundInfo; EpochIndexInfo tmpIndex; - QearnLogger log; + QEARNLogger log; uint32 t; uint32 endIndex; - + _RemoveGapsInLockerArray_input gapRemovalInput; + _RemoveGapsInLockerArray_output gapRemovalOutput; }; PUBLIC_PROCEDURE_WITH_LOCALS(lock) @@ -547,18 +632,27 @@ struct QEARN : public ContractBase } - if(locals.endIndex == QEARN_MAX_LOCKS - 1) + if(locals.endIndex >= QEARN_MAX_LOCKS - 1) { - output.returnCode = QEARN_OVERFLOW_USER; + // Remove gaps in locker array to free up memory slots + CALL(_RemoveGapsInLockerArray, locals.gapRemovalInput, locals.gapRemovalOutput); - locals.log = {QEARN_CONTRACT_INDEX, SELF, qpi.invocator(), qpi.invocationReward(), QearnOverflowUser, 0}; - LOG_INFO(locals.log); - - if(qpi.invocationReward() > 0) + // Re-check if there's space after gap removal + locals.endIndex = state._epochIndex.get(qpi.epoch()).endIndex; + + if(locals.endIndex >= QEARN_MAX_LOCKS - 1) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.returnCode = QEARN_OVERFLOW_USER; + + locals.log = {QEARN_CONTRACT_INDEX, SELF, qpi.invocator(), qpi.invocationReward(), QearnOverflowUser, 0}; + LOG_INFO(locals.log); + + if(qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; // overflow users in Qearn after gap removal } - return ; // overflow users in Qearn } if(qpi.invocationReward() > QEARN_MAX_LOCK_AMOUNT) @@ -605,7 +699,7 @@ struct QEARN : public ContractBase LockInfo updatedUserInfo; HistoryInfo unlockerInfo; StatsInfo tmpStats; - QearnLogger log; + QEARNLogger log; uint64 amountOfUnlocking; uint64 amountOfReward; @@ -938,17 +1032,16 @@ struct QEARN : public ContractBase RoundInfo INITIALIZE_ROUNDINFO; EpochIndexInfo tmpEpochIndex; StatsInfo tmpStats; - QearnLogger log; + QEARNLogger log; + _RemoveGapsInLockerArray_input gapRemovalInput; + _RemoveGapsInLockerArray_output gapRemovalOutput; uint64 _rewardPercent; uint64 _rewardAmount; uint64 _burnAmount; sint64 transferAmount; uint32 lockedEpoch; - uint32 startEpoch; uint32 _t; - sint32 st; - sint32 en; uint32 endIndex; }; @@ -1007,64 +1100,8 @@ struct QEARN : public ContractBase locals.tmpEpochIndex.endIndex = 0; state._epochIndex.set(locals.lockedEpoch, locals.tmpEpochIndex); - locals.startEpoch = locals.lockedEpoch + 1; - if (locals.startEpoch < QEARN_INITIAL_EPOCH) - locals.startEpoch = QEARN_INITIAL_EPOCH; - // remove all gaps in Locker array (from beginning) and update epochIndex - locals.tmpEpochIndex.startIndex = 0; - for(locals._t = locals.startEpoch; locals._t <= qpi.epoch(); locals._t++) - { - // This for loop iteration moves all elements of one epoch the to start of its range of the Locker array. - // The startIndex is given by the end of the range of the previous epoch, the new endIndex is found in the - // gap removal process. - locals.st = locals.tmpEpochIndex.startIndex; - locals.en = state._epochIndex.get(locals._t).endIndex; - ASSERT(locals.st <= locals.en); - - while(locals.st < locals.en) - { - // try to set locals.st to first empty slot - while (state.locker.get(locals.st)._lockedAmount && locals.st < locals.en) - { - locals.st++; - } - - // try set locals.en to last non-empty slot in epoch - --locals.en; - while (!state.locker.get(locals.en)._lockedAmount && locals.st < locals.en) - { - locals.en--; - } - - // if st and en meet, there are no gaps to be closed by moving in this epoch range - if (locals.st >= locals.en) - { - // make locals.en point behind last element again - ++locals.en; - break; - } - - // move entry from locals.en to locals.st - state.locker.set(locals.st, state.locker.get(locals.en)); - - // make locals.en slot empty -> locals.en points behind last element again - locals.INITIALIZE_USER.ID = NULL_ID; - locals.INITIALIZE_USER._lockedAmount = 0; - locals.INITIALIZE_USER._lockedEpoch = 0; - state.locker.set(locals.en, locals.INITIALIZE_USER); - } - - // update epoch index - locals.tmpEpochIndex.endIndex = locals.en; - state._epochIndex.set(locals._t, locals.tmpEpochIndex); - - // set start index of next epoch to end index of current epoch - locals.tmpEpochIndex.startIndex = locals.tmpEpochIndex.endIndex; - } - - locals.tmpEpochIndex.endIndex = locals.tmpEpochIndex.startIndex; - state._epochIndex.set(qpi.epoch() + 1, locals.tmpEpochIndex); + CALL(_RemoveGapsInLockerArray, locals.gapRemovalInput, locals.gapRemovalOutput); qpi.burn(locals._burnAmount); diff --git a/src/contracts/Qswap.h b/src/contracts/Qswap.h index 193ff3965..b73333e49 100644 --- a/src/contracts/Qswap.h +++ b/src/contracts/Qswap.h @@ -1,10 +1,21 @@ using namespace QPI; +// Log types enum for QSWAP contract +enum QSWAPLogInfo { + QSWAPAddLiquidity = 4, + QSWAPRemoveLiquidity = 5, + QSWAPSwapExactQuForAsset = 6, + QSWAPSwapQuForExactAsset = 7, + QSWAPSwapExactAssetForQu = 8, + QSWAPSwapAssetForExactQu = 9, + QSWAPFailedDistribution = 10, +}; + // FIXED CONSTANTS constexpr uint64 QSWAP_INITIAL_MAX_POOL = 16384; constexpr uint64 QSWAP_MAX_POOL = QSWAP_INITIAL_MAX_POOL * X_MULTIPLIER; constexpr uint64 QSWAP_MAX_USER_PER_POOL = 256; -constexpr sint64 QSWAP_MIN_LIQUDITY = 1000; +constexpr sint64 QSWAP_MIN_LIQUIDITY = 1000; constexpr uint32 QSWAP_SWAP_FEE_BASE = 10000; constexpr uint32 QSWAP_FEE_BASE_100 = 100; @@ -12,6 +23,48 @@ struct QSWAP2 { }; +// Logging message structures for QSWAP procedures +struct QSWAPAddLiquidityMessage +{ + uint32 _contractIndex; + uint32 _type; + id assetIssuer; + uint64 assetName; + sint64 userIncreaseLiquidity; + sint64 quAmount; + sint64 assetAmount; + sint8 _terminator; +}; + +struct QSWAPRemoveLiquidityMessage +{ + uint32 _contractIndex; + uint32 _type; + sint64 quAmount; + sint64 assetAmount; + sint8 _terminator; +}; + +struct QSWAPSwapMessage +{ + uint32 _contractIndex; + uint32 _type; + id assetIssuer; + uint64 assetName; + sint64 assetAmountIn; + sint64 assetAmountOut; + sint8 _terminator; +}; + +struct QSWAPFailedDistributionMessage +{ + uint32 _contractIndex; + uint32 _type; + id dst; + uint64 amount; + sint8 _terminator; +}; + struct QSWAP : public ContractBase { public: @@ -25,24 +78,26 @@ struct QSWAP : public ContractBase uint32 transferFee; // Amount of qus uint32 swapFee; // 30 -> 0.3% - uint32 protocolFee; // 20 -> 20%, for ipo share holders - uint32 teamFee; // 20 -> 20%, for dev team + uint32 shareholderFee; // 27 -> 27% of swap fee, for SC shareholders + uint32 investRewardsFee; // 3 -> 3% of swap fee, for Invest & Rewards + uint32 qxFee; // 5 -> 5% of swap fee, for QX + uint32 burnFee; // 1 -> 1% of swap fee, burned }; - struct TeamInfo_input + struct InvestRewardsInfo_input { }; - struct TeamInfo_output + struct InvestRewardsInfo_output { - uint32 teamFee; // 20 -> 20% - id teamId; + uint32 investRewardsFee; // 3 -> 3% of swap fee + id investRewardsId; }; - struct SetTeamInfo_input + struct SetInvestRewardsInfo_input { - id newTeamId; + id newInvestRewardsId; }; - struct SetTeamInfo_output + struct SetInvestRewardsInfo_output { bool success; }; @@ -57,18 +112,18 @@ struct QSWAP : public ContractBase sint64 poolExists; sint64 reservedQuAmount; sint64 reservedAssetAmount; - sint64 totalLiqudity; + sint64 totalLiquidity; }; - struct GetLiqudityOf_input + struct GetLiquidityOf_input { id assetIssuer; uint64 assetName; id account; }; - struct GetLiqudityOf_output + struct GetLiquidityOf_output { - sint64 liqudity; + sint64 liquidity; }; struct QuoteExactQuInput_input @@ -154,7 +209,7 @@ struct QSWAP : public ContractBase * @param quAmountMin Bounds the extent to which the B/A price can go up before the transaction reverts. Must be <= amountADesired. * @param assetAmountMin Bounds the extent to which the A/B price can go up before the transaction reverts. Must be <= amountBDesired. */ - struct AddLiqudity_input + struct AddLiquidity_input { id assetIssuer; uint64 assetName; @@ -162,23 +217,23 @@ struct QSWAP : public ContractBase sint64 quAmountMin; sint64 assetAmountMin; }; - struct AddLiqudity_output + struct AddLiquidity_output { - sint64 userIncreaseLiqudity; + sint64 userIncreaseLiquidity; sint64 quAmount; sint64 assetAmount; }; - struct RemoveLiqudity_input + struct RemoveLiquidity_input { id assetIssuer; uint64 assetName; - sint64 burnLiqudity; + sint64 burnLiquidity; sint64 quAmountMin; sint64 assetAmountMin; }; - struct RemoveLiqudity_output + struct RemoveLiquidity_output { sint64 quAmount; sint64 assetAmount; @@ -230,35 +285,65 @@ struct QSWAP : public ContractBase sint64 assetAmountIn; }; -protected: - uint32 swapFeeRate; // e.g. 30: 0.3% (base: 10_000) - uint32 teamFeeRate; // e.g. 20: 20% (base: 100) - uint32 protocolFeeRate; // e.g. 20: 20% (base: 100) only charge in qu - uint32 poolCreationFeeRate; // e.g. 10: 10% (base: 100) - - id teamId; - uint64 teamEarnedFee; - uint64 teamDistributedAmount; + struct TransferShareManagementRights_input + { + Asset asset; + sint64 numberOfShares; + uint32 newManagingContractIndex; + }; + struct TransferShareManagementRights_output + { + sint64 transferredNumberOfShares; + }; - uint64 protocolEarnedFee; - uint64 protocolDistributedAmount; +protected: struct PoolBasicState { id poolID; sint64 reservedQuAmount; sint64 reservedAssetAmount; - sint64 totalLiqudity; + sint64 totalLiquidity; }; - struct LiqudityInfo + struct LiquidityInfo { id entity; - sint64 liqudity; + sint64 liquidity; }; + // ----------------------------- + // --- state variables begin --- + // ----------------------------- + + uint32 swapFeeRate; // e.g. 30: 0.3% (base: 10_000) + uint32 investRewardsFeeRate;// 3: 3% of swap fees to Invest & Rewards (base: 100) + uint32 shareholderFeeRate; // 27: 27% of swap fees to SC shareholders (base: 100) + uint32 poolCreationFeeRate; // e.g. 10: 10% (base: 100) + + id investRewardsId; + uint64 investRewardsEarnedFee; + uint64 investRewardsDistributedAmount; + + uint64 shareholderEarnedFee; + uint64 shareholderDistributedAmount; + Array mPoolBasicStates; - Collection mLiquditys; + Collection mLiquidities; + + uint32 qxFeeRate; // 5: 5% of swap fees to QX (base: 100) + uint32 burnFeeRate; // 1: 1% of swap fees burned (base: 100) + + uint64 qxEarnedFee; + uint64 qxDistributedAmount; + + uint64 burnEarnedFee; // Total burn fees collected (to be burned in END_TICK) + uint64 burnedAmount; // Total amount actually burned + + // ----------------------------- + // ---- state variables end ---- + // ----------------------------- + inline static sint64 min(sint64 a, sint64 b) { @@ -317,6 +402,8 @@ struct QSWAP : public ContractBase uint128& tmpRes ) { + if (amountIn >= MAX_AMOUNT) return -1; + amountInWithFee = uint128(amountIn) * uint128(QSWAP_SWAP_FEE_BASE - fee); numerator = uint128(reserveOut) * amountInWithFee; denominator = uint128(reserveIn) * uint128(QSWAP_SWAP_FEE_BASE) + amountInWithFee; @@ -334,17 +421,19 @@ struct QSWAP : public ContractBase } // reserveIn * reserveOut = (reserveIn + x * (1-fee)) * (reserveOut - amountOut) - // x = (reserveIn * amountOut)/((1-fee) * (reserveOut - amountOut) - inline static sint64 getAmountInTakeFeeFromInToken(sint64& amountOut, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& tmpRes) - { - // reserveIn*amountOut/(reserveOut - amountOut)*QSWAP_SWAP_FEE_BASE / (QSWAP_SWAP_FEE_BASE - fee) - tmpRes = div( - div( - uint128(reserveIn) * uint128(amountOut), - uint128(reserveOut - amountOut) - ) * uint128(QSWAP_SWAP_FEE_BASE), - uint128(QSWAP_SWAP_FEE_BASE - fee) - ); + // x = (reserveIn * amountOut * 10000) / ((reserveOut - amountOut) * (10000 - fee)) + inline static sint64 getAmountInTakeFeeFromInToken(sint64& amountOut, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) + { + if (amountOut >= MAX_AMOUNT) return -1; + + // Calculate full numerator first to avoid premature truncation + numerator = uint128(reserveIn) * uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); + denominator = uint128(reserveOut - amountOut) * uint128(QSWAP_SWAP_FEE_BASE - fee); + + // Perform single division at the end + // Use floor + 1 to ensure user pays at least enough (protects LPs) + tmpRes = div(numerator, denominator) + uint128(1); + if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) { return -1; @@ -357,8 +446,13 @@ struct QSWAP : public ContractBase // (reserveIn + amountIn) * (reserveOut - x) = reserveIn * reserveOut // x = reserveOut * amountIn / (reserveIn + amountIn) + // NOTE: Despite the name, this returns the GROSS output (before fee deduction). + // The fee parameter is unused here because fee is applied separately by the caller. + // This is intentional: the caller needs the gross value for fee distribution calculation. inline static sint64 getAmountOutTakeFeeFromOutToken(sint64& amountIn, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) { + if (amountIn >= MAX_AMOUNT) return -1; + numerator = uint128(reserveOut) * uint128(amountIn); denominator = uint128(reserveIn + amountIn); @@ -374,18 +468,31 @@ struct QSWAP : public ContractBase } // (reserveIn + x) * (reserveOut - amountOut/(1 - fee)) = reserveIn * reserveOut - // x = (reserveIn * amountOut ) / (reserveOut * (1-fee) - amountOut) + // x = (reserveIn * amountOut * 10000) / (reserveOut * (10000-fee) - amountOut * 10000) inline static sint64 getAmountInTakeFeeFromOutToken(sint64& amountOut, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) { - numerator = uint128(reserveIn) * uint128(amountOut); - if (div(uint128(reserveOut) * uint128(QSWAP_SWAP_FEE_BASE - fee), uint128(QSWAP_SWAP_FEE_BASE)) < uint128(amountOut)) + if (amountOut >= MAX_AMOUNT) return -1; + + // Calculate full numerator to avoid premature truncation + numerator = uint128(reserveIn) * uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); + + // Check: reserveOut * (1-fee) must be greater than amountOut + // Scale reserveOut by (10000-fee) and amountOut by 10000 for comparison + // Use tmpRes and denominator temporarily for the comparison + tmpRes = uint128(reserveOut) * uint128(QSWAP_SWAP_FEE_BASE - fee); + denominator = uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); + + if (tmpRes <= denominator) { return -1; } - denominator = div(uint128(reserveOut) * uint128(QSWAP_SWAP_FEE_BASE - fee), uint128(QSWAP_SWAP_FEE_BASE)) - uint128(amountOut); - tmpRes = div(numerator, denominator); - if ((tmpRes.high != 0)|| (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) + denominator = tmpRes - denominator; + + // Use floor + 1 to ensure user pays at least enough (protects LPs) + tmpRes = div(numerator, denominator) + uint128(1); + + if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) { return -1; } @@ -410,8 +517,10 @@ struct QSWAP : public ContractBase output.poolCreationFee = uint32(div(uint64(locals.feesOutput.assetIssuanceFee) * uint64(state.poolCreationFeeRate), uint64(QSWAP_FEE_BASE_100))); output.transferFee = locals.feesOutput.transferFee; output.swapFee = state.swapFeeRate; - output.teamFee = state.teamFeeRate; - output.protocolFee = state.protocolFeeRate; + output.shareholderFee = state.shareholderFeeRate; + output.investRewardsFee = state.investRewardsFeeRate; + output.qxFee = state.qxFeeRate; + output.burnFee = state.burnFeeRate; } struct GetPoolBasicState_locals @@ -425,7 +534,7 @@ struct QSWAP : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(GetPoolBasicState) { output.poolExists = 0; - output.totalLiqudity = -1; + output.totalLiquidity = -1; output.reservedAssetAmount = -1; output.reservedQuAmount = -1; @@ -459,32 +568,32 @@ struct QSWAP : public ContractBase output.reservedQuAmount = locals.poolBasicState.reservedQuAmount; output.reservedAssetAmount = locals.poolBasicState.reservedAssetAmount; - output.totalLiqudity = locals.poolBasicState.totalLiqudity; + output.totalLiquidity = locals.poolBasicState.totalLiquidity; } - struct GetLiqudityOf_locals + struct GetLiquidityOf_locals { id poolID; sint64 liqElementIndex; }; - PUBLIC_FUNCTION_WITH_LOCALS(GetLiqudityOf) + PUBLIC_FUNCTION_WITH_LOCALS(GetLiquidityOf) { - output.liqudity = 0; + output.liquidity = 0; locals.poolID = input.assetIssuer; locals.poolID.u64._3 = input.assetName; - locals.liqElementIndex = state.mLiquditys.headIndex(locals.poolID, 0); + locals.liqElementIndex = state.mLiquidities.headIndex(locals.poolID, 0); while (locals.liqElementIndex != NULL_INDEX) { - if (state.mLiquditys.element(locals.liqElementIndex).entity == input.account) + if (state.mLiquidities.element(locals.liqElementIndex).entity == input.account) { - output.liqudity = state.mLiquditys.element(locals.liqElementIndex).liqudity; + output.liquidity = state.mLiquidities.element(locals.liqElementIndex).liquidity; return; } - locals.liqElementIndex = state.mLiquditys.nextElementIndex(locals.liqElementIndex); + locals.liqElementIndex = state.mLiquidities.nextElementIndex(locals.liqElementIndex); } } @@ -528,8 +637,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -586,8 +695,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -649,8 +758,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -685,7 +794,7 @@ struct QSWAP : public ContractBase PoolBasicState poolBasicState; uint32 i0; - uint128 i1; + uint128 i1, i2, i3; }; PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactAssetOutput) @@ -718,8 +827,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -734,14 +843,16 @@ struct QSWAP : public ContractBase locals.poolBasicState.reservedQuAmount, locals.poolBasicState.reservedAssetAmount, state.swapFeeRate, - locals.i1 + locals.i1, + locals.i2, + locals.i3 ); } - PUBLIC_FUNCTION(TeamInfo) + PUBLIC_FUNCTION(InvestRewardsInfo) { - output.teamId = state.teamId; - output.teamFee = state.teamFeeRate; + output.investRewardsFee = state.investRewardsFeeRate; + output.investRewardsId = state.investRewardsId; } // @@ -800,10 +911,13 @@ struct QSWAP : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - else if (qpi.invocationReward() > locals.feesOutput.assetIssuanceFee ) + else { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.feesOutput.assetIssuanceFee); - state.protocolEarnedFee += locals.feesOutput.assetIssuanceFee; + if (qpi.invocationReward() > locals.feesOutput.assetIssuanceFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.feesOutput.assetIssuanceFee); + } + state.shareholderEarnedFee += locals.feesOutput.assetIssuanceFee; } } @@ -879,7 +993,7 @@ struct QSWAP : public ContractBase locals.poolBasicState.poolID = locals.poolID; locals.poolBasicState.reservedAssetAmount = 0; locals.poolBasicState.reservedQuAmount = 0; - locals.poolBasicState.totalLiqudity = 0; + locals.poolBasicState.totalLiquidity = 0; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); @@ -887,27 +1001,28 @@ struct QSWAP : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.poolCreationFee ); } - state.protocolEarnedFee += locals.poolCreationFee; + state.shareholderEarnedFee += locals.poolCreationFee; output.success = true; } - struct AddLiqudity_locals + struct AddLiquidity_locals { + QSWAPAddLiquidityMessage addLiquidityMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; - LiqudityInfo tmpLiqudity; + LiquidityInfo tmpLiquidity; - sint64 userLiqudityElementIndex; + sint64 userLiquidityElementIndex; sint64 quAmountDesired; sint64 quTransferAmount; sint64 assetTransferAmount; sint64 quOptimalAmount; sint64 assetOptimalAmount; - sint64 increaseLiqudity; + sint64 increaseLiquidity; sint64 reservedAssetAmountBefore; sint64 reservedAssetAmountAfter; @@ -918,13 +1033,13 @@ struct QSWAP : public ContractBase uint128 i1, i2, i3; }; - PUBLIC_PROCEDURE_WITH_LOCALS(AddLiqudity) + PUBLIC_PROCEDURE_WITH_LOCALS(AddLiquidity) { - output.userIncreaseLiqudity = 0; + output.userIncreaseLiquidity = 0; output.assetAmount = 0; output.quAmount = 0; - // add liqudity must stake both qu and asset + // add liquidity must stake both qu and asset if (qpi.invocationReward() <= 0) { return; @@ -965,7 +1080,7 @@ struct QSWAP : public ContractBase // check if pool state meet the input condition before desposit // and confirm the final qu and asset amount to stake - if (locals.poolBasicState.totalLiqudity == 0) + if (locals.poolBasicState.totalLiquidity == 0) { locals.quTransferAmount = locals.quAmountDesired; locals.assetTransferAmount = input.assetAmountDesired; @@ -1046,11 +1161,11 @@ struct QSWAP : public ContractBase } // for pool's initial mint - if (locals.poolBasicState.totalLiqudity == 0) + if (locals.poolBasicState.totalLiquidity == 0) { - locals.increaseLiqudity = sqrt(locals.quTransferAmount, locals.assetTransferAmount, locals.i1, locals.i2, locals.i3); + locals.increaseLiquidity = sqrt(locals.quTransferAmount, locals.assetTransferAmount, locals.i1, locals.i2, locals.i3); - if (locals.increaseLiqudity < QSWAP_MIN_LIQUDITY ) + if (locals.increaseLiquidity < QSWAP_MIN_LIQUIDITY ) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1088,22 +1203,22 @@ struct QSWAP : public ContractBase } // permanently lock the first MINIMUM_LIQUIDITY tokens - locals.tmpLiqudity.entity = SELF; - locals.tmpLiqudity.liqudity = QSWAP_MIN_LIQUDITY; - state.mLiquditys.add(locals.poolID, locals.tmpLiqudity, 0); + locals.tmpLiquidity.entity = SELF; + locals.tmpLiquidity.liquidity = QSWAP_MIN_LIQUIDITY; + state.mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - locals.tmpLiqudity.entity = qpi.invocator(); - locals.tmpLiqudity.liqudity = locals.increaseLiqudity - QSWAP_MIN_LIQUDITY; - state.mLiquditys.add(locals.poolID, locals.tmpLiqudity, 0); + locals.tmpLiquidity.entity = qpi.invocator(); + locals.tmpLiquidity.liquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; + state.mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); output.quAmount = locals.quTransferAmount; output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiqudity = locals.increaseLiqudity - QSWAP_MIN_LIQUDITY; + output.userIncreaseLiquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; } else { locals.tmpIncLiq0 = div( - uint128(locals.quTransferAmount) * uint128(locals.poolBasicState.totalLiqudity), + uint128(locals.quTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), uint128(locals.poolBasicState.reservedQuAmount) ); if (locals.tmpIncLiq0.high != 0 || locals.tmpIncLiq0.low > 0x7FFFFFFFFFFFFFFF) @@ -1112,7 +1227,7 @@ struct QSWAP : public ContractBase return; } locals.tmpIncLiq1 = div( - uint128(locals.assetTransferAmount) * uint128(locals.poolBasicState.totalLiqudity), + uint128(locals.assetTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), uint128(locals.poolBasicState.reservedAssetAmount) ); if (locals.tmpIncLiq1.high != 0 || locals.tmpIncLiq1.low > 0x7FFFFFFFFFFFFFFF) @@ -1125,29 +1240,29 @@ struct QSWAP : public ContractBase // quTransferAmount * totalLiquity / reserveQuAmount, // assetTransferAmount * totalLiquity / reserveAssetAmount // ); - locals.increaseLiqudity = min(sint64(locals.tmpIncLiq0.low), sint64(locals.tmpIncLiq1.low)); + locals.increaseLiquidity = min(sint64(locals.tmpIncLiq0.low), sint64(locals.tmpIncLiq1.low)); // maybe too little input - if (locals.increaseLiqudity == 0) + if (locals.increaseLiquidity == 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - // find user liqudity index - locals.userLiqudityElementIndex = state.mLiquditys.headIndex(locals.poolID, 0); - while (locals.userLiqudityElementIndex != NULL_INDEX) + // find user liquidity index + locals.userLiquidityElementIndex = state.mLiquidities.headIndex(locals.poolID, 0); + while (locals.userLiquidityElementIndex != NULL_INDEX) { - if(state.mLiquditys.element(locals.userLiqudityElementIndex).entity == qpi.invocator()) + if(state.mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) { break; } - locals.userLiqudityElementIndex = state.mLiquditys.nextElementIndex(locals.userLiqudityElementIndex); + locals.userLiquidityElementIndex = state.mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); } - // no more space for new liqudity item - if ((locals.userLiqudityElementIndex == NULL_INDEX) && ( state.mLiquditys.population() == state.mLiquditys.capacity())) + // no more space for new liquidity item + if ((locals.userLiquidityElementIndex == NULL_INDEX) && ( state.mLiquidities.population() == state.mLiquidities.capacity())) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1186,50 +1301,61 @@ struct QSWAP : public ContractBase return; } - if (locals.userLiqudityElementIndex == NULL_INDEX) + if (locals.userLiquidityElementIndex == NULL_INDEX) { - locals.tmpLiqudity.entity = qpi.invocator(); - locals.tmpLiqudity.liqudity = locals.increaseLiqudity; - state.mLiquditys.add(locals.poolID, locals.tmpLiqudity, 0); + locals.tmpLiquidity.entity = qpi.invocator(); + locals.tmpLiquidity.liquidity = locals.increaseLiquidity; + state.mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); } else { - locals.tmpLiqudity = state.mLiquditys.element(locals.userLiqudityElementIndex); - locals.tmpLiqudity.liqudity += locals.increaseLiqudity; - state.mLiquditys.replace(locals.userLiqudityElementIndex, locals.tmpLiqudity); + locals.tmpLiquidity = state.mLiquidities.element(locals.userLiquidityElementIndex); + locals.tmpLiquidity.liquidity += locals.increaseLiquidity; + state.mLiquidities.replace(locals.userLiquidityElementIndex, locals.tmpLiquidity); } output.quAmount = locals.quTransferAmount; output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiqudity = locals.increaseLiqudity; + output.userIncreaseLiquidity = locals.increaseLiquidity; } locals.poolBasicState.reservedQuAmount += locals.quTransferAmount; locals.poolBasicState.reservedAssetAmount += locals.assetTransferAmount; - locals.poolBasicState.totalLiqudity += locals.increaseLiqudity; + locals.poolBasicState.totalLiquidity += locals.increaseLiquidity; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + // Log AddLiquidity procedure + locals.addLiquidityMessage._contractIndex = SELF_INDEX; + locals.addLiquidityMessage._type = QSWAPAddLiquidity; + locals.addLiquidityMessage.assetIssuer = input.assetIssuer; + locals.addLiquidityMessage.assetName = input.assetName; + locals.addLiquidityMessage.userIncreaseLiquidity = output.userIncreaseLiquidity; + locals.addLiquidityMessage.quAmount = output.quAmount; + locals.addLiquidityMessage.assetAmount = output.assetAmount; + LOG_INFO(locals.addLiquidityMessage); + if (qpi.invocationReward() > locals.quTransferAmount) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.quTransferAmount); } } - struct RemoveLiqudity_locals + struct RemoveLiquidity_locals { + QSWAPRemoveLiquidityMessage removeLiquidityMessage; id poolID; PoolBasicState poolBasicState; - sint64 userLiqudityElementIndex; + sint64 userLiquidityElementIndex; sint64 poolSlot; - LiqudityInfo userLiqudity; + LiquidityInfo userLiquidity; sint64 burnQuAmount; sint64 burnAssetAmount; uint32 i0; }; - PUBLIC_PROCEDURE_WITH_LOCALS(RemoveLiqudity) + PUBLIC_PROCEDURE_WITH_LOCALS(RemoveLiquidity) { output.quAmount = 0; output.assetAmount = 0; @@ -1268,45 +1394,45 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - locals.userLiqudityElementIndex = state.mLiquditys.headIndex(locals.poolID, 0); - while (locals.userLiqudityElementIndex != NULL_INDEX) + locals.userLiquidityElementIndex = state.mLiquidities.headIndex(locals.poolID, 0); + while (locals.userLiquidityElementIndex != NULL_INDEX) { - if(state.mLiquditys.element(locals.userLiqudityElementIndex).entity == qpi.invocator()) + if(state.mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) { break; } - locals.userLiqudityElementIndex = state.mLiquditys.nextElementIndex(locals.userLiqudityElementIndex); + locals.userLiquidityElementIndex = state.mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); } - if (locals.userLiqudityElementIndex == NULL_INDEX) + if (locals.userLiquidityElementIndex == NULL_INDEX) { return; } - locals.userLiqudity = state.mLiquditys.element(locals.userLiqudityElementIndex); + locals.userLiquidity = state.mLiquidities.element(locals.userLiquidityElementIndex); - // not enough liqudity for burning - if (locals.userLiqudity.liqudity < input.burnLiqudity ) + // not enough liquidity for burning + if (locals.userLiquidity.liquidity < input.burnLiquidity ) { return; } - if (locals.poolBasicState.totalLiqudity < input.burnLiqudity ) + if (locals.poolBasicState.totalLiquidity < input.burnLiquidity ) { return; } - // since burnLiqudity < totalLiqudity, so there will be no overflow risk + // since burnLiquidity < totalLiquidity, so there will be no overflow risk locals.burnQuAmount = sint64(div( - uint128(input.burnLiqudity) * uint128(locals.poolBasicState.reservedQuAmount), - uint128(locals.poolBasicState.totalLiqudity) + uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedQuAmount), + uint128(locals.poolBasicState.totalLiquidity) ).low); - // since burnLiqudity < totalLiqudity, so there will be no overflow risk + // since burnLiquidity < totalLiquidity, so there will be no overflow risk locals.burnAssetAmount = sint64(div( - uint128(input.burnLiqudity) * uint128(locals.poolBasicState.reservedAssetAmount), - uint128(locals.poolBasicState.totalLiqudity) + uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedAssetAmount), + uint128(locals.poolBasicState.totalLiquidity) ).low); @@ -1329,27 +1455,35 @@ struct QSWAP : public ContractBase output.quAmount = locals.burnQuAmount; output.assetAmount = locals.burnAssetAmount; - // modify invocator's liqudity info - locals.userLiqudity.liqudity -= input.burnLiqudity; - if (locals.userLiqudity.liqudity == 0) + // modify invocator's liquidity info + locals.userLiquidity.liquidity -= input.burnLiquidity; + if (locals.userLiquidity.liquidity == 0) { - state.mLiquditys.remove(locals.userLiqudityElementIndex); + state.mLiquidities.remove(locals.userLiquidityElementIndex); } else { - state.mLiquditys.replace(locals.userLiqudityElementIndex, locals.userLiqudity); + state.mLiquidities.replace(locals.userLiquidityElementIndex, locals.userLiquidity); } - // modify the pool's liqudity info - locals.poolBasicState.totalLiqudity -= input.burnLiqudity; + // modify the pool's liquidity info + locals.poolBasicState.totalLiquidity -= input.burnLiquidity; locals.poolBasicState.reservedQuAmount -= locals.burnQuAmount; locals.poolBasicState.reservedAssetAmount -= locals.burnAssetAmount; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log RemoveLiquidity procedure + locals.removeLiquidityMessage._contractIndex = SELF_INDEX; + locals.removeLiquidityMessage._type = QSWAPRemoveLiquidity; + locals.removeLiquidityMessage.quAmount = output.quAmount; + locals.removeLiquidityMessage.assetAmount = output.assetAmount; + LOG_INFO(locals.removeLiquidityMessage); } struct SwapExactQuForAsset_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; sint64 quAmountIn; @@ -1359,8 +1493,11 @@ struct QSWAP : public ContractBase uint32 i0; uint128 i1, i2, i3, i4; uint128 swapFee; - uint128 feeToTeam; - uint128 feeToProtocol; + uint128 feeToInvestRewards; + uint128 feeToShareholders; + + uint128 feeToQx; + uint128 feeToBurn; }; // given an input qu amountIn, only execute swap in case (amountOut >= amountOutMin) @@ -1404,8 +1541,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check the liqudity validity - if (locals.poolBasicState.totalLiqudity == 0) + // check the liquidity validity + if (locals.poolBasicState.totalLiquidity == 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1436,6 +1573,22 @@ struct QSWAP : public ContractBase return; } + // swapFee = quAmountIn * 0.3% (swapFeeRate/10000) + // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP + locals.swapFee = div(uint128(locals.quAmountIn) * uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); + locals.feeToShareholders = div(locals.swapFee * uint128(state.shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToQx = div(locals.swapFee * uint128(state.qxFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToInvestRewards = div(locals.swapFee * uint128(state.investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToBurn = div(locals.swapFee * uint128(state.burnFeeRate), uint128(QSWAP_FEE_BASE_100)); + + // Overflow protection: ensure all fees fit in uint64 + if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || + locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + // transfer the asset from pool to qpi.invocator() output.assetAmountOut = qpi.transferShareOwnershipAndPossession( input.assetName, @@ -1453,23 +1606,29 @@ struct QSWAP : public ContractBase return; } - // swapFee = quAmountIn * 0.3% (swapFeeRate/10000) - // feeToTeam = swapFee * 20% (teamFeeRate/100) - // feeToProtocol = (swapFee - feeToTeam) * 20% (protocolFeeRate/100) - locals.swapFee = div(uint128(locals.quAmountIn)*uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - locals.feeToTeam = div(locals.swapFee * uint128(state.teamFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToProtocol = div((locals.swapFee - locals.feeToTeam) * uint128(state.protocolFeeRate), uint128(QSWAP_FEE_BASE_100)); + // update fee state after successful transfer + state.shareholderEarnedFee += locals.feeToShareholders.low; + state.qxEarnedFee += locals.feeToQx.low; + state.investRewardsEarnedFee += locals.feeToInvestRewards.low; + state.burnEarnedFee += locals.feeToBurn.low; - state.teamEarnedFee += locals.feeToTeam.low; - state.protocolEarnedFee += locals.feeToProtocol.low; - - locals.poolBasicState.reservedQuAmount += locals.quAmountIn - sint64(locals.feeToTeam.low) - sint64(locals.feeToProtocol.low); + locals.poolBasicState.reservedQuAmount += locals.quAmountIn - sint64(locals.feeToShareholders.low) - sint64(locals.feeToQx.low) - sint64(locals.feeToInvestRewards.low) - sint64(locals.feeToBurn.low); locals.poolBasicState.reservedAssetAmount -= locals.assetAmountOut; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapExactQuForAsset procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapExactQuForAsset; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = locals.quAmountIn; + locals.swapMessage.assetAmountOut = output.assetAmountOut; + LOG_INFO(locals.swapMessage); } struct SwapQuForExactAsset_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; @@ -1477,10 +1636,12 @@ struct QSWAP : public ContractBase sint64 transferredAssetAmount; uint32 i0; - uint128 i1; + uint128 i1, i2, i3; uint128 swapFee; - uint128 feeToTeam; - uint128 feeToProtocol; + uint128 feeToInvestRewards; + uint128 feeToShareholders; + uint128 feeToQx; + uint128 feeToBurn; }; // https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#swaptokensforexacttokens @@ -1522,8 +1683,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check if there is liqudity in the poool - if (locals.poolBasicState.totalLiqudity == 0) + // check if there is liquidity in the poool + if (locals.poolBasicState.totalLiquidity == 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1541,7 +1702,9 @@ struct QSWAP : public ContractBase locals.poolBasicState.reservedQuAmount, locals.poolBasicState.reservedAssetAmount, state.swapFeeRate, - locals.i1 + locals.i1, + locals.i2, + locals.i3 ); // above call overflow @@ -1565,6 +1728,22 @@ struct QSWAP : public ContractBase return; } + // swapFee = quAmountIn * 0.3% (swapFeeRate/10000) + // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP + locals.swapFee = div(uint128(locals.quAmountIn) * uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); + locals.feeToShareholders = div(locals.swapFee * uint128(state.shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToQx = div(locals.swapFee * uint128(state.qxFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToInvestRewards = div(locals.swapFee * uint128(state.investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToBurn = div(locals.swapFee * uint128(state.burnFeeRate), uint128(QSWAP_FEE_BASE_100)); + + // Overflow protection: ensure all fees fit in uint64 + if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || + locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + // transfer the asset from pool to qpi.invocator() locals.transferredAssetAmount = qpi.transferShareOwnershipAndPossession( input.assetName, @@ -1588,23 +1767,29 @@ struct QSWAP : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.quAmountIn); } - // swapFee = quAmountIn * 0.3% - // feeToTeam = swapFee * 20% - // feeToProtocol = (swapFee - feeToTeam) * 20% - locals.swapFee = div(uint128(locals.quAmountIn)*uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - locals.feeToTeam = div(locals.swapFee * uint128(state.teamFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToProtocol = div((locals.swapFee - locals.feeToTeam) * uint128(state.protocolFeeRate), uint128(QSWAP_FEE_BASE_100)); - - state.teamEarnedFee += locals.feeToTeam.low; - state.protocolEarnedFee += locals.feeToProtocol.low; + // update fee state after successful transfer + state.shareholderEarnedFee += locals.feeToShareholders.low; + state.qxEarnedFee += locals.feeToQx.low; + state.investRewardsEarnedFee += locals.feeToInvestRewards.low; + state.burnEarnedFee += locals.feeToBurn.low; - locals.poolBasicState.reservedQuAmount += locals.quAmountIn - sint64(locals.feeToTeam.low) - sint64(locals.feeToProtocol.low); + locals.poolBasicState.reservedQuAmount += locals.quAmountIn - sint64(locals.feeToShareholders.low) - sint64(locals.feeToQx.low) - sint64(locals.feeToInvestRewards.low) - sint64(locals.feeToBurn.low); locals.poolBasicState.reservedAssetAmount -= input.assetAmountOut; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapQuForExactAsset procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapQuForExactAsset; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = output.quAmountIn; + locals.swapMessage.assetAmountOut = input.assetAmountOut; + LOG_INFO(locals.swapMessage); } struct SwapExactAssetForQu_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; @@ -1616,8 +1801,10 @@ struct QSWAP : public ContractBase uint32 i0; uint128 i1, i2, i3; uint128 swapFee; - uint128 feeToTeam; - uint128 feeToProtocol; + uint128 feeToInvestRewards; + uint128 feeToShareholders; + uint128 feeToQx; + uint128 feeToBurn; }; // given an amount of asset swap in, only execute swaping if quAmountOut >= input.amountOutMin @@ -1650,14 +1837,13 @@ struct QSWAP : public ContractBase if (locals.poolSlot == -1) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check the liqudity validity - if (locals.poolBasicState.totalLiqudity == 0) + // check the liquidity validity + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -1704,6 +1890,22 @@ struct QSWAP : public ContractBase return; } + // swapFee = quAmountOutWithFee * 0.3% (swapFeeRate/10000) + // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP + locals.swapFee = div(uint128(locals.quAmountOutWithFee) * uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); + locals.feeToShareholders = div(locals.swapFee * uint128(state.shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToQx = div(locals.swapFee * uint128(state.qxFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToInvestRewards = div(locals.swapFee * uint128(state.investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToBurn = div(locals.swapFee * uint128(state.burnFeeRate), uint128(QSWAP_FEE_BASE_100)); + + // Overflow protection: ensure all fees fit in uint64 + if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || + locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) + { + return; + } + + // transfer assets from user to pool locals.transferredAssetAmountBefore = qpi.numberOfPossessedShares( input.assetName, input.assetIssuer, @@ -1729,36 +1931,55 @@ struct QSWAP : public ContractBase SELF_INDEX ); - // pool does not receive enough asset + // pool does not receive enough asset, rollback any received shares if (locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore < input.assetAmountIn) { + // return any shares that were transferred + if (locals.transferredAssetAmountAfter > locals.transferredAssetAmountBefore) + { + qpi.transferShareOwnershipAndPossession( + input.assetName, + input.assetIssuer, + SELF, + SELF, + locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore, + qpi.invocator() + ); + } return; } qpi.transfer(qpi.invocator(), locals.quAmountOut); output.quAmountOut = locals.quAmountOut; - // swapFee = quAmountOutWithFee * 0.3% - // feeToTeam = swapFee * 20% - // feeToProtocol = (swapFee - feeToTeam) * 20% - locals.swapFee = div(uint128(locals.quAmountOutWithFee) * uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - locals.feeToTeam = div(locals.swapFee * uint128(state.teamFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToProtocol = div((locals.swapFee - locals.feeToTeam) * uint128(state.protocolFeeRate), uint128(QSWAP_FEE_BASE_100)); + // update fee state after successful transfers + state.shareholderEarnedFee += locals.feeToShareholders.low; + state.qxEarnedFee += locals.feeToQx.low; + state.investRewardsEarnedFee += locals.feeToInvestRewards.low; + state.burnEarnedFee += locals.feeToBurn.low; // update pool states locals.poolBasicState.reservedAssetAmount += input.assetAmountIn; locals.poolBasicState.reservedQuAmount -= locals.quAmountOut; - locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToTeam.low); - locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToProtocol.low); - - state.teamEarnedFee += locals.feeToTeam.low; - state.protocolEarnedFee += locals.feeToProtocol.low; - + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToShareholders.low); + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToQx.low); + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToInvestRewards.low); + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToBurn.low); state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapExactAssetForQu procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapExactAssetForQu; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = input.assetAmountIn; + locals.swapMessage.assetAmountOut = output.quAmountOut; + LOG_INFO(locals.swapMessage); } struct SwapAssetForExactQu_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; @@ -1769,8 +1990,10 @@ struct QSWAP : public ContractBase uint32 i0; uint128 i1, i2, i3; uint128 swapFee; - uint128 feeToTeam; - uint128 feeToProtocol; + uint128 feeToInvestRewards; + uint128 feeToShareholders; + uint128 feeToQx; + uint128 feeToBurn; }; PUBLIC_PROCEDURE_WITH_LOCALS(SwapAssetForExactQu) @@ -1799,16 +2022,15 @@ struct QSWAP : public ContractBase } } - if (locals.poolSlot == -1) + if (locals.poolSlot == -1) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check the liqudity validity - if (locals.poolBasicState.totalLiqudity == 0) + // check the liquidity validity + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -1854,6 +2076,21 @@ struct QSWAP : public ContractBase return; } + // swapFee = quAmountOut * 30/(10_000 - 30) + // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP + locals.swapFee = div(uint128(input.quAmountOut) * uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE - state.swapFeeRate)); + locals.feeToShareholders = div(locals.swapFee * uint128(state.shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToQx = div(locals.swapFee * uint128(state.qxFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToInvestRewards = div(locals.swapFee * uint128(state.investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); + locals.feeToBurn = div(locals.swapFee * uint128(state.burnFeeRate), uint128(QSWAP_FEE_BASE_100)); + + // Overflow protection: ensure all fees fit in uint64 + if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || + locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) + { + return; + } + locals.transferredAssetAmountBefore = qpi.numberOfPossessedShares( input.assetName, input.assetIssuer, @@ -1879,30 +2116,50 @@ struct QSWAP : public ContractBase SELF_INDEX ); + // pool does not receive enough asset, rollback any received shares if (locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore < locals.assetAmountIn) { + // return any shares that were transferred + if (locals.transferredAssetAmountAfter > locals.transferredAssetAmountBefore) + { + qpi.transferShareOwnershipAndPossession( + input.assetName, + input.assetIssuer, + SELF, + SELF, + locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore, + qpi.invocator() + ); + } return; } qpi.transfer(qpi.invocator(), input.quAmountOut); output.assetAmountIn = locals.assetAmountIn; - // swapFee = quAmountOut * 30/(10_000 - 30) - // feeToTeam = swapFee * 20% (teamFeeRate/100) - // feeToProtocol = (swapFee - feeToTeam) * 20% (protocolFeeRate/100) - locals.swapFee = div(uint128(input.quAmountOut) * uint128(state.swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE - state.swapFeeRate)); - locals.feeToTeam = div(locals.swapFee * uint128(state.teamFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToProtocol = div((locals.swapFee - locals.feeToTeam) * uint128(state.protocolFeeRate), uint128(QSWAP_FEE_BASE_100)); - - state.teamEarnedFee += locals.feeToTeam.low; - state.protocolEarnedFee += locals.feeToProtocol.low; + // update fee state after successful transfers + state.shareholderEarnedFee += locals.feeToShareholders.low; + state.qxEarnedFee += locals.feeToQx.low; + state.investRewardsEarnedFee += locals.feeToInvestRewards.low; + state.burnEarnedFee += locals.feeToBurn.low; // update pool states locals.poolBasicState.reservedAssetAmount += locals.assetAmountIn; locals.poolBasicState.reservedQuAmount -= input.quAmountOut; - locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToTeam.low); - locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToProtocol.low); + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToShareholders.low); + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToQx.low); + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToInvestRewards.low); + locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToBurn.low); state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapAssetForExactQu procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapAssetForExactQu; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = output.assetAmountIn; + locals.swapMessage.assetAmountOut = input.quAmountOut; + LOG_INFO(locals.swapMessage); } struct TransferShareOwnershipAndPossession_locals @@ -1962,78 +2219,174 @@ struct QSWAP : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - else if (qpi.invocationReward() > locals.feesOutput.transferFee) + else { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.feesOutput.transferFee); + if (qpi.invocationReward() > locals.feesOutput.transferFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.feesOutput.transferFee); + } + state.shareholderEarnedFee += locals.feesOutput.transferFee; } - - state.protocolEarnedFee += locals.feesOutput.transferFee; } - PUBLIC_PROCEDURE(SetTeamInfo) + PUBLIC_PROCEDURE(SetInvestRewardsInfo) { output.success = false; - if (qpi.invocator() != state.teamId) + if (qpi.invocator() != state.investRewardsId) { return; } - state.teamId = input.newTeamId; + state.investRewardsId = input.newInvestRewardsId; output.success = true; } + PUBLIC_PROCEDURE(TransferShareManagementRights) + { + if (qpi.invocationReward() < QSWAP_FEE_BASE_100) + { + return ; + } + + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + // not enough shares available + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, + input.newManagingContractIndex, input.newManagingContractIndex, QSWAP_FEE_BASE_100) < 0) + { + // error + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + // success + output.transferredNumberOfShares = input.numberOfShares; + if (qpi.invocationReward() > QSWAP_FEE_BASE_100) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QSWAP_FEE_BASE_100); + } + } + } + } + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { // functions REGISTER_USER_FUNCTION(Fees, 1); REGISTER_USER_FUNCTION(GetPoolBasicState, 2); - REGISTER_USER_FUNCTION(GetLiqudityOf, 3); + REGISTER_USER_FUNCTION(GetLiquidityOf, 3); REGISTER_USER_FUNCTION(QuoteExactQuInput, 4); REGISTER_USER_FUNCTION(QuoteExactQuOutput, 5); REGISTER_USER_FUNCTION(QuoteExactAssetInput, 6); REGISTER_USER_FUNCTION(QuoteExactAssetOutput, 7); - REGISTER_USER_FUNCTION(TeamInfo, 8); + REGISTER_USER_FUNCTION(InvestRewardsInfo, 8); // procedure REGISTER_USER_PROCEDURE(IssueAsset, 1); REGISTER_USER_PROCEDURE(TransferShareOwnershipAndPossession, 2); REGISTER_USER_PROCEDURE(CreatePool, 3); - REGISTER_USER_PROCEDURE(AddLiqudity, 4); - REGISTER_USER_PROCEDURE(RemoveLiqudity, 5); + REGISTER_USER_PROCEDURE(AddLiquidity, 4); + REGISTER_USER_PROCEDURE(RemoveLiquidity, 5); REGISTER_USER_PROCEDURE(SwapExactQuForAsset, 6); REGISTER_USER_PROCEDURE(SwapQuForExactAsset, 7); REGISTER_USER_PROCEDURE(SwapExactAssetForQu, 8); REGISTER_USER_PROCEDURE(SwapAssetForExactQu, 9); - REGISTER_USER_PROCEDURE(SetTeamInfo, 10); + REGISTER_USER_PROCEDURE(SetInvestRewardsInfo, 10); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 11); } INITIALIZE() { - state.swapFeeRate = 30; // 0.3%, must less than 10000 - state.poolCreationFeeRate = 20; // 20%, must less than 100 - // earned fee: 20% to team, 80% to (shareholders and stakers), share holders take 16% (20% * 80%), stakers take 64% (80% * 80%) - state.teamFeeRate = 20; // 20% - state.protocolFeeRate = 20; // 20%, must less than 100 - // IRUNQTXZRMLDEENHPRZQPSGPCFACORRUJYSBVJPQEHFCEKLLURVDDJVEXNBL - state.teamId = ID(_I, _R, _U, _N, _Q, _T, _X, _Z, _R, _M, _L, _D, _E, _E, _N, _H, _P, _R, _Z, _Q, _P, _S, _G, _P, _C, _F, _A, _C, _O, _R, _R, _U, _J, _Y, _S, _B, _V, _J, _P, _Q, _E, _H, _F, _C, _E, _K, _L, _L, _U, _R, _V, _D, _D, _J, _V, _E); + state.swapFeeRate = 30; // 0.3%, must be less than 10000 + state.poolCreationFeeRate = 20; // 20%, must be less than 100 + + // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP providers + state.shareholderFeeRate = 27; // 27% of swap fees to SC shareholders + state.investRewardsFeeRate = 3; // 3% of swap fees to Invest & Rewards + state.qxFeeRate = 5; // 5% of swap fees to QX + state.burnFeeRate = 1; // 1% of swap fees burned + + // + state.investRewardsId = ID(_V, _J, _G, _R, _U, _F, _W, _J, _C, _U, _S, _N, _H, _C, _Q, _J, _R, _W, _R, _R, _Y, _X, _A, _U, _E, _J, _F, _C, _V, _H, _Y, _P, _X, _W, _K, _T, _D, _L, _Y, _K, _U, _A, _C, _P, _V, _V, _Y, _B, _G, _O, _L, _V, _C, _J, _S, _F); } - END_TICK() + struct END_TICK_locals + { + uint64 toDistribute; + uint64 toBurn; + uint64 dividendPerComputor; + sint64 transferredAmount; + QSWAPFailedDistributionMessage logMsg; + }; + + END_TICK_WITH_LOCALS() { - // distribute team fee - if (state.teamEarnedFee > state.teamDistributedAmount) + // Distribute Invest & Rewards fees + if (state.investRewardsEarnedFee > state.investRewardsDistributedAmount) { - qpi.transfer(state.teamId, state.teamEarnedFee - state.teamDistributedAmount); - state.teamDistributedAmount += state.teamEarnedFee - state.teamDistributedAmount; + locals.toDistribute = state.investRewardsEarnedFee - state.investRewardsDistributedAmount; + locals.transferredAmount = qpi.transfer(state.investRewardsId, locals.toDistribute); + if (locals.transferredAmount < 0) + { + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSWAPFailedDistribution; + locals.logMsg.dst = state.investRewardsId; + locals.logMsg.amount = locals.toDistribute; + LOG_INFO(locals.logMsg); + } + else + state.investRewardsDistributedAmount += locals.toDistribute; } - // distribute ipo fee - if ((div((state.protocolEarnedFee - state.protocolDistributedAmount), 676ULL) > 0) && (state.protocolEarnedFee > state.protocolDistributedAmount)) + // Distribute QX fees as donation + if (state.qxEarnedFee > state.qxDistributedAmount) { - if (qpi.distributeDividends(div((state.protocolEarnedFee - state.protocolDistributedAmount), 676ULL))) + locals.toDistribute = state.qxEarnedFee - state.qxDistributedAmount; + locals.transferredAmount = qpi.transfer(id(QX_CONTRACT_INDEX, 0, 0, 0), locals.toDistribute); + if (locals.transferredAmount < 0) { - state.protocolDistributedAmount += div((state.protocolEarnedFee- state.protocolDistributedAmount), 676ULL) * NUMBER_OF_COMPUTORS; + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSWAPFailedDistribution; + locals.logMsg.dst = id(QX_CONTRACT_INDEX, 0, 0, 0); + locals.logMsg.amount = locals.toDistribute; + LOG_INFO(locals.logMsg); } + else + state.qxDistributedAmount += locals.toDistribute; + } + + // Distribute shareholder fees (to IPO shareholders via dividends) + if (state.shareholderEarnedFee > state.shareholderDistributedAmount) + { + locals.dividendPerComputor = div((state.shareholderEarnedFee - state.shareholderDistributedAmount), 676ULL); + if (locals.dividendPerComputor > 0 && qpi.distributeDividends(locals.dividendPerComputor)) + { + state.shareholderDistributedAmount += locals.dividendPerComputor * NUMBER_OF_COMPUTORS; + } + } + + // Burn fees (adds to contract execution fee reserve) + if (state.burnEarnedFee > state.burnedAmount) + { + locals.toBurn = state.burnEarnedFee - state.burnedAmount; + qpi.burn(locals.toBurn); + state.burnedAmount += locals.toBurn; } } + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } }; diff --git a/src/contracts/Quottery.h b/src/contracts/Quottery.h index a76e0dc59..cd6c06a8b 100644 --- a/src/contracts/Quottery.h +++ b/src/contracts/Quottery.h @@ -14,14 +14,13 @@ constexpr unsigned long long QUOTTERY_MIN_AMOUNT_PER_BET_SLOT_ = 10000ULL; constexpr unsigned long long QUOTTERY_SHAREHOLDER_FEE_ = 1000; // 10% constexpr unsigned long long QUOTTERY_GAME_OPERATOR_FEE_ = 50; // 0.5% constexpr unsigned long long QUOTTERY_BURN_FEE_ = 200; // 2% -static_assert(QUOTTERY_BURN_FEE_ > 0, "SC requires burning qu to operate, the burn fee must be higher than 0!"); - +STATIC_ASSERT(QUOTTERY_BURN_FEE_ > 0, BurningRequiredToOperate); constexpr unsigned long long QUOTTERY_TICK_TO_KEEP_AFTER_END = 100ULL; -enum QuotteryLogInfo { +enum QUOTTERYLogInfo { invalidMaxBetSlotPerOption=0, invalidOption = 1, invalidBetAmount = 2, @@ -38,12 +37,12 @@ enum QuotteryLogInfo { betIsAlreadyFinalized = 13, totalError = 14 }; -struct QuotteryLogger +struct QUOTTERYLogger { uint32 _contractIndex; uint32 _type; // Assign a random unique (per contract) number to distinguish messages of different types // Other data go here - char _terminator; // Only data before "_terminator" are logged + sint8 _terminator; // Only data before "_terminator" are logged }; struct QUOTTERY2 @@ -196,6 +195,7 @@ struct QUOTTERY : public ContractBase uint64 baseId0, baseId1; sint32 numberOP, requiredVote, winOption, totalOption, voteCounter, numberOfSlot, currentState; uint64 amountPerSlot, totalBetSlot, potAmountTotal, feeChargedAmount, transferredAmount, fee, profitPerBetSlot, nWinBet; + QUOTTERYLogger log; }; /**************************************/ @@ -223,24 +223,24 @@ struct QUOTTERY : public ContractBase Array mBetResultWonOption; Array mBetResultOPId; - //static assert for developing: - static_assert(sizeof(mBetID) == (sizeof(uint32) * QUOTTERY_MAX_BET), "bet id array"); - static_assert(sizeof(mCreator) == (sizeof(id) * QUOTTERY_MAX_BET), "creator array"); - static_assert(sizeof(mBetDesc) == (sizeof(id) * QUOTTERY_MAX_BET), "desc array"); - static_assert(sizeof(mOptionDesc) == (sizeof(id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_OPTION), "option desc array"); - static_assert(sizeof(mBetAmountPerSlot) == (sizeof(uint64) * QUOTTERY_MAX_BET), "bet amount per slot array"); - static_assert(sizeof(mMaxNumberOfBetSlotPerOption) == (sizeof(uint32) * QUOTTERY_MAX_BET), "number of bet slot per option array"); - static_assert(sizeof(mOracleProvider) == (sizeof(QPI::id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), "oracle providers"); - static_assert(sizeof(mOracleFees) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), "oracle providers fees"); - static_assert(sizeof(mCurrentBetState) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), "bet states"); - static_assert(sizeof(mNumberOption) == (sizeof(uint8) * QUOTTERY_MAX_BET), "number of options"); - static_assert(sizeof(mOpenDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), "open date"); - static_assert(sizeof(mCloseDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), "close date"); - static_assert(sizeof(mEndDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), "end date"); - static_assert(sizeof(mBetResultWonOption) == (QUOTTERY_MAX_BET * 8), "won option array"); - static_assert(sizeof(mBetResultOPId) == (QUOTTERY_MAX_BET * 8), "op id array"); - static_assert(sizeof(mBettorID) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(id)), "bettor array"); - static_assert(sizeof(mBettorBetOption) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(uint8)), "bet option"); + // static assert for developing: + STATIC_ASSERT(sizeof(mBetID) == (sizeof(uint32) * QUOTTERY_MAX_BET), BetIdArray); + STATIC_ASSERT(sizeof(mCreator) == (sizeof(id) * QUOTTERY_MAX_BET), CreatorArray); + STATIC_ASSERT(sizeof(mBetDesc) == (sizeof(id) * QUOTTERY_MAX_BET), DescArray); + STATIC_ASSERT(sizeof(mOptionDesc) == (sizeof(id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_OPTION), OptionDescArray); + STATIC_ASSERT(sizeof(mBetAmountPerSlot) == (sizeof(uint64) * QUOTTERY_MAX_BET), BetAmountPerSlotArray); + STATIC_ASSERT(sizeof(mMaxNumberOfBetSlotPerOption) == (sizeof(uint32) * QUOTTERY_MAX_BET), NumberOfBetSlotPerOptionArray); + STATIC_ASSERT(sizeof(mOracleProvider) == (sizeof(QPI::id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), OracleProviders); + STATIC_ASSERT(sizeof(mOracleFees) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), OracleProvidersFees); + STATIC_ASSERT(sizeof(mCurrentBetState) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), BetStates); + STATIC_ASSERT(sizeof(mNumberOption) == (sizeof(uint8) * QUOTTERY_MAX_BET), NumberOfOptions); + STATIC_ASSERT(sizeof(mOpenDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), OpenDate); + STATIC_ASSERT(sizeof(mCloseDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), CloseDate); + STATIC_ASSERT(sizeof(mEndDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), EndDate); + STATIC_ASSERT(sizeof(mBetResultWonOption) == (QUOTTERY_MAX_BET * 8), WonOptionArray); + STATIC_ASSERT(sizeof(mBetResultOPId) == (QUOTTERY_MAX_BET * 8), OpIdArray); + STATIC_ASSERT(sizeof(mBettorID) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(id)), BettorArray); + STATIC_ASSERT(sizeof(mBettorBetOption) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(uint8)), BetOption); // other stats uint32 mCurrentBetID; @@ -335,13 +335,6 @@ struct QUOTTERY : public ContractBase _second = qtryGetSecond(data); //6bits } - /** - * @return Current date from core node system - */ - inline static void getCurrentDate(const QPI::QpiContextProcedureCall& qpi, uint32& res) { - packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), res); - } - inline static void accumulatedDay(sint32 month, uint64& res) { switch (month) @@ -609,8 +602,8 @@ struct QUOTTERY : public ContractBase } else { - QuotteryLogger log{ 0,QuotteryLogInfo::notEnoughVote,0 }; - LOG_INFO(log); + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::notEnoughVote,0 }; + LOG_INFO(locals.log); } } /**************************************/ @@ -849,7 +842,7 @@ struct QUOTTERY : public ContractBase checkAndCleanMemorySlots_input _checkAndCleanMemorySlots_input; checkAndCleanMemorySlots_output _checkAndCleanMemorySlots_output; checkAndCleanMemorySlots_locals _checkAndCleanMemorySlots_locals; - QuotteryLogger log; + QUOTTERYLogger log; }; PUBLIC_PROCEDURE_WITH_LOCALS(issueBet) @@ -860,10 +853,10 @@ struct QUOTTERY : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if (!checkValidQtryDateTime(input.closeDate) || !checkValidQtryDateTime(input.endDate)) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidDate,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidDate,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -872,28 +865,28 @@ struct QUOTTERY : public ContractBase if (dateCompare(input.closeDate, input.endDate, locals.i0) == 1 || dateCompare(locals.curDate, input.closeDate, locals.i0) == 1) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidDate,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidDate,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } if (input.amountPerSlot < state.mMinAmountPerBetSlot) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetAmount,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetAmount,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } if (input.numberOfOption > QUOTTERY_MAX_OPTION || input.numberOfOption < 2) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidOption,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidOption,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } if (input.maxBetSlotPerOption == 0 || input.maxBetSlotPerOption > QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidMaxBetSlotPerOption,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidMaxBetSlotPerOption,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -907,7 +900,7 @@ struct QUOTTERY : public ContractBase // fee is higher than sent amount, exit if (locals.fee > qpi.invocationReward()) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::insufficientFund,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::insufficientFund,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -934,7 +927,7 @@ struct QUOTTERY : public ContractBase //out of bet storage, exit if (locals.slotId == -1) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::outOfStorage,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::outOfStorage,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -974,7 +967,7 @@ struct QUOTTERY : public ContractBase } if (locals.numberOP == 0) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidNumberOfOracleProvider,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidNumberOfOracleProvider,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1011,7 +1004,7 @@ struct QUOTTERY : public ContractBase sint64 amountPerSlot, fee; uint32 availableSlotForBet; sint64 slotId; - QuotteryLogger log; + QUOTTERYLogger log; }; /** * Join a bet @@ -1041,7 +1034,7 @@ struct QUOTTERY : public ContractBase if (locals.slotId == -1) { // can't find betId - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetId,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1053,14 +1046,14 @@ struct QUOTTERY : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.closeDate = state.mCloseDate.get(locals.slotId); if (dateCompare(locals.curDate, locals.closeDate, locals.i0) > 0) { // bet is closed for betting - QuotteryLogger log{ 0,QuotteryLogInfo::expiredBet,0 }; - LOG_INFO(log); + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::expiredBet,0 }; + LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } @@ -1069,7 +1062,7 @@ struct QUOTTERY : public ContractBase if (input.option >= state.mNumberOption.get(locals.slotId)) { // bet is closed for betting - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidOption,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidOption,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1086,7 +1079,7 @@ struct QUOTTERY : public ContractBase if (locals.numberOfSlot == 0) { // out of slot - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::outOfSlot,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::outOfSlot,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1096,7 +1089,7 @@ struct QUOTTERY : public ContractBase if (locals.fee > qpi.invocationReward()) { // not send enough amount - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::insufficientFund,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::insufficientFund,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1136,7 +1129,7 @@ struct QUOTTERY : public ContractBase uint64 baseId0; sint64 slotId, writeId; sint8 opId; - QuotteryLogger log; + QUOTTERYLogger log; tryFinalizeBet_locals tfb; tryFinalizeBet_input _tryFinalizeBet_input; tryFinalizeBet_output _tryFinalizeBet_output; @@ -1166,7 +1159,7 @@ struct QUOTTERY : public ContractBase if (locals.slotId == -1) { // can't find betId - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetId,0 }; LOG_INFO(locals.log); return; } @@ -1174,11 +1167,11 @@ struct QUOTTERY : public ContractBase if (state.mIsOccupied.get(locals.slotId) == 0) { // the bet is over - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::expiredBet,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::expiredBet,0 }; LOG_INFO(locals.log); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.endDate = state.mEndDate.get(locals.slotId); // endDate is counted as 23:59 of that day if (dateCompare(locals.curDate, locals.endDate, locals.i0) <= 0) @@ -1201,7 +1194,7 @@ struct QUOTTERY : public ContractBase if (locals.opId == -1) { // is not oracle provider - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidOPId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidOPId,0 }; LOG_INFO(locals.log); return; } @@ -1212,7 +1205,7 @@ struct QUOTTERY : public ContractBase if (state.mBetEndTick.get(locals.slotId) != 0) { // is already finalized - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::betIsAlreadyFinalized,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::betIsAlreadyFinalized,0 }; LOG_INFO(locals.log); return; } @@ -1271,7 +1264,7 @@ struct QUOTTERY : public ContractBase uint64 amountPerSlot; uint64 duration, u64_0, u64_1; sint64 slotId; - QuotteryLogger log; + QUOTTERYLogger log; cleanMemorySlot_locals cms; cleanMemorySlot_input _cleanMemorySlot_input; cleanMemorySlot_output _cleanMemorySlot_output; @@ -1287,7 +1280,7 @@ struct QUOTTERY : public ContractBase // all funds will be returned if (qpi.invocator() != state.mGameOperatorId) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::notGameOperator,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::notGameOperator,0 }; LOG_INFO(locals.log); return; } @@ -1303,7 +1296,7 @@ struct QUOTTERY : public ContractBase if (locals.slotId == -1) { // can't find betId - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetId,0 }; LOG_INFO(locals.log); return; } @@ -1311,11 +1304,11 @@ struct QUOTTERY : public ContractBase if (state.mIsOccupied.get(locals.slotId) == 0) { // the bet is over - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::expiredBet,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::expiredBet,0 }; LOG_INFO(locals.log); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.endDate = state.mEndDate.get(locals.slotId); // endDate is counted as 23:59 of that day diff --git a/src/contracts/Qx.h b/src/contracts/Qx.h index 601a374a4..95983f6b7 100644 --- a/src/contracts/Qx.h +++ b/src/contracts/Qx.h @@ -220,7 +220,7 @@ struct QX : public ContractBase sint64 price; sint64 numberOfShares; - char _terminator; + sint8 _terminator; } _tradeMessage; struct _NumberOfReservedShares_input @@ -533,8 +533,9 @@ struct QX : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (input.price <= 0 - || input.numberOfShares <= 0) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT + || smul(input.price, input.numberOfShares) >= MAX_AMOUNT) { output.addedNumberOfShares = 0; } @@ -624,8 +625,15 @@ struct QX : public ContractBase state._elementIndex2 = state._entityOrders.nextElementIndex(state._elementIndex2); } - - state._fee = (state._price * state._assetOrder.numberOfShares * state._tradeFee / 1000000000UL) + 1; + if (smul(state._price, state._assetOrder.numberOfShares) >= div(INT64_MAX, state._tradeFee)) + { + // in this case, traders will pay more fee because it's rounding down, it's better to split the trade into multiple smaller trades + state._fee = div(state._price * state._assetOrder.numberOfShares, div(1000000000LL, state._tradeFee)) + 1; + } + else + { + state._fee = div(state._price * state._assetOrder.numberOfShares * state._tradeFee, 1000000000LL) + 1; + } state._earnedAmount += state._fee; qpi.transfer(qpi.invocator(), state._price * state._assetOrder.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, qpi.invocator(), qpi.invocator(), state._assetOrder.numberOfShares, state._assetOrder.entity); @@ -658,8 +666,15 @@ struct QX : public ContractBase state._elementIndex = state._entityOrders.nextElementIndex(state._elementIndex); } - - state._fee = (state._price * input.numberOfShares * state._tradeFee / 1000000000UL) + 1; + if (smul(state._price, input.numberOfShares) >= div(INT64_MAX, state._tradeFee)) + { + // in this case, traders will pay more fee because it's rounding down, it's better to split the trade into multiple smaller trades + state._fee = div(state._price * input.numberOfShares, div(1000000000LL, state._tradeFee)) + 1; + } + else + { + state._fee = div(state._price * input.numberOfShares * state._tradeFee, 1000000000LL) + 1; + } state._earnedAmount += state._fee; qpi.transfer(qpi.invocator(), state._price * input.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, qpi.invocator(), qpi.invocator(), input.numberOfShares, state._assetOrder.entity); @@ -694,9 +709,10 @@ struct QX : public ContractBase PUBLIC_PROCEDURE(AddToBidOrder) { - if (input.price <= 0 - || input.numberOfShares <= 0 - || qpi.invocationReward() < input.price * input.numberOfShares) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT + || smul(input.price, input.numberOfShares) >= MAX_AMOUNT + || qpi.invocationReward() < smul(input.price, input.numberOfShares)) { output.addedNumberOfShares = 0; @@ -707,9 +723,9 @@ struct QX : public ContractBase } else { - if (qpi.invocationReward() > input.price * input.numberOfShares) + if (qpi.invocationReward() > smul(input.price, input.numberOfShares)) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.price * input.numberOfShares); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - smul(input.price, input.numberOfShares)); } output.addedNumberOfShares = input.numberOfShares; @@ -788,7 +804,15 @@ struct QX : public ContractBase state._elementIndex2 = state._entityOrders.nextElementIndex(state._elementIndex2); } - state._fee = (state._price * state._assetOrder.numberOfShares * state._tradeFee / 1000000000UL) + 1; + if (smul(state._price, state._assetOrder.numberOfShares) >= div(INT64_MAX, state._tradeFee)) + { + // in this case, traders will pay more fee because it's rounding down, it's better to split the trade into multiple smaller trades + state._fee = div(state._price * state._assetOrder.numberOfShares, div(1000000000LL, state._tradeFee)) + 1; + } + else + { + state._fee = div(state._price * state._assetOrder.numberOfShares * state._tradeFee, 1000000000LL) + 1; + } state._earnedAmount += state._fee; qpi.transfer(state._assetOrder.entity, state._price * state._assetOrder.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, state._assetOrder.entity, state._assetOrder.entity, state._assetOrder.numberOfShares, qpi.invocator()); @@ -826,7 +850,15 @@ struct QX : public ContractBase state._elementIndex = state._entityOrders.nextElementIndex(state._elementIndex); } - state._fee = (state._price * input.numberOfShares * state._tradeFee / 1000000000UL) + 1; + if (smul(state._price, input.numberOfShares) >= div(INT64_MAX, state._tradeFee)) + { + // in this case, traders will pay more fee because it's rounding down, it's better to split the trade into multiple smaller trades + state._fee = div(state._price * input.numberOfShares, div(1000000000LL, state._tradeFee)) + 1; + } + else + { + state._fee = div(state._price * input.numberOfShares * state._tradeFee, 1000000000LL) + 1; + } state._earnedAmount += state._fee; qpi.transfer(state._assetOrder.entity, state._price * input.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, state._assetOrder.entity, state._assetOrder.entity, input.numberOfShares, qpi.invocator()); @@ -869,8 +901,9 @@ struct QX : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (input.price <= 0 - || input.numberOfShares <= 0) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT + || smul(input.price, input.numberOfShares) >= MAX_AMOUNT) { output.removedNumberOfShares = 0; } @@ -956,8 +989,9 @@ struct QX : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (input.price <= 0 - || input.numberOfShares <= 0) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT + || smul(input.price, input.numberOfShares) >= MAX_AMOUNT) { output.removedNumberOfShares = 0; } @@ -1144,5 +1178,22 @@ struct QX : public ContractBase POST_ACQUIRE_SHARES() { } + + POST_INCOMING_TRANSFER() + { + switch (input.type) + { + case TransferType::standardTransaction: + qpi.transfer(input.sourceId, input.amount); + break; + case TransferType::qpiTransfer: + case TransferType::revenueDonation: + // add amount to _earnedAmount which will be distributed to shareholders in END_TICK + state._earnedAmount += input.amount; + break; + default: + break; + } + } }; diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h new file mode 100644 index 000000000..2765a4537 --- /dev/null +++ b/src/contracts/RandomLottery.h @@ -0,0 +1,964 @@ +/** + * @file RandomLottery.h + * @brief Random Lottery contract definition: state, data structures, and user / internal + * procedures. + * + * This header declares the RL (Random Lottery) contract which: + * - Sells tickets during a SELLING epoch. + * - Draws a pseudo-random winner when the epoch ends or at scheduled intra-epoch draws. + * - Distributes fees (team, distribution, burn, winner). + * - Records winners' history in a ring-like buffer. + * + * Notes: + * - Percentages must sum to <= 100; the remainder goes to the winner. + * - Players array stores one entry per ticket, so a single address can appear multiple times. + * - When only one player bought a ticket in the epoch, funds are refunded instead of drawing. + * - Day-of-week mapping used here is 0..6 where 0 = WEDNESDAY, 1 = THURSDAY, ..., 6 = TUESDAY. + * - Schedule uses a 7-bit mask aligned to the mapping above (bit 0 -> WEDNESDAY, bit 6 -> TUESDAY). + */ + +using namespace QPI; + +/// Maximum number of players allowed in the lottery for a single epoch (one entry == one ticket). +constexpr uint16 RL_MAX_NUMBER_OF_PLAYERS = 1024; + +/// Maximum number of winners stored in the on-chain winners history ring buffer. +constexpr uint16 RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY = 1024; + +/// Default ticket price (denominated in the smallest currency unit). +constexpr uint64 RL_TICKET_PRICE = 1000000; + +/// Team fee percent of epoch revenue (0..100). +constexpr uint8 RL_TEAM_FEE_PERCENT = 10; + +/// Distribution (shareholders/validators) fee percent of epoch revenue (0..100). +constexpr uint8 RL_SHAREHOLDER_FEE_PERCENT = 20; + +/// Burn percent of epoch revenue (0..100). +constexpr uint8 RL_BURN_PERCENT = 2; + +/// Throttling period: process BEGIN_TICK logic once per this many ticks. +constexpr uint8 RL_TICK_UPDATE_PERIOD = 100; + +/// Default draw hour (UTC). +constexpr uint8 RL_DEFAULT_DRAW_HOUR = 11; // 11:00 UTC + +constexpr uint8 RL_DEFAULT_SCHEDULE = 1 << WEDNESDAY | 1 << FRIDAY | 1 << SUNDAY; // Draws on WED, FRI, SUN + +constexpr uint32 RL_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; + +/// Placeholder structure for future extensions. +struct RL2 +{ +}; + +/** + * @brief Main contract class implementing the random lottery mechanics. + * + * Lifecycle: + * 1. INITIALIZE sets defaults (fees, ticket price, state LOCKED). + * 2. BEGIN_EPOCH opens ticket selling (SELLING). + * 3. Users call BuyTicket while SELLING. + * 4. END_EPOCH closes, computes fees, selects winner, distributes, burns rest. + * 5. Players list is cleared for next epoch. + */ +struct RL : public ContractBase +{ +public: + /** + * @brief High-level finite state of the lottery. + * SELLING: tickets can be purchased. + * LOCKED: purchases closed; waiting for epoch transition. + */ + enum class EState : uint8 + { + SELLING = 1 << 0, // Ticket selling is open + }; + + friend EState operator|(const EState& a, const EState& b) { return static_cast(static_cast(a) | static_cast(b)); } + + friend EState operator&(const EState& a, const EState& b) { return static_cast(static_cast(a) & static_cast(b)); } + + friend EState operator~(const EState& a) { return static_cast(~static_cast(a)); } + + template friend bool operator==(const EState& a, const T& b) { return static_cast(a) == b; } + + template friend bool operator!=(const EState& a, const T& b) { return !(a == b); } + + /** + * @brief Standardized return / error codes for procedures. + */ + enum class EReturnCode : uint8 + { + SUCCESS, + + // Ticket-related errors + TICKET_INVALID_PRICE, // Not enough funds to buy at least one ticket / price mismatch + TICKET_ALL_SOLD_OUT, // No free slots left in players array + TICKET_SELLING_CLOSED, // Attempted to buy while state is LOCKED + // Access-related errors + ACCESS_DENIED, // Caller is not authorized to perform the action + + // Value-related errors + INVALID_VALUE, // Input value is not acceptable + + UNKNOWN_ERROR = UINT8_MAX + }; + + static constexpr uint8 toReturnCode(EReturnCode code) { return static_cast(code); } + + struct NextEpochData + { + uint64 newPrice; // Ticket price to apply after END_EPOCH; 0 means "no change queued" + uint8 schedule; // Schedule bitmask (bit 0 = WEDNESDAY, ..., bit 6 = TUESDAY); applied after END_EPOCH + }; + + //---- User-facing I/O structures ------------------------------------------------------------- + + struct BuyTicket_input + { + }; + + struct BuyTicket_output + { + uint8 returnCode; + }; + + struct GetFees_input + { + }; + + struct GetFees_output + { + uint8 teamFeePercent; // Team share in percent + uint8 distributionFeePercent; // Distribution/shareholders share in percent + uint8 winnerFeePercent; // Winner share in percent + uint8 burnPercent; // Burn share in percent + uint8 returnCode; + }; + + struct GetPlayers_input + { + }; + + struct GetPlayers_output + { + Array players; // Current epoch ticket holders (duplicates allowed) + uint64 playerCounter; // Actual count of filled entries + uint8 returnCode; + }; + + /** + * @brief Stored winner snapshot for an epoch. + */ + struct WinnerInfo + { + id winnerAddress; // Winner address + uint64 revenue; // Payout value sent to the winner for that epoch + uint32 tick; // Tick when the decision was made + uint16 epoch; // Epoch number when winner was recorded + uint8 dayOfWeek; // Day of week when the winner was drawn [0..6] 0 = WEDNESDAY + }; + + struct FillWinnersInfo_input + { + id winnerAddress; // Winner address to store + uint64 revenue; // Winner payout to store + }; + + struct FillWinnersInfo_output + { + }; + + struct FillWinnersInfo_locals + { + WinnerInfo winnerInfo; // Temporary buffer to compose a WinnerInfo record + uint64 insertIdx; // Index in ring buffer where to insert new winner + }; + + struct GetWinners_input + { + }; + + struct GetWinners_output + { + Array winners; // Ring buffer snapshot + uint64 winnersCounter; // Number of valid entries = (totalWinners % capacity) + uint8 returnCode; + }; + + struct GetTicketPrice_input + { + }; + + struct GetTicketPrice_output + { + uint64 ticketPrice; // Current ticket price + }; + + struct GetMaxNumberOfPlayers_input + { + }; + + struct GetMaxNumberOfPlayers_output + { + uint64 numberOfPlayers; // Max capacity of players array + }; + + struct GetState_input + { + }; + + struct GetState_output + { + uint8 currentState; // Current finite state of the lottery + }; + + struct GetBalance_input + { + }; + + struct GetBalance_output + { + uint64 balance; // Current contract net balance (incoming - outgoing) + }; + + // Local variables for GetBalance procedure + struct GetBalance_locals + { + Entity entity; // Entity accounting snapshot for SELF + }; + + // Local variables for BuyTicket procedure + struct BuyTicket_locals + { + uint64 reward; // Funds sent with call (invocationReward) + uint64 capacity; // Max capacity of players array + uint64 slotsLeft; // Remaining slots available to fill this epoch + uint64 desired; // How many tickets the caller wants to buy + uint64 remainder; // Change to return (reward % price) + uint64 toBuy; // Actual number of tickets to purchase (bounded by slotsLeft) + uint64 unfilled; // Portion of desired tickets not purchased due to capacity limit + uint64 refundAmount; // Total refund: remainder + unfilled * price + uint64 i; // Loop counter + }; + + struct ReturnAllTickets_input + { + }; + struct ReturnAllTickets_output + { + }; + + struct ReturnAllTickets_locals + { + uint64 i; // Loop counter for mass-refund + }; + + struct SetPrice_input + { + uint64 newPrice; // New ticket price to be applied at the end of the epoch + }; + + struct SetPrice_output + { + uint8 returnCode; + }; + + struct SetSchedule_input + { + uint8 newSchedule; // New schedule bitmask to be applied at the end of the epoch + }; + + struct SetSchedule_output + { + uint8 returnCode; + }; + + struct BEGIN_TICK_locals + { + id winnerAddress; + id firstPlayer; + m256i mixedSpectrumValue; + Entity entity; + uint64 revenue; + uint64 randomNum; + uint64 shuffleIndex; + uint64 swapIndex; + uint64 winnerAmount; + uint64 teamFee; + uint64 distributionFee; + uint64 burnedAmount; + uint64 index; + FillWinnersInfo_locals fillWinnersInfoLocals; + FillWinnersInfo_input fillWinnersInfoInput; + uint32 currentDateStamp; + uint8 currentDayOfWeek; + uint8 currentHour; + uint8 isWednesday; + uint8 isScheduledToday; + bit hasMultipleParticipants; + ReturnAllTickets_locals returnAllTicketsLocals; + ReturnAllTickets_input returnAllTicketsInput; + ReturnAllTickets_output returnAllTicketsOutput; + FillWinnersInfo_output fillWinnersInfoOutput; + }; + + struct GetNextEpochData_input + { + }; + + struct GetNextEpochData_output + { + NextEpochData nextEpochData; + }; + + struct GetDrawHour_input + { + }; + + struct GetDrawHour_output + { + uint8 drawHour; + }; + + // New: expose current schedule mask + struct GetSchedule_input + { + }; + struct GetSchedule_output + { + uint8 schedule; + }; + +public: + /** + * @brief Registers all externally callable functions and procedures with their numeric + * identifiers. Mapping numbers must remain stable to preserve external interface compatibility. + */ + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_FUNCTION(GetFees, 1); + REGISTER_USER_FUNCTION(GetPlayers, 2); + REGISTER_USER_FUNCTION(GetWinners, 3); + REGISTER_USER_FUNCTION(GetTicketPrice, 4); + REGISTER_USER_FUNCTION(GetMaxNumberOfPlayers, 5); + REGISTER_USER_FUNCTION(GetState, 6); + REGISTER_USER_FUNCTION(GetBalance, 7); + REGISTER_USER_FUNCTION(GetNextEpochData, 8); + REGISTER_USER_FUNCTION(GetDrawHour, 9); + REGISTER_USER_FUNCTION(GetSchedule, 10); + + REGISTER_USER_PROCEDURE(BuyTicket, 1); + REGISTER_USER_PROCEDURE(SetPrice, 2); + REGISTER_USER_PROCEDURE(SetSchedule, 3); + } + + /** + * @brief Contract initialization hook. + * Sets default fees, ticket price, addresses, and locks the lottery (no selling yet). + */ + INITIALIZE() + { + // Set team/developer address (owner and team are the same for now) + state.teamAddress = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, _R, _L, + _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + state.ownerAddress = state.teamAddress; + + // Fee configuration (winner gets the remainder) + state.teamFeePercent = RL_TEAM_FEE_PERCENT; + state.distributionFeePercent = RL_SHAREHOLDER_FEE_PERCENT; + state.burnPercent = RL_BURN_PERCENT; + state.winnerFeePercent = 100 - state.teamFeePercent - state.distributionFeePercent - state.burnPercent; + + // Initial ticket price + state.ticketPrice = RL_TICKET_PRICE; + + // Start in LOCKED state; selling must be explicitly opened with BEGIN_EPOCH + enableBuyTicket(state, false); + + // Reset player counter + state.playerCounter = 0; + + // Default schedule: WEDNESDAY + state.schedule = RL_DEFAULT_SCHEDULE; + } + + /** + * @brief Opens ticket selling for a new epoch. + */ + BEGIN_EPOCH() + { + if (state.schedule == 0) + { + // Default to WEDNESDAY if no schedule is set (bit 0) + state.schedule = RL_DEFAULT_SCHEDULE; + } + + if (state.drawHour == 0) + { + state.drawHour = RL_DEFAULT_DRAW_HOUR; // Default draw hour (UTC) + } + + // Mark the current date as already processed to avoid immediate draw on the same calendar day + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), state.lastDrawDateStamp); + + // Open selling for the new epoch + enableBuyTicket(state, state.lastDrawDateStamp != RL_DEFAULT_INIT_TIME); + } + + END_EPOCH() + { + enableBuyTicket(state, false); + + clearStateOnEndEpoch(state); + applyNextEpochData(state); + } + + BEGIN_TICK_WITH_LOCALS() + { + // Throttle: run logic only once per RL_TICK_UPDATE_PERIOD ticks + if (mod(qpi.tick(), static_cast(RL_TICK_UPDATE_PERIOD)) != 0) + { + return; + } + + // Snapshot current hour + locals.currentHour = qpi.hour(); + locals.currentDayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + locals.isWednesday = locals.currentDayOfWeek == WEDNESDAY; + + // Do nothing before the configured draw hour + if (locals.currentHour < state.drawHour) + { + return; + } + + // Ensure only one action per calendar day (UTC) + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + + if (locals.currentDateStamp == RL_DEFAULT_INIT_TIME) + { + enableBuyTicket(state, false); + + // Safety check: avoid processing on uninitialized time but remember that this date was encountered + state.lastDrawDateStamp = RL_DEFAULT_INIT_TIME; + return; + } + + // Set lastDrawDateStamp on first valid date processed + if (state.lastDrawDateStamp == RL_DEFAULT_INIT_TIME) + { + enableBuyTicket(state, true); + + if (locals.isWednesday) + { + state.lastDrawDateStamp = locals.currentDateStamp; + } + else + { + state.lastDrawDateStamp = 0; + } + } + + if (state.lastDrawDateStamp == locals.currentDateStamp) + { + return; + } + + locals.isScheduledToday = ((state.schedule & (1u << locals.currentDayOfWeek)) != 0); + + // Two-Wednesdays rule: + // - First Wednesday (epoch start) is "consumed" in BEGIN_EPOCH (we set lastDrawDateStamp), + // - Any subsequent Wednesday performs a draw and leaves selling CLOSED until next BEGIN_EPOCH, + // - Any other day performs a draw only if included in schedule and then re-opens selling. + if (!locals.isWednesday && !locals.isScheduledToday) + { + return; // Non-Wednesday day that is not scheduled: nothing to do + } + + // Mark today's action and timestamp + state.lastDrawDateStamp = locals.currentDateStamp; + + // Temporarily close selling for the draw + enableBuyTicket(state, false); + + // Draw + { + locals.hasMultipleParticipants = false; + if (state.playerCounter >= 2) + { + for (locals.index = 1; locals.index < state.playerCounter; ++locals.index) + { + if (state.players.get(locals.index) != state.players.get(0)) + { + locals.hasMultipleParticipants = true; + break; + } + } + } + + if (!locals.hasMultipleParticipants) + { + ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); + } + else + { + // Deterministically shuffle players before drawing so all nodes observe the same order + locals.mixedSpectrumValue = qpi.getPrevSpectrumDigest(); + locals.mixedSpectrumValue.u64._0 ^= qpi.tick(); + locals.mixedSpectrumValue.u64._1 ^= state.playerCounter; + locals.randomNum = qpi.K12(locals.mixedSpectrumValue).u64._0; + + for (locals.shuffleIndex = state.playerCounter - 1; locals.shuffleIndex > 0; --locals.shuffleIndex) + { + locals.randomNum ^= locals.randomNum << 13; + locals.randomNum ^= locals.randomNum >> 7; + locals.randomNum ^= locals.randomNum << 17; + locals.swapIndex = mod(locals.randomNum, locals.shuffleIndex + 1); + + if (locals.swapIndex != locals.shuffleIndex) + { + locals.firstPlayer = state.players.get(locals.shuffleIndex); + state.players.set(locals.shuffleIndex, state.players.get(locals.swapIndex)); + state.players.set(locals.swapIndex, locals.firstPlayer); + } + } + + // Current contract net balance = incoming - outgoing for this contract + qpi.getEntity(SELF, locals.entity); + getSCRevenue(locals.entity, locals.revenue); + + // Winner selection (pseudo-random using K12(prevSpectrumDigest)). + { + locals.winnerAddress = id::zero(); + + if (state.playerCounter != 0) + { + locals.randomNum = qpi.K12(locals.mixedSpectrumValue).u64._0; + locals.randomNum = mod(locals.randomNum, state.playerCounter); + + // Index directly into players array + locals.winnerAddress = state.players.get(locals.randomNum); + } + } + + if (locals.winnerAddress != id::zero()) + { + // Split revenue by configured percentages + locals.winnerAmount = div(smul(locals.revenue, static_cast(state.winnerFeePercent)), 100ULL); + locals.teamFee = div(smul(locals.revenue, static_cast(state.teamFeePercent)), 100ULL); + locals.distributionFee = div(smul(locals.revenue, static_cast(state.distributionFeePercent)), 100ULL); + locals.burnedAmount = div(smul(locals.revenue, static_cast(state.burnPercent)), 100ULL); + + // Team payout + if (locals.teamFee > 0) + { + qpi.transfer(state.teamAddress, locals.teamFee); + } + + // Distribution payout (equal per validator) + if (locals.distributionFee > 0) + { + qpi.distributeDividends(div(locals.distributionFee, static_cast(NUMBER_OF_COMPUTORS))); + } + + // Winner payout + if (locals.winnerAmount > 0) + { + qpi.transfer(locals.winnerAddress, locals.winnerAmount); + } + + // Burn configured portion + if (locals.burnedAmount > 0) + { + qpi.burn(locals.burnedAmount); + } + + // Store winner snapshot into history (ring buffer) + locals.fillWinnersInfoInput.winnerAddress = locals.winnerAddress; + locals.fillWinnersInfoInput.revenue = locals.winnerAmount; + FillWinnersInfo(qpi, state, locals.fillWinnersInfoInput, locals.fillWinnersInfoOutput, locals.fillWinnersInfoLocals); + } + else + { + // Fallback: if winner couldn't be selected (should not happen), refund all tickets + ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); + } + } + } + + clearStateOnEndDraw(state); + + // Resume selling unless today is Wednesday (remains closed until next epoch) + enableBuyTicket(state, !locals.isWednesday); + } + + POST_INCOMING_TRANSFER() + { + switch (input.type) + { + case TransferType::standardTransaction: + // Return any funds sent via standard transaction + if (input.amount > 0) + { + qpi.transfer(input.sourceId, input.amount); + } + default: break; + } + } + + /** + * @brief Returns currently configured fee percentages. + */ + PUBLIC_FUNCTION(GetFees) + { + output.teamFeePercent = state.teamFeePercent; + output.distributionFeePercent = state.distributionFeePercent; + output.winnerFeePercent = state.winnerFeePercent; + output.burnPercent = state.burnPercent; + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + /** + * @brief Retrieves the active players list for the ongoing epoch. + */ + PUBLIC_FUNCTION(GetPlayers) + { + output.players = state.players; + output.playerCounter = min(state.playerCounter, state.players.capacity()); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + /** + * @brief Returns historical winners (ring buffer segment). + */ + PUBLIC_FUNCTION(GetWinners) + { + output.winners = state.winners; + getWinnerCounter(state, output.winnersCounter); + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_FUNCTION(GetTicketPrice) { output.ticketPrice = state.ticketPrice; } + PUBLIC_FUNCTION(GetMaxNumberOfPlayers) { output.numberOfPlayers = RL_MAX_NUMBER_OF_PLAYERS; } + PUBLIC_FUNCTION(GetState) { output.currentState = static_cast(state.currentState); } + PUBLIC_FUNCTION(GetNextEpochData) { output.nextEpochData = state.nexEpochData; } + PUBLIC_FUNCTION(GetDrawHour) { output.drawHour = state.drawHour; } + PUBLIC_FUNCTION(GetSchedule) { output.schedule = state.schedule; } + PUBLIC_FUNCTION_WITH_LOCALS(GetBalance) + { + qpi.getEntity(SELF, locals.entity); + getSCRevenue(locals.entity, output.balance); + } + + PUBLIC_PROCEDURE(SetPrice) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + // Only team/owner can queue a price change + if (qpi.invocator() != state.teamAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + // Zero price is invalid + if (input.newPrice == 0) + { + output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); + return; + } + + // Defer application until END_EPOCH + state.nexEpochData.newPrice = input.newPrice; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + PUBLIC_PROCEDURE(SetSchedule) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.teamAddress) + { + output.returnCode = toReturnCode(EReturnCode::ACCESS_DENIED); + return; + } + + if (input.newSchedule == 0) + { + output.returnCode = toReturnCode(EReturnCode::INVALID_VALUE); + return; + } + + state.nexEpochData.schedule = input.newSchedule; + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + + /** + * @brief Attempts to buy tickets while SELLING state is active. + * Logic: + * - If locked: refund full invocationReward and return TICKET_SELLING_CLOSED. + * - If reward < price: refund full reward and return TICKET_INVALID_PRICE. + * - If no capacity left: refund full reward and return TICKET_ALL_SOLD_OUT. + * - Otherwise: add up to slotsLeft tickets; refund remainder and unfilled part. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(BuyTicket) + { + locals.reward = qpi.invocationReward(); + + // Selling closed: refund any attached funds and exit + if (!isSellingOpen(state)) + { + if (locals.reward > 0) + { + qpi.transfer(qpi.invocator(), locals.reward); + } + + output.returnCode = toReturnCode(EReturnCode::TICKET_SELLING_CLOSED); + return; + } + + // Not enough to buy even a single ticket: refund everything + if (locals.reward < state.ticketPrice) + { + if (locals.reward > 0) + { + qpi.transfer(qpi.invocator(), locals.reward); + } + + output.returnCode = toReturnCode(EReturnCode::TICKET_INVALID_PRICE); + return; + } + + // Capacity check + locals.capacity = state.players.capacity(); + locals.slotsLeft = (state.playerCounter < locals.capacity) ? (locals.capacity - state.playerCounter) : 0; + if (locals.slotsLeft == 0) + { + // All sold out: refund full amount + if (locals.reward > 0) + { + qpi.transfer(qpi.invocator(), locals.reward); + } + output.returnCode = toReturnCode(EReturnCode::TICKET_ALL_SOLD_OUT); + return; + } + + // Compute desired number of tickets and change + locals.desired = div(locals.reward, state.ticketPrice); // How many tickets the caller attempts to buy + locals.remainder = mod(locals.reward, state.ticketPrice); // Change to return + locals.toBuy = min(locals.desired, locals.slotsLeft); // Do not exceed available slots + + // Add tickets (the same address may be inserted multiple times) + for (locals.i = 0; locals.i < locals.toBuy; ++locals.i) + { + if (state.playerCounter < locals.capacity) + { + state.players.set(state.playerCounter, qpi.invocator()); + state.playerCounter = min(state.playerCounter + 1, locals.capacity); + } + } + + // Refund change and unfilled portion (if desired > slotsLeft) + locals.unfilled = locals.desired - locals.toBuy; + locals.refundAmount = locals.remainder + (smul(locals.unfilled, state.ticketPrice)); + if (locals.refundAmount > 0) + { + qpi.transfer(qpi.invocator(), locals.refundAmount); + } + + output.returnCode = toReturnCode(EReturnCode::SUCCESS); + } + +private: + /** + * @brief Internal: records a winner into the cyclic winners array. + * Overwrites oldest entries when capacity is exceeded (ring buffer). + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FillWinnersInfo) + { + if (input.winnerAddress == id::zero()) + { + return; // Nothing to store + } + + // Compute ring-buffer index without clamping the total counter + getWinnerCounter(state, locals.insertIdx); + ++state.winnersCounter; + + locals.winnerInfo.winnerAddress = input.winnerAddress; + locals.winnerInfo.revenue = input.revenue; + locals.winnerInfo.epoch = qpi.epoch(); + locals.winnerInfo.tick = qpi.tick(); + locals.winnerInfo.dayOfWeek = qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()); + + state.winners.set(locals.insertIdx, locals.winnerInfo); + } + + PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) + { + // Refund ticket price to each recorded ticket entry (1 transfer per entry) + for (locals.i = 0; locals.i < state.playerCounter; ++locals.i) + { + qpi.transfer(state.players.get(locals.i), state.ticketPrice); + } + } + +protected: + /** + * @brief Circular buffer storing the history of winners. + * Maximum capacity is defined by RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY. + */ + Array winners; + + /** + * @brief Set of players participating in the current lottery epoch. + * Maximum capacity is defined by RL_MAX_NUMBER_OF_PLAYERS. + */ + Array players; + + /** + * @brief Address of the team managing the lottery contract. + * Initialized to a zero address. + */ + id teamAddress; + + /** + * @brief Address of the owner of the lottery contract. + * Initialized to a zero address. + */ + id ownerAddress; + + /** + * @brief Data structure for deferred changes to apply at the end of the epoch. + */ + NextEpochData nexEpochData; + + /** + * @brief Price of a single lottery ticket. + * Value is in the smallest currency unit (e.g., cents). + */ + uint64 ticketPrice; + + /** + * @brief Number of players (tickets sold) in the current epoch. + */ + uint64 playerCounter; + + /** + * @brief Index pointing to the next empty slot in the winners array. + * Used for maintaining the circular buffer of winners. + */ + uint64 winnersCounter; + + /** + * @brief Date/time guard for draw operations. + * lastDrawDateStamp prevents more than one action per calendar day (UTC). + */ + uint8 lastDrawDay; + uint8 lastDrawHour; + uint32 lastDrawDateStamp; // Compact YYYY/MM/DD marker + + /** + * @brief Percentage of the revenue allocated to the team. + * Value is between 0 and 100. + */ + uint8 teamFeePercent; + + /** + * @brief Percentage of the revenue allocated for distribution. + * Value is between 0 and 100. + */ + uint8 distributionFeePercent; + + /** + * @brief Percentage of the revenue allocated to the winner. + * Automatically calculated as the remainder after other fees. + */ + uint8 winnerFeePercent; + + /** + * @brief Percentage of the revenue to be burned. + * Value is between 0 and 100. + */ + uint8 burnPercent; + + /** + * @brief Schedule bitmask: bit 0 = WEDNESDAY, 1 = THURSDAY, ..., 6 = TUESDAY. + * If a bit is set, a draw may occur on that day (subject to drawHour and daily guard). + * Wednesday also follows the "Two-Wednesdays rule" (selling stays closed after Wednesday draw). + */ + uint8 schedule; + + /** + * @brief UTC hour [0..23] when a draw is allowed to run (daily time gate). + */ + uint8 drawHour; + + /** + * @brief Current state of the lottery contract. + * SELLING: tickets available; LOCKED: selling closed. + */ + EState currentState; + +protected: + static void clearStateOnEndEpoch(RL& state) + { + // Prepare for next epoch: clear players and reset daily guards + state.playerCounter = 0; + setMemory(state.players, 0); + + state.lastDrawDateStamp = 0; + } + + static void clearStateOnEndDraw(RL& state) + { + // After each draw period, clear current tickets + state.playerCounter = 0; + setMemory(state.players, 0); + } + + static void applyNextEpochData(RL& state) + { + // Apply deferred ticket price (if any) + if (state.nexEpochData.newPrice != 0) + { + state.ticketPrice = state.nexEpochData.newPrice; + state.nexEpochData.newPrice = 0; + } + + // Apply deferred schedule (if any) + if (state.nexEpochData.schedule != 0) + { + state.schedule = state.nexEpochData.schedule; + state.nexEpochData.schedule = 0; + } + } + + static void enableBuyTicket(RL& state, bool bEnable) + { + state.currentState = bEnable ? state.currentState | EState::SELLING : state.currentState & ~EState::SELLING; + } + + static bool isSellingOpen(const RL& state) { return (state.currentState & EState::SELLING) != 0; } + + static void getWinnerCounter(const RL& state, uint64& outCounter) { outCounter = mod(state.winnersCounter, state.winners.capacity()); } + + // Packs current date into a compact stamp (Y/M/D) used to ensure a single action per calendar day. + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } + + // Reads current net on-chain balance of SELF (incoming - outgoing). + static void getSCRevenue(const Entity& entity, uint64& revenue) { revenue = entity.incomingAmount - entity.outgoingAmount; } + + template static constexpr const T& min(const T& a, const T& b) { return (a < b) ? a : b; } + + template static constexpr const T& max(const T& a, const T& b) { return (a > b) ? a : b; } +}; diff --git a/src/contracts/TestExampleA.h b/src/contracts/TestExampleA.h index ec3910fbe..f8eedc102 100644 --- a/src/contracts/TestExampleA.h +++ b/src/contracts/TestExampleA.h @@ -1,5 +1,7 @@ using namespace QPI; +constexpr uint64 TESTEXA_ASSET_NAME = 18392928276923732; + struct TESTEXA2 { }; @@ -312,7 +314,7 @@ struct TESTEXA : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -327,7 +329,7 @@ struct TESTEXA : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -342,7 +344,7 @@ struct TESTEXA : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -357,7 +359,7 @@ struct TESTEXA : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -424,6 +426,370 @@ struct TESTEXA : public ContractBase output = state.heavyComputationResult; } + //--------------------------------------------------------------- + // SHAREHOLDER PROPOSALS WITH COMPACT STORAGE (OPTIONS: NO/YES) + +public: + // Proposal data type. We only support yes/no voting. + typedef ProposalDataYesNo ProposalDataT; + + // MultiVariables proposal option data type, which is custom per contract + struct MultiVariablesProposalExtraData + { + struct Option + { + uint64 dummyStateVariable1; + uint32 dummyStateVariable2; + sint8 dummyStateVariable3; + }; + + Option optionYesValues; + bool hasValueDummyStateVariable1; + bool hasValueDummyStateVariable2; + bool hasValueDummyStateVariable3; + + bool isValid() const + { + return hasValueDummyStateVariable1 || hasValueDummyStateVariable2 || hasValueDummyStateVariable3; + } + }; + + struct SetShareholderProposal_input + { + ProposalDataT proposalData; + MultiVariablesProposalExtraData multiVarData; // may be skipped when sending TX if not MultiVariables proposal + }; + typedef QPI::SET_SHAREHOLDER_PROPOSAL_output SetShareholderProposal_output; + + PUBLIC_PROCEDURE(SetShareholderProposal) + { + // - fee can be handled as you like + // - input.proposalData.epoch == 0 means clearing a proposal + + // default return code: failure + output = INVALID_PROPOSAL_INDEX; + + // custom checks + if (input.proposalData.epoch != 0) + { + switch (ProposalTypes::cls(input.proposalData.type)) + { + case ProposalTypes::Class::MultiVariables: + // check input + if (!input.multiVarData.isValid()) + return; + + // check that proposed values are in valid range + if (input.multiVarData.hasValueDummyStateVariable1 && input.multiVarData.optionYesValues.dummyStateVariable1 > 1000000000000llu) + return; + if (input.multiVarData.hasValueDummyStateVariable2 && input.multiVarData.optionYesValues.dummyStateVariable2 > 1000000llu) + return; + if (input.multiVarData.hasValueDummyStateVariable3 && (input.multiVarData.optionYesValues.dummyStateVariable3 > 100 || input.multiVarData.optionYesValues.dummyStateVariable3 < -100)) + return; + + break; + + case ProposalTypes::Class::Variable: + // check that variable index is in valid range + if (input.proposalData.variableOptions.variable >= 3) + return; + + // check that proposed value is in valid range + if (input.proposalData.variableOptions.variable == 0 && input.proposalData.variableOptions.value > 1000000000000llu) + return; + if (input.proposalData.variableOptions.variable == 1 && input.proposalData.variableOptions.value > 1000000llu) + return; + if (input.proposalData.variableOptions.variable == 2 && (input.proposalData.variableOptions.value > 100 || input.proposalData.variableOptions.value < -100)) + return; + + break; + + case ProposalTypes::Class::GeneralOptions: + // allow without check + break; + + default: + // this forbids other proposals including transfers and all future propsasl classes not implemented yet + return; + } + } + + // Try to set proposal (checks invocator's rights and general validity of input proposal), returns proposal index + output = qpi(state.proposals).setProposal(qpi.invocator(), input.proposalData); + + if (output != INVALID_PROPOSAL_INDEX) + { + // success + if (ProposalTypes::cls(input.proposalData.type) == ProposalTypes::Class::MultiVariables) + { + // store custom data of multi-variable proposal in array (at position proposalIdx) + state.multiVariablesProposalData.set(output, input.multiVarData); + } + } + } + + typedef ProposalMultiVoteDataV1 SetShareholderVotes_input; + typedef bit SetShareholderVotes_output; + + PUBLIC_PROCEDURE(SetShareholderVotes) + { + // - fee can be handled as you like + + output = qpi(state.proposals).vote(qpi.invocator(), input); + } + + struct END_EPOCH_locals + { + sint32 proposalIndex; + ProposalDataT proposal; + ProposalSummarizedVotingDataV1 results; + MultiVariablesProposalExtraData multiVarData; + }; + + END_EPOCH_WITH_LOCALS() + { + // Analyze proposal results and set variables + + // Iterate all proposals that were open for voting in this epoch ... + locals.proposalIndex = -1; + while ((locals.proposalIndex = qpi(state.proposals).nextProposalIndex(locals.proposalIndex, qpi.epoch())) >= 0) + { + if (!qpi(state.proposals).getProposal(locals.proposalIndex, locals.proposal)) + continue; + + // handle Variable proposal type + if (ProposalTypes::cls(locals.proposal.type) == ProposalTypes::Class::Variable) + { + // Get voting results and check if conditions for proposal acceptance are met + if (!qpi(state.proposals).getVotingSummary(locals.proposalIndex, locals.results)) + continue; + + // Check if the yes option (1) has been accepted + if (locals.results.getAcceptedOption() == 1) + { + if (locals.proposal.variableOptions.variable == 0) + state.dummyStateVariable1 = uint64(locals.proposal.variableOptions.value); + if (locals.proposal.variableOptions.variable == 1) + state.dummyStateVariable2 = uint32(locals.proposal.variableOptions.value); + if (locals.proposal.variableOptions.variable == 2) + state.dummyStateVariable3 = sint8(locals.proposal.variableOptions.value); + } + } + + // handle MultiVariables proposal type + if (ProposalTypes::cls(locals.proposal.type) == ProposalTypes::Class::MultiVariables) + { + // Get voting results and check if conditions for proposal acceptance are met + if (!qpi(state.proposals).getVotingSummary(locals.proposalIndex, locals.results)) + continue; + + // Check if the yes option (1) has been accepted + if (locals.results.getAcceptedOption() == 1) + { + locals.multiVarData = state.multiVariablesProposalData.get(locals.proposalIndex); + + if (locals.multiVarData.hasValueDummyStateVariable1) + state.dummyStateVariable1 = locals.multiVarData.optionYesValues.dummyStateVariable1; + if (locals.multiVarData.hasValueDummyStateVariable2) + state.dummyStateVariable2 = locals.multiVarData.optionYesValues.dummyStateVariable2; + if (locals.multiVarData.hasValueDummyStateVariable3) + state.dummyStateVariable3 = locals.multiVarData.optionYesValues.dummyStateVariable3; + } + } + } + } + + struct GetShareholderProposalIndices_input + { + bit activeProposals; // Set true to return indices of active proposals, false for proposals of prior epochs + sint32 prevProposalIndex; // Set -1 to start getting indices. If returned index array is full, call again with highest index returned. + }; + struct GetShareholderProposalIndices_output + { + uint16 numOfIndices; // Number of valid entries in indices. Call again if it is 64. + Array indices; // Requested proposal indices. Valid entries are in range 0 ... (numOfIndices - 1). + }; + + PUBLIC_FUNCTION(GetShareholderProposalIndices) + { + if (input.activeProposals) + { + // Return proposals that are open for voting in current epoch + // (output is initalized with zeros by contract system) + while ((input.prevProposalIndex = qpi(state.proposals).nextProposalIndex(input.prevProposalIndex, qpi.epoch())) >= 0) + { + output.indices.set(output.numOfIndices, input.prevProposalIndex); + ++output.numOfIndices; + + if (output.numOfIndices == output.indices.capacity()) + break; + } + } + else + { + // Return proposals of previous epochs not overwritten yet + // (output is initalized with zeros by contract system) + while ((input.prevProposalIndex = qpi(state.proposals).nextFinishedProposalIndex(input.prevProposalIndex)) >= 0) + { + output.indices.set(output.numOfIndices, input.prevProposalIndex); + ++output.numOfIndices; + + if (output.numOfIndices == output.indices.capacity()) + break; + } + } + } + + typedef NoData GetShareholderProposalFees_input; + struct GetShareholderProposalFees_output + { + sint64 setProposalFee; + sint64 setVoteFee; + }; + + PUBLIC_FUNCTION(GetShareholderProposalFees) + { + output.setProposalFee = 0; + output.setVoteFee = 0; + } + + struct GetShareholderProposal_input + { + uint16 proposalIndex; + }; + struct GetShareholderProposal_output + { + ProposalDataT proposal; + id proposerPubicKey; + MultiVariablesProposalExtraData multiVarData; + }; + + PUBLIC_FUNCTION(GetShareholderProposal) + { + // On error, output.proposal.type is set to 0 + output.proposerPubicKey = qpi(state.proposals).proposerId(input.proposalIndex); + qpi(state.proposals).getProposal(input.proposalIndex, output.proposal); + if (ProposalTypes::cls(output.proposal.type) == ProposalTypes::Class::MultiVariables) + { + output.multiVarData = state.multiVariablesProposalData.get(input.proposalIndex); + } + } + + struct GetShareholderVotes_input + { + id voter; + uint16 proposalIndex; + }; + typedef ProposalMultiVoteDataV1 GetShareholderVotes_output; + + PUBLIC_FUNCTION(GetShareholderVotes) + { + // On error, output.votes.proposalType is set to 0 + qpi(state.proposals).getVotes( + input.proposalIndex, + input.voter, + output); + } + + + struct GetShareholderVotingResults_input + { + uint16 proposalIndex; + }; + typedef ProposalSummarizedVotingDataV1 GetShareholderVotingResults_output; + + PUBLIC_FUNCTION(GetShareholderVotingResults) + { + // On error, output.totalVotesAuthorized is set to 0 + qpi(state.proposals).getVotingSummary( + input.proposalIndex, output); + } + + struct SET_SHAREHOLDER_PROPOSAL_locals + { + SetShareholderProposal_input userProcInput; + }; + + SET_SHAREHOLDER_PROPOSAL_WITH_LOCALS() + { + copyFromBuffer(locals.userProcInput, input); + CALL(SetShareholderProposal, locals.userProcInput, output); + + // bug-checking: qpi.setShareholder*() must fail + ASSERT(!qpi.setShareholderVotes(10, ProposalMultiVoteDataV1(), qpi.invocationReward())); + ASSERT(qpi.setShareholderProposal(10, input, qpi.invocationReward()) == INVALID_PROPOSAL_INDEX); + } + + SET_SHAREHOLDER_VOTES() + { + CALL(SetShareholderVotes, input, output); + +#ifdef NO_UEFI + // bug-checking: qpi.setShareholder*() must fail + ASSERT(!qpi.setShareholderVotes(10, input, qpi.invocationReward())); + ASSERT(qpi.setShareholderProposal(10, SET_SHAREHOLDER_PROPOSAL_input(), qpi.invocationReward()) == INVALID_PROPOSAL_INDEX); +#endif + } + +protected: + // Variables that can be set with proposals + uint64 dummyStateVariable1; + uint32 dummyStateVariable2; + sint8 dummyStateVariable3; + + // Shareholders of TESTEXA have right to propose and vote. Only 16 slots provided. + typedef ProposalAndVotingByShareholders<16, TESTEXA_ASSET_NAME> ProposersAndVotersT; + + // Proposal and voting storage type + typedef ProposalVoting ProposalVotingT; + + // Proposal storage + ProposalVotingT proposals; + + // MultiVariables proposal option data storage (same number of slots as proposals) + Array multiVariablesProposalData; + + +public: + struct SetProposalInOtherContractAsShareholder_input + { + Array proposalDataBuffer; + uint16 otherContractIndex; + }; + struct SetProposalInOtherContractAsShareholder_output + { + uint16 proposalIndex; + }; + struct SetProposalInOtherContractAsShareholder_locals + { + Array proposalDataBuffer; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(SetProposalInOtherContractAsShareholder) + { + // User procedure for letting TESTEXB create a shareholder proposal in TESTEXA as shareholder of TESTEXA. + // Skipped here: checking that invocator has right to set proposal for this contract (e.g., is contract "admin") + copyToBuffer(locals.proposalDataBuffer, input.proposalDataBuffer); + output.proposalIndex = qpi.setShareholderProposal(input.otherContractIndex, locals.proposalDataBuffer, qpi.invocationReward()); + } + + struct SetVotesInOtherContractAsShareholder_input + { + ProposalMultiVoteDataV1 voteData; + uint16 otherContractIndex; + }; + struct SetVotesInOtherContractAsShareholder_output + { + bit success; + }; + + PUBLIC_PROCEDURE(SetVotesInOtherContractAsShareholder) + { + // User procedure for letting TESTEXB cast shareholder votes in TESTEXA as shareholder of TESTEXA. + // Skipped here: checking that invocator has right to cast votes for this contract (e.g., is contract "admin") + output.success = qpi.setShareholderVotes(input.otherContractIndex, input.voteData, qpi.invocationReward()); + } + //--------------------------------------------------------------- // COMMON PARTS @@ -443,5 +809,19 @@ struct TESTEXA : public ContractBase REGISTER_USER_PROCEDURE(AcquireShareManagementRights, 6); REGISTER_USER_PROCEDURE(QueryQpiFunctionsToState, 7); REGISTER_USER_PROCEDURE(RunHeavyComputation, 8); + + REGISTER_USER_PROCEDURE(SetProposalInOtherContractAsShareholder, 40); + REGISTER_USER_PROCEDURE(SetVotesInOtherContractAsShareholder, 41); + + // Shareholder proposals: use standard function/procedure indices + REGISTER_USER_FUNCTION(GetShareholderProposalFees, 65531); + REGISTER_USER_FUNCTION(GetShareholderProposalIndices, 65532); + REGISTER_USER_FUNCTION(GetShareholderProposal, 65533); + REGISTER_USER_FUNCTION(GetShareholderVotes, 65534); + REGISTER_USER_FUNCTION(GetShareholderVotingResults, 65535); + + REGISTER_USER_PROCEDURE(SetShareholderProposal, 65534); + REGISTER_USER_PROCEDURE(SetShareholderVotes, 65535); + } }; diff --git a/src/contracts/TestExampleB.h b/src/contracts/TestExampleB.h index f5b8d857a..3cd6420d2 100644 --- a/src/contracts/TestExampleB.h +++ b/src/contracts/TestExampleB.h @@ -1,5 +1,7 @@ using namespace QPI; +constexpr uint64 TESTEXB_ASSET_NAME = 18674403253634388; + struct TESTEXB2 { }; @@ -159,7 +161,7 @@ struct TESTEXB : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -174,7 +176,7 @@ struct TESTEXB : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -189,7 +191,7 @@ struct TESTEXB : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -204,7 +206,7 @@ struct TESTEXB : public ContractBase ASSERT(qpi.invocator().u64._0 == input.otherContractIndex); // calling qpi.releaseShares() and qpi.acquireShares() is forbidden in *_SHARES callbacks - // and should return with an error immeditately + // and should return with an error immediately ASSERT(qpi.releaseShares(input.asset, input.owner, input.possessor, input.numberOfShares, input.otherContractIndex, input.otherContractIndex, qpi.invocationReward()) == INVALID_AMOUNT); ASSERT(qpi.acquireShares(input.asset, input.owner, qpi.invocator(), input.numberOfShares, @@ -345,6 +347,247 @@ struct TESTEXB : public ContractBase output = qpi.bidInIPO(input.ipoContractIndex, input.pricePerShare, input.numberOfShares); } + //--------------------------------------------------------------- + // SHAREHOLDER PROPOSALS WITH MULTI-OPTION + SCALAR STORAGE + +protected: + // Variables that can be set with proposals + sint64 fee1; + sint64 fee2; + sint64 fee3; + +public: + // Proposal data type. Support up to 8 options and scalar voting. + typedef ProposalDataV1 ProposalDataT; + + // Shareholders of TESTEXA have right to propose and vote. Only 16 slots provided. + typedef ProposalAndVotingByShareholders<16, TESTEXB_ASSET_NAME> ProposersAndVotersT; + + // Proposal and voting storage type + typedef ProposalVoting ProposalVotingT; + +protected: + // Proposal storage + ProposalVotingT proposals; +public: + + struct SetShareholderProposal_input + { + ProposalDataT proposalData; + }; + typedef QPI::SET_SHAREHOLDER_PROPOSAL_output SetShareholderProposal_output; + + struct SetShareholderProposal_locals + { + uint16 optionCount; + uint16 i; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(SetShareholderProposal) + { + // - fee can be handled as you like + // - input.proposalData.epoch == 0 means clearing a proposal + + // default return code: failure + output = INVALID_PROPOSAL_INDEX; + + // custom checks + if (input.proposalData.epoch != 0) + { + switch (ProposalTypes::cls(input.proposalData.type)) + { + case ProposalTypes::Class::Variable: + // check that variable index is in valid range + if (input.proposalData.variableOptions.variable >= 3) + return; + + // check that proposed value is in valid range + // (in this example, it is independent of the variable index; all fees must be positive) + locals.optionCount = ProposalTypes::optionCount(input.proposalData.type); + if (locals.optionCount == 0) + { + // votes are scalar values + if (input.proposalData.variableScalar.minValue < 0 + || input.proposalData.variableScalar.maxValue < 0 + || input.proposalData.variableScalar.proposedValue < 0) + return; + } + else + { + // votes are option indices (option 0 is no change, value i is option i + 1) + for (locals.i = 0; locals.i < locals.optionCount - 1; ++locals.i) + if (input.proposalData.variableOptions.values.get(locals.i) < 0) + return; + } + + break; + + default: + // this forbids all other proposals including transfers, multi-variable, general, and all future propsasl classes + return; + } + } + + // Try to set proposal (checks invocator's rights and general validity of input proposal), returns proposal index + output = qpi(state.proposals).setProposal(qpi.invocator(), input.proposalData); + } + + + + + struct FinalizeShareholderProposalSetStateVar_input + { + sint32 proposalIndex; + ProposalDataT proposal; + ProposalSummarizedVotingDataV1 results; + sint32 acceptedOption; + sint64 acceptedValue; + }; + typedef NoData FinalizeShareholderProposalSetStateVar_output; + + PRIVATE_PROCEDURE(FinalizeShareholderProposalSetStateVar) + { + if (input.proposal.variableOptions.variable == 0) + state.fee1 = input.acceptedValue; + else if (input.proposal.variableOptions.variable == 1) + state.fee2 = input.acceptedValue; + else if (input.proposal.variableOptions.variable == 2) + state.fee3 = input.acceptedValue; + } + + typedef NoData FinalizeShareholderStateVarProposals_input; + typedef NoData FinalizeShareholderStateVarProposals_output; + struct FinalizeShareholderStateVarProposals_locals + { + FinalizeShareholderProposalSetStateVar_input p; + uint16 proposalClass; + }; + + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeShareholderStateVarProposals) + { + // Analyze proposal results and set variables: + // Iterate all proposals that were open for voting in this epoch ... + locals.p.proposalIndex = -1; + while ((locals.p.proposalIndex = qpi(state.proposals).nextProposalIndex(locals.p.proposalIndex, qpi.epoch())) >= 0) + { + if (!qpi(state.proposals).getProposal(locals.p.proposalIndex, locals.p.proposal)) + continue; + + locals.proposalClass = ProposalTypes::cls(locals.p.proposal.type); + + // Handle proposal type Variable / MultiVariables + if (locals.proposalClass == ProposalTypes::Class::Variable || locals.proposalClass == ProposalTypes::Class::MultiVariables) + { + // Get voting results and check if conditions for proposal acceptance are met + if (!qpi(state.proposals).getVotingSummary(locals.p.proposalIndex, locals.p.results)) + continue; + + if (locals.p.proposal.type == ProposalTypes::VariableScalarMean) + { + if (locals.p.results.totalVotesCasted < QUORUM) + continue; + + locals.p.acceptedValue = locals.p.results.scalarVotingResult; + } + else + { + locals.p.acceptedOption = locals.p.results.getAcceptedOption(); + if (locals.p.acceptedOption <= 0) + continue; + + // option 0 is "no change", option 1 has index 0 in variableOptions + locals.p.acceptedValue = locals.p.proposal.variableOptions.values.get(locals.p.acceptedOption - 1); + } + + CALL(FinalizeShareholderProposalSetStateVar, locals.p, output); + } + } + } + + END_EPOCH() + { + CALL(FinalizeShareholderStateVarProposals, input, output); + } + + + IMPLEMENT_GetShareholderProposalFees(0) + IMPLEMENT_GetShareholderProposal() + IMPLEMENT_GetShareholderProposalIndices() + IMPLEMENT_SetShareholderVotes() + IMPLEMENT_GetShareholderVotes() + IMPLEMENT_GetShareholderVotingResults() + IMPLEMENT_SET_SHAREHOLDER_PROPOSAL() + IMPLEMENT_SET_SHAREHOLDER_VOTES() + +public: + struct SetProposalInOtherContractAsShareholder_input + { + Array proposalDataBuffer; + uint16 otherContractIndex; + }; + struct SetProposalInOtherContractAsShareholder_output + { + uint16 proposalIndex; + }; + struct SetProposalInOtherContractAsShareholder_locals + { + Array proposalDataBuffer; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(SetProposalInOtherContractAsShareholder) + { + // User procedure for letting TESTEXB create a shareholder proposal in TESTEXA as shareholder of TESTEXA. + // Skipped here: checking that invocator has right to set proposal for this contract (e.g., is contract "admin") + copyToBuffer(locals.proposalDataBuffer, input.proposalDataBuffer); + output.proposalIndex = qpi.setShareholderProposal(input.otherContractIndex, locals.proposalDataBuffer, qpi.invocationReward()); + } + + struct SetVotesInOtherContractAsShareholder_input + { + ProposalMultiVoteDataV1 voteData; + uint16 otherContractIndex; + }; + struct SetVotesInOtherContractAsShareholder_output + { + bit success; + }; + + PUBLIC_PROCEDURE(SetVotesInOtherContractAsShareholder) + { + // User procedure for letting TESTEXB cast shareholder votes in TESTEXA as shareholder of TESTEXA. + // Skipped here: checking that invocator has right to cast votes for this contract (e.g., is contract "admin") + output.success = qpi.setShareholderVotes(input.otherContractIndex, input.voteData, qpi.invocationReward()); + } + + // Test inter-contract call error handling + struct TestInterContractCallError_input + { + uint8 dummy; // Dummy field to avoid zero-size struct + }; + + struct TestInterContractCallError_output + { + uint8 errorCode; + uint8 callSucceeded; // 1 if call happened, 0 if it was skipped + }; + + struct TestInterContractCallError_locals + { + TESTEXA::QueryQpiFunctionsToState_input procInput; + TESTEXA::QueryQpiFunctionsToState_output procOutput; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(TestInterContractCallError) + { + // Try to invoke a procedure in TestExampleA + // This will fail if TestExampleA has insufficient fees + INVOKE_OTHER_CONTRACT_PROCEDURE(TESTEXA, QueryQpiFunctionsToState, locals.procInput, locals.procOutput, 0); + + // interContractCallError is now available from the macro + output.errorCode = interContractCallError; + output.callSucceeded = (interContractCallError == NoCallError) ? 1 : 0; + } + //--------------------------------------------------------------- // COMMON PARTS @@ -366,5 +609,10 @@ struct TESTEXB : public ContractBase REGISTER_USER_PROCEDURE(QpiTransfer, 20); REGISTER_USER_PROCEDURE(QpiDistributeDividends, 21); REGISTER_USER_PROCEDURE(QpiBidInIpo, 30); + REGISTER_USER_PROCEDURE(SetProposalInOtherContractAsShareholder, 40); + REGISTER_USER_PROCEDURE(SetVotesInOtherContractAsShareholder, 41); + REGISTER_USER_PROCEDURE(TestInterContractCallError, 50); + + REGISTER_SHAREHOLDER_PROPOSAL_VOTING(); } }; diff --git a/src/contracts/TestExampleC.h b/src/contracts/TestExampleC.h index 32f72d8c9..87b3c4bf8 100644 --- a/src/contracts/TestExampleC.h +++ b/src/contracts/TestExampleC.h @@ -51,13 +51,13 @@ struct TESTEXC : public ContractBase // This is for the ResolveDeadlockCallbackProcedureAndConcurrentFunction in test/contract_testex.cpp: // 1. Check reuse of already owned write lock of TESTEXA and delay execution in order to make sure that the // concurrent contract function TESTEXB::CallTextExAFunc() is running or waiting for read lock of TEXTEXA. - INVOKE_OTHER_CONTRACT_PROCEDURE(TESTEXA, RunHeavyComputation, locals.heavyComputationInput, locals.heavyComputationOutput, 0); + INVOKE_OTHER_CONTRACT_PROCEDURE_E(TESTEXA, RunHeavyComputation, locals.heavyComputationInput, locals.heavyComputationOutput, 0, callError1); // 2. Try to invoke procedure of TESTEXB to trigger deadlock (waiting for release of read lock). #ifdef NO_UEFI printf("Before wait/deadlock in contract %u procedure\n", CONTRACT_INDEX); #endif - INVOKE_OTHER_CONTRACT_PROCEDURE(TESTEXB, SetPreAcquireSharesOutput, locals.textExBInput, locals.textExBOutput, 0); + INVOKE_OTHER_CONTRACT_PROCEDURE_E(TESTEXB, SetPreAcquireSharesOutput, locals.textExBInput, locals.textExBOutput, 0, callError2); #ifdef NO_UEFI printf("After wait/deadlock in contract %u procedure\n", CONTRACT_INDEX); #endif diff --git a/src/contracts/TestExampleD.h b/src/contracts/TestExampleD.h index d78a054de..3cb3e0caf 100644 --- a/src/contracts/TestExampleD.h +++ b/src/contracts/TestExampleD.h @@ -19,7 +19,7 @@ struct TESTEXD : public ContractBase locals.balance = locals.entity.incomingAmount - locals.entity.outgoingAmount; if (locals.balance > NUMBER_OF_COMPUTORS) { - qpi.distributeDividends(locals.balance / NUMBER_OF_COMPUTORS); + qpi.distributeDividends(div(locals.balance, NUMBER_OF_COMPUTORS)); } } diff --git a/src/contracts/VottunBridge.h b/src/contracts/VottunBridge.h new file mode 100644 index 000000000..614060c02 --- /dev/null +++ b/src/contracts/VottunBridge.h @@ -0,0 +1,2128 @@ +using namespace QPI; + +struct VOTTUNBRIDGE2 +{ +}; + +struct VOTTUNBRIDGE : public ContractBase +{ +public: + // Bridge Order Structure + struct BridgeOrder + { + id qubicSender; // Sender address on Qubic + id qubicDestination; // Destination address on Qubic + Array ethAddress; // Destination Ethereum address + uint64 orderId; // Unique ID for the order + uint64 amount; // Amount to transfer + uint8 orderType; // Type of order (e.g., mint, transfer) + uint8 status; // Order status (e.g., Created, Pending, Refunded) + bit fromQubicToEthereum; // Direction of transfer + bit tokensReceived; // Flag to indicate if tokens have been received + bit tokensLocked; // Flag to indicate if tokens are in locked state + }; + + // Input and Output Structs + struct createOrder_input + { + id qubicDestination; // Destination address on Qubic (for EVM to Qubic orders) + uint64 amount; + Array ethAddress; + bit fromQubicToEthereum; + }; + + struct createOrder_output + { + uint8 status; + uint64 orderId; + }; + + struct addManager_input + { + id address; + }; + + struct addManager_output + { + uint8 status; + }; + + struct removeManager_input + { + id address; + }; + + struct removeManager_output + { + uint8 status; + }; + + struct getTotalReceivedTokens_input + { + uint64 amount; + }; + + struct getTotalReceivedTokens_output + { + uint64 totalTokens; + }; + + struct completeOrder_input + { + uint64 orderId; + }; + + struct completeOrder_output + { + uint8 status; + }; + + struct refundOrder_input + { + uint64 orderId; + }; + + struct refundOrder_output + { + uint8 status; + }; + + struct transferToContract_input + { + uint64 amount; + uint64 orderId; + }; + + struct transferToContract_output + { + uint8 status; + }; + + // Withdraw Fees structures + struct withdrawFees_input + { + uint64 amount; + }; + + struct withdrawFees_output + { + uint8 status; + }; + + // Get Available Fees structures + struct getAvailableFees_input + { + // No parameters + }; + + struct getAvailableFees_output + { + uint64 availableFees; + uint64 totalEarnedFees; + uint64 totalDistributedFees; + }; + + // Order Response Structure + struct OrderResponse + { + id originAccount; // Origin account + Array destinationAccount; // Destination account + uint64 orderId; // Order ID as uint64 + uint64 amount; // Amount as uint64 + Array memo; // Notes or metadata + uint32 sourceChain; // Source chain identifier + id qubicDestination; + uint8 status; // Order status (0=pending, 1=completed, 2=refunded) + }; + + struct getOrder_input + { + uint64 orderId; + }; + + struct getOrder_output + { + uint8 status; + OrderResponse order; // Updated response format + Array message; + }; + + struct getContractInfo_input + { + // No parameters + }; + + struct getContractInfo_output + { + Array managers; + uint64 nextOrderId; + uint64 lockedTokens; + uint64 totalReceivedTokens; + uint64 earnedFees; + uint32 tradeFeeBillionths; + uint32 sourceChain; + // Debug info + Array firstOrders; // First 16 orders + uint64 totalOrdersFound; // How many non-empty orders exist + uint64 emptySlots; + // Multisig info + Array multisigAdmins; // List of multisig admins + uint8 numberOfAdmins; // Number of active admins + uint8 requiredApprovals; // Required approvals threshold + uint64 totalProposals; // Total number of active proposals + }; + + // Logger structures + struct EthBridgeLogger + { + uint32 _contractIndex; // Index of the contract + uint32 _errorCode; // Error code + uint64 _orderId; // Order ID if applicable + uint64 _amount; // Amount involved in the operation + sint8 _terminator; // Marks the end of the logged data + }; + + struct AddressChangeLogger + { + id _newAdminAddress; + uint32 _contractIndex; + uint8 _eventCode; // Event code 'adminchanged' + sint8 _terminator; + }; + + struct TokensLogger + { + uint32 _contractIndex; + uint64 _lockedTokens; // Balance tokens locked + uint64 _totalReceivedTokens; // Balance total receivedTokens + sint8 _terminator; + }; + + struct getTotalLockedTokens_locals + { + EthBridgeLogger log; + }; + + struct getTotalLockedTokens_input + { + // No input parameters + }; + + struct getTotalLockedTokens_output + { + uint64 totalLockedTokens; + }; + + // Enum for error codes + enum EthBridgeError + { + onlyManagersCanCompleteOrders = 1, + invalidAmount = 2, + insufficientTransactionFee = 3, + orderNotFound = 4, + invalidOrderState = 5, + insufficientLockedTokens = 6, + transferFailed = 7, + maxManagersReached = 8, + notAuthorized = 9, + onlyManagersCanRefundOrders = 10, + proposalNotFound = 11, + proposalAlreadyExecuted = 12, + proposalAlreadyApproved = 13, + notOwner = 14, + maxProposalsReached = 15 + }; + + // Enum for proposal types + enum ProposalType + { + PROPOSAL_SET_ADMIN = 1, + PROPOSAL_ADD_MANAGER = 2, + PROPOSAL_REMOVE_MANAGER = 3, + PROPOSAL_WITHDRAW_FEES = 4, + PROPOSAL_CHANGE_THRESHOLD = 5 + }; + + // Admin proposal structure for multisig + struct AdminProposal + { + uint64 proposalId; + uint8 proposalType; // Type from ProposalType enum + id targetAddress; // For setAdmin/addManager/removeManager (new admin address) + id oldAddress; // For setAdmin: which admin to replace + uint64 amount; // For withdrawFees or changeThreshold + Array approvals; // Array of owner IDs who approved + uint8 approvalsCount; // Count of approvals + bit executed; // Whether proposal was executed + bit active; // Whether proposal is active (not cancelled) + }; + +public: + // Contract State + Array orders; + id feeRecipient; // Specific wallet to receive fees + Array managers; // Managers list + uint64 nextOrderId; // Counter for order IDs + uint64 lockedTokens; // Total locked tokens in the contract (balance) + uint64 totalReceivedTokens; // Total tokens received + uint32 sourceChain; // Source chain identifier (e.g., Ethereum=1, Qubic=0) + uint32 _tradeFeeBillionths; // Trade fee in billionths (e.g., 0.5% = 5,000,000) + uint64 _earnedFees; // Accumulated fees from trades + uint64 _distributedFees; // Fees already distributed to shareholders + uint64 _earnedFeesQubic; // Accumulated fees from Qubic trades + uint64 _distributedFeesQubic; // Fees already distributed to Qubic shareholders + uint64 _reservedFees; // Fees reserved for pending orders (not distributed yet) + uint64 _reservedFeesQubic; // Qubic fees reserved for pending orders (not distributed yet) + + // Multisig state + Array admins; // List of multisig admins + uint8 numberOfAdmins; // Number of active admins + uint8 requiredApprovals; // Threshold: number of approvals needed (2 of 3) + Array proposals; // Pending admin proposals + uint64 nextProposalId; // Counter for proposal IDs + + // Internal methods for admin/manager permissions + typedef id isManager_input; + typedef bit isManager_output; + + struct isManager_locals + { + uint64 i; + }; + + PRIVATE_FUNCTION_WITH_LOCALS(isManager) + { + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + if (state.managers.get(locals.i) == input) + { + output = true; + return; + } + } + output = false; + } + + typedef id isMultisigAdmin_input; + typedef bit isMultisigAdmin_output; + + struct isMultisigAdmin_locals + { + uint64 i; + }; + + PRIVATE_FUNCTION_WITH_LOCALS(isMultisigAdmin) + { + for (locals.i = 0; locals.i < (uint64)state.numberOfAdmins; ++locals.i) + { + if (state.admins.get(locals.i) == input) + { + output = true; + return; + } + } + output = false; + } + +public: + // Create a new order and lock tokens + struct createOrder_locals + { + BridgeOrder newOrder; + EthBridgeLogger log; + uint64 i; + uint64 j; + bit slotFound; + uint64 cleanedSlots; // Counter for cleaned slots + BridgeOrder emptyOrder; // Empty order to clean slots + uint64 requiredFeeEth; + uint64 requiredFeeQubic; + uint64 totalRequiredFee; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(createOrder) + { + // Validate the input + if (input.amount == 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + 0, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 1; // Error + return; + } + + // Calculate fees as percentage of amount (0.5% each, 1% total) + locals.requiredFeeEth = div(input.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.requiredFeeQubic = div(input.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.totalRequiredFee = locals.requiredFeeEth + locals.requiredFeeQubic; + + // Verify that the fee paid is sufficient for both fees + if (qpi.invocationReward() < static_cast(locals.totalRequiredFee)) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientTransactionFee, + 0, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientTransactionFee; + return; + } + + // Create the order + locals.newOrder.orderId = state.nextOrderId++; + locals.newOrder.qubicSender = qpi.invocator(); + + // Set qubicDestination according to the direction + if (!input.fromQubicToEthereum) + { + // EVM TO QUBIC + locals.newOrder.qubicDestination = input.qubicDestination; + + // Verify that there are enough locked tokens for EVM to Qubic orders + if (state.lockedTokens < input.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientLockedTokens, + 0, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientLockedTokens; // Error + return; + } + } + else + { + // QUBIC TO EVM + locals.newOrder.qubicDestination = qpi.invocator(); + } + + for (locals.i = 0; locals.i < 42; ++locals.i) + { + locals.newOrder.ethAddress.set(locals.i, input.ethAddress.get(locals.i)); + } + locals.newOrder.amount = input.amount; + locals.newOrder.orderType = 0; // Default order type + locals.newOrder.status = 0; // Created + locals.newOrder.fromQubicToEthereum = input.fromQubicToEthereum; + locals.newOrder.tokensReceived = false; + locals.newOrder.tokensLocked = false; + + // Store the order + locals.slotFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).status == 255) + { // Empty slot + state.orders.set(locals.i, locals.newOrder); + locals.slotFound = true; + + // Accumulate fees only after order is successfully created + state._earnedFees += locals.requiredFeeEth; + state._earnedFeesQubic += locals.requiredFeeQubic; + + // Reserve fees for this pending order (won't be distributed until complete/refund) + state._reservedFees += locals.requiredFeeEth; + state._reservedFeesQubic += locals.requiredFeeQubic; + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + locals.newOrder.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; // Success + output.orderId = locals.newOrder.orderId; + return; + } + } + + // No available slots - attempt cleanup of completed orders + if (!locals.slotFound) + { + // Clean up completed and refunded orders to free slots + locals.cleanedSlots = 0; + for (locals.j = 0; locals.j < state.orders.capacity(); ++locals.j) + { + if (state.orders.get(locals.j).status == 1 || state.orders.get(locals.j).status == 2) // Completed or Refunded + { + // Create empty order to overwrite + locals.emptyOrder.status = 255; // Mark as empty + locals.emptyOrder.orderId = 0; + locals.emptyOrder.amount = 0; + // Clear other fields as needed + state.orders.set(locals.j, locals.emptyOrder); + locals.cleanedSlots++; + } + } + + // If we cleaned some slots, try to find a slot again + if (locals.cleanedSlots > 0) + { + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).status == 255) + { // Empty slot + state.orders.set(locals.i, locals.newOrder); + locals.slotFound = true; + + // Accumulate fees only after order is successfully created + state._earnedFees += locals.requiredFeeEth; + state._earnedFeesQubic += locals.requiredFeeQubic; + + // Reserve fees for this pending order (won't be distributed until complete/refund) + state._reservedFees += locals.requiredFeeEth; + state._reservedFeesQubic += locals.requiredFeeQubic; + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + locals.newOrder.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; // Success + output.orderId = locals.newOrder.orderId; + return; + } + } + } + + // If still no slots available after cleanup + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 99, // Custom error code for "no available slots" + 0, // No orderId + locals.cleanedSlots, // Number of slots cleaned + 0 }; // Terminator + LOG_INFO(locals.log); + output.status = 3; // Error: no available slots + return; + } + } + + // Retrieve an order + struct getOrder_locals + { + EthBridgeLogger log; + BridgeOrder order; + OrderResponse orderResp; + uint64 i; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getOrder) + { + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + locals.order = state.orders.get(locals.i); + if (locals.order.orderId == input.orderId && locals.order.status != 255) + { + // Populate OrderResponse with BridgeOrder data + locals.orderResp.orderId = locals.order.orderId; + locals.orderResp.originAccount = locals.order.qubicSender; + locals.orderResp.destinationAccount = locals.order.ethAddress; + locals.orderResp.amount = locals.order.amount; + locals.orderResp.sourceChain = state.sourceChain; + locals.orderResp.qubicDestination = locals.order.qubicDestination; + locals.orderResp.status = locals.order.status; + + output.status = 0; // Success + output.order = locals.orderResp; + return; + } + } + + // If order not found + output.status = 1; // Error + } + + // Multisig Proposal Functions + + // Create proposal structures + struct createProposal_input + { + uint8 proposalType; // Type of proposal + id targetAddress; // Target address (new admin/manager address) + id oldAddress; // Old address (for setAdmin: which admin to replace) + uint64 amount; // Amount (for withdrawFees or changeThreshold) + }; + + struct createProposal_output + { + uint8 status; + uint64 proposalId; + }; + + struct createProposal_locals + { + EthBridgeLogger log; + id invocatorAddress; + uint64 i; + uint64 j; + bit slotFound; + uint64 slotIndex; + AdminProposal newProposal; + AdminProposal emptyProposal; + bit isMultisigAdminResult; + uint64 freeSlots; + uint64 cleanedSlots; + }; + + // Approve proposal structures + struct approveProposal_input + { + uint64 proposalId; + }; + + struct approveProposal_output + { + uint8 status; + bit executed; + }; + + struct approveProposal_locals + { + EthBridgeLogger log; + id invocatorAddress; + AddressChangeLogger adminLog; + AdminProposal proposal; + uint64 i; + bit found; + bit alreadyApproved; + bit isMultisigAdminResult; + uint64 proposalIndex; + uint64 availableFees; + bit adminAdded; + uint64 managerCount; + }; + + // Get proposal structures + struct getProposal_input + { + uint64 proposalId; + }; + + struct getProposal_output + { + uint8 status; + AdminProposal proposal; + }; + + struct getProposal_locals + { + uint64 i; + }; + + // Create a new proposal (only multisig admins can create) + PUBLIC_PROCEDURE_WITH_LOCALS(createProposal) + { + // Verify that the invocator is a multisig admin + locals.invocatorAddress = qpi.invocator(); + CALL(isMultisigAdmin, locals.invocatorAddress, locals.isMultisigAdminResult); + if (!locals.isMultisigAdminResult) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notOwner, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notOwner; + return; + } + + // Validate proposal type + if (input.proposalType < PROPOSAL_SET_ADMIN || input.proposalType > PROPOSAL_CHANGE_THRESHOLD) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, // Reusing error code + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Count free slots and find an empty slot for the proposal + locals.slotFound = false; + locals.freeSlots = 0; + locals.slotIndex = 0; + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (!state.proposals.get(locals.i).active && state.proposals.get(locals.i).proposalId == 0) + { + locals.freeSlots++; + if (!locals.slotFound) + { + locals.slotFound = true; + locals.slotIndex = locals.i; // Save the slot index + // Don't break, continue counting free slots + } + } + } + + // If found slot but less than 5 free slots, cleanup executed or inactive proposals + if (locals.slotFound && locals.freeSlots < 5) + { + locals.cleanedSlots = 0; + for (locals.j = 0; locals.j < state.proposals.capacity(); ++locals.j) + { + // Clean executed proposals OR inactive proposals with a proposalId (failed/abandoned) + if (state.proposals.get(locals.j).executed || + (!state.proposals.get(locals.j).active && state.proposals.get(locals.j).proposalId > 0)) + { + // Clear proposal + locals.emptyProposal.proposalId = 0; + locals.emptyProposal.proposalType = 0; + locals.emptyProposal.approvalsCount = 0; + locals.emptyProposal.executed = false; + locals.emptyProposal.active = false; + state.proposals.set(locals.j, locals.emptyProposal); + locals.cleanedSlots++; + } + } + } + + // If no slot found at all, try cleanup and search again + if (!locals.slotFound) + { + // Attempt cleanup of executed or inactive proposals + locals.cleanedSlots = 0; + for (locals.j = 0; locals.j < state.proposals.capacity(); ++locals.j) + { + // Clean executed proposals OR inactive proposals with a proposalId (failed/abandoned) + if (state.proposals.get(locals.j).executed || + (!state.proposals.get(locals.j).active && state.proposals.get(locals.j).proposalId > 0)) + { + // Clear proposal + locals.emptyProposal.proposalId = 0; + locals.emptyProposal.proposalType = 0; + locals.emptyProposal.approvalsCount = 0; + locals.emptyProposal.executed = false; + locals.emptyProposal.active = false; + state.proposals.set(locals.j, locals.emptyProposal); + locals.cleanedSlots++; + } + } + + // Try to find slot again after cleanup + if (locals.cleanedSlots > 0) + { + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (!state.proposals.get(locals.i).active && state.proposals.get(locals.i).proposalId == 0) + { + locals.slotFound = true; + locals.slotIndex = locals.i; // Save the slot index + break; + } + } + } + + // If still no slot available + if (!locals.slotFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::maxProposalsReached, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::maxProposalsReached; + return; + } + } + + // Create the new proposal + locals.newProposal.proposalId = state.nextProposalId++; + locals.newProposal.proposalType = input.proposalType; + locals.newProposal.targetAddress = input.targetAddress; + locals.newProposal.oldAddress = input.oldAddress; + locals.newProposal.amount = input.amount; + locals.newProposal.approvalsCount = 1; // Creator automatically approves + locals.newProposal.executed = false; + locals.newProposal.active = true; + + // Set creator as first approver + locals.newProposal.approvals.set(0, qpi.invocator()); + + // Store the proposal + state.proposals.set(locals.slotIndex, locals.newProposal); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + locals.newProposal.proposalId, + input.amount, + 0 }; + LOG_INFO(locals.log); + + output.status = 0; // Success + output.proposalId = locals.newProposal.proposalId; + } + + // Approve a proposal (only multisig admins can approve) + PUBLIC_PROCEDURE_WITH_LOCALS(approveProposal) + { + // Verify that the invocator is a multisig admin + locals.invocatorAddress = qpi.invocator(); + CALL(isMultisigAdmin, locals.invocatorAddress, locals.isMultisigAdminResult); + if (!locals.isMultisigAdminResult) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notOwner, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notOwner; + output.executed = false; + return; + } + + // Find the proposal + locals.found = false; + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + locals.proposal = state.proposals.get(locals.i); + if (locals.proposal.proposalId == input.proposalId && locals.proposal.active) + { + locals.found = true; + locals.proposalIndex = locals.i; + break; + } + } + + if (!locals.found) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalNotFound, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalNotFound; + output.executed = false; + return; + } + + // Check if already executed + if (locals.proposal.executed) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalAlreadyExecuted, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalAlreadyExecuted; + output.executed = false; + return; + } + + // Check if this owner has already approved + locals.alreadyApproved = false; + for (locals.i = 0; locals.i < (uint64)locals.proposal.approvalsCount; ++locals.i) + { + if (locals.proposal.approvals.get(locals.i) == qpi.invocator()) + { + locals.alreadyApproved = true; + break; + } + } + + if (locals.alreadyApproved) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalAlreadyApproved, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalAlreadyApproved; + output.executed = false; + return; + } + + // Add approval + locals.proposal.approvals.set((uint64)locals.proposal.approvalsCount, qpi.invocator()); + locals.proposal.approvalsCount++; + + // Check if threshold reached and execute + if (locals.proposal.approvalsCount >= state.requiredApprovals) + { + // Execute the proposal based on type + if (locals.proposal.proposalType == PROPOSAL_SET_ADMIN) + { + // Replace existing admin with new admin (max 3 admins: 2 of 3 multisig) + // oldAddress specifies which admin to replace + + // SECURITY: Check that targetAddress is not already an admin (prevent duplicates) + locals.adminAdded = false; + for (locals.i = 0; locals.i < (uint64)state.numberOfAdmins; ++locals.i) + { + if (state.admins.get(locals.i) == locals.proposal.targetAddress) + { + // targetAddress is already an admin, reject to prevent duplicate voting power + locals.adminAdded = true; // Reuse flag to indicate rejection + break; + } + } + + // Only proceed if targetAddress is not already an admin + if (!locals.adminAdded) + { + for (locals.i = 0; locals.i < state.admins.capacity(); ++locals.i) + { + if (state.admins.get(locals.i) == locals.proposal.oldAddress) + { + // Replace the old admin with the new one + state.admins.set(locals.i, locals.proposal.targetAddress); + locals.adminAdded = true; + locals.adminLog = AddressChangeLogger{ + locals.proposal.targetAddress, + CONTRACT_INDEX, + 1, // Admin changed + 0 }; + LOG_INFO(locals.adminLog); + break; + } + } + } + // numberOfAdmins stays the same (we're replacing, not adding) + } + else if (locals.proposal.proposalType == PROPOSAL_ADD_MANAGER) + { + // SECURITY: Check that targetAddress is not already a manager (prevent duplicates) + locals.adminAdded = false; + locals.managerCount = 0; + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + if (state.managers.get(locals.i) == locals.proposal.targetAddress) + { + // targetAddress is already a manager, reject + locals.adminAdded = true; + break; + } + if (state.managers.get(locals.i) != NULL_ID) + { + locals.managerCount++; + } + } + + // LIMIT: Check that we don't exceed 3 managers + if (locals.managerCount >= 3) + { + locals.adminAdded = true; // Reject if already 3 managers + } + + // Only proceed if targetAddress is not already a manager and limit not reached + if (!locals.adminAdded) + { + // Find empty slot in managers + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + if (state.managers.get(locals.i) == NULL_ID) + { + state.managers.set(locals.i, locals.proposal.targetAddress); + locals.adminLog = AddressChangeLogger{ + locals.proposal.targetAddress, + CONTRACT_INDEX, + 2, // Manager added + 0 }; + LOG_INFO(locals.adminLog); + break; + } + } + } + } + else if (locals.proposal.proposalType == PROPOSAL_REMOVE_MANAGER) + { + // Find and remove manager + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + if (state.managers.get(locals.i) == locals.proposal.targetAddress) + { + state.managers.set(locals.i, NULL_ID); + locals.adminLog = AddressChangeLogger{ + locals.proposal.targetAddress, + CONTRACT_INDEX, + 3, // Manager removed + 0 }; + LOG_INFO(locals.adminLog); + break; + } + } + } + else if (locals.proposal.proposalType == PROPOSAL_WITHDRAW_FEES) + { + // Calculate available fees (excluding reserved fees for pending orders) + locals.availableFees = (state._earnedFees > (state._distributedFees + state._reservedFees)) + ? (state._earnedFees - state._distributedFees - state._reservedFees) + : 0; + if (locals.proposal.amount <= locals.availableFees && locals.proposal.amount > 0) + { + if (qpi.transfer(state.feeRecipient, locals.proposal.amount) >= 0) + { + state._distributedFees += locals.proposal.amount; + } + } + } + else if (locals.proposal.proposalType == PROPOSAL_CHANGE_THRESHOLD) + { + // Amount field is used to store new threshold + // Hard limit: minimum threshold is 2 to maintain multisig security + if (locals.proposal.amount >= 2 && locals.proposal.amount <= (uint64)state.numberOfAdmins) + { + state.requiredApprovals = (uint8)locals.proposal.amount; + } + } + + locals.proposal.executed = true; + output.executed = true; + } + else + { + output.executed = false; + } + + // Update the proposal + state.proposals.set(locals.proposalIndex, locals.proposal); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + input.proposalId, + locals.proposal.approvalsCount, + 0 }; + LOG_INFO(locals.log); + + output.status = 0; // Success + } + + // Get proposal details + PUBLIC_FUNCTION_WITH_LOCALS(getProposal) + { + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (state.proposals.get(locals.i).proposalId == input.proposalId) + { + output.proposal = state.proposals.get(locals.i); + output.status = 0; // Success + return; + } + } + + output.status = EthBridgeError::proposalNotFound; + } + + // Cancel proposal structures + struct cancelProposal_input + { + uint64 proposalId; + }; + + struct cancelProposal_output + { + uint8 status; + }; + + struct cancelProposal_locals + { + EthBridgeLogger log; + AdminProposal proposal; + uint64 i; + bit found; + }; + + // Cancel a proposal (only the creator can cancel) + PUBLIC_PROCEDURE_WITH_LOCALS(cancelProposal) + { + // Find the proposal + locals.found = false; + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + locals.proposal = state.proposals.get(locals.i); + if (locals.proposal.proposalId == input.proposalId && locals.proposal.active) + { + locals.found = true; + break; + } + } + + if (!locals.found) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalNotFound, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalNotFound; + return; + } + + // Check if already executed + if (locals.proposal.executed) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalAlreadyExecuted, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalAlreadyExecuted; + return; + } + + // Verify that the invocator is the creator (first approver) + if (locals.proposal.approvals.get(0) != qpi.invocator()) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + // Cancel the proposal by marking it as inactive + locals.proposal.active = false; + state.proposals.set(locals.i, locals.proposal); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + + output.status = 0; // Success + } + + // Admin Functions (now deprecated - use multisig proposals) + struct addManager_locals + { + EthBridgeLogger log; + AddressChangeLogger managerLog; + uint64 i; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(addManager) + { + // DEPRECATED: Use createProposal/approveProposal with PROPOSAL_ADD_MANAGER instead + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + struct removeManager_locals + { + EthBridgeLogger log; + AddressChangeLogger managerLog; + uint64 i; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(removeManager) + { + // DEPRECATED: Use createProposal/approveProposal with PROPOSAL_REMOVE_MANAGER instead + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + struct getTotalReceivedTokens_locals + { + EthBridgeLogger log; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getTotalReceivedTokens) + { + output.totalTokens = state.totalReceivedTokens; + } + + struct completeOrder_locals + { + EthBridgeLogger log; + id invocatorAddress; + bit isManagerOperating; + bit orderFound; + BridgeOrder order; + TokensLogger logTokens; + uint64 i; + uint64 netAmount; + uint64 feeOperator; + uint64 feeNetwork; + }; + + // Complete an order and release tokens + PUBLIC_PROCEDURE_WITH_LOCALS(completeOrder) + { + locals.invocatorAddress = qpi.invocator(); + locals.isManagerOperating = false; + CALL(isManager, locals.invocatorAddress, locals.isManagerOperating); + + // Verify that the invocator is a manager + if (!locals.isManagerOperating) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::onlyManagersCanCompleteOrders, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::onlyManagersCanCompleteOrders; // Error: not a manager + return; + } + + // Check if the order exists + locals.orderFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).orderId == input.orderId) + { + locals.order = state.orders.get(locals.i); + locals.orderFound = true; + break; + } + } + + // Order not found + if (!locals.orderFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::orderNotFound, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::orderNotFound; // Error + return; + } + + // Check order status + if (locals.order.status != 0) + { // Check it is not completed or refunded already + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; // Error + return; + } + + // Use full amount without deducting commission (commission was already charged in createOrder) + locals.netAmount = locals.order.amount; + + // Handle order based on transfer direction + if (locals.order.fromQubicToEthereum) + { + // Verify that tokens were received + if (!locals.order.tokensReceived || !locals.order.tokensLocked) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Tokens are already in lockedTokens from transferToContract + // No need to modify lockedTokens here + } + else + { + // Ensure sufficient tokens are locked for the order + if (state.lockedTokens < locals.order.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientLockedTokens, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientLockedTokens; // Error + return; + } + + // Transfer tokens back to the user + if (qpi.transfer(locals.order.qubicDestination, locals.netAmount) < 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::transferFailed, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::transferFailed; // Error + return; + } + + state.lockedTokens -= locals.order.amount; + locals.logTokens = TokensLogger{ + CONTRACT_INDEX, + state.lockedTokens, + state.totalReceivedTokens, + 0 }; + LOG_INFO(locals.logTokens); + } + + // Release reserved fees now that order is completed (fees can now be distributed) + locals.feeOperator = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.feeNetwork = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + + // UNDERFLOW PROTECTION: Only release if enough reserved + if (state._reservedFees >= locals.feeOperator) + { + state._reservedFees -= locals.feeOperator; + } + if (state._reservedFeesQubic >= locals.feeNetwork) + { + state._reservedFeesQubic -= locals.feeNetwork; + } + + // Mark the order as completed + locals.order.status = 1; // Completed + state.orders.set(locals.i, locals.order); // Use the loop index + + output.status = 0; // Success + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + } + + // Refund an order and unlock tokens + struct refundOrder_locals + { + EthBridgeLogger log; + id invocatorAddress; + bit isManagerOperating; + bit orderFound; + BridgeOrder order; + uint64 i; + uint64 feeOperator; + uint64 feeNetwork; + uint64 totalRefund; + uint64 availableFeesOperator; + uint64 availableFeesNetwork; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(refundOrder) + { + locals.invocatorAddress = qpi.invocator(); + locals.isManagerOperating = false; + CALL(isManager, locals.invocatorAddress, locals.isManagerOperating); + + // Check if the order is handled by a manager + if (!locals.isManagerOperating) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::onlyManagersCanRefundOrders, + input.orderId, + 0, // No amount involved + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::onlyManagersCanRefundOrders; // Error + return; + } + + // Retrieve the order + locals.orderFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).orderId == input.orderId) + { + locals.order = state.orders.get(locals.i); + locals.orderFound = true; + break; + } + } + + // Order not found + if (!locals.orderFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::orderNotFound, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::orderNotFound; // Error + return; + } + + // Check order status + if (locals.order.status != 0) + { // Check it is not completed or refunded already + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; // Error + return; + } + + // Handle refund based on transfer direction + if (locals.order.fromQubicToEthereum) + { + // Only refund if tokens were received + if (!locals.order.tokensReceived) + { + // No tokens to return, but refund fees + // Calculate fees to refund (theoretical) + locals.feeOperator = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.feeNetwork = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + + // Track actually refunded amounts + locals.totalRefund = 0; + + // Release reserved fees and deduct from earned (UNDERFLOW PROTECTION) + if (state._reservedFees >= locals.feeOperator && state._earnedFees >= locals.feeOperator) + { + state._reservedFees -= locals.feeOperator; + state._earnedFees -= locals.feeOperator; + locals.totalRefund += locals.feeOperator; + } + if (state._reservedFeesQubic >= locals.feeNetwork && state._earnedFeesQubic >= locals.feeNetwork) + { + state._reservedFeesQubic -= locals.feeNetwork; + state._earnedFeesQubic -= locals.feeNetwork; + locals.totalRefund += locals.feeNetwork; + } + + // Transfer fees back to user + if (qpi.transfer(locals.order.qubicSender, locals.totalRefund) < 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::transferFailed, + input.orderId, + locals.totalRefund, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::transferFailed; + return; + } + + // Mark order as refunded + locals.order.status = 2; + state.orders.set(locals.i, locals.order); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, + input.orderId, + locals.totalRefund, + 0 }; + LOG_INFO(locals.log); + output.status = 0; + return; + } + + // Tokens were received and are in lockedTokens + if (!locals.order.tokensLocked) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Verify sufficient locked tokens + if (state.lockedTokens < locals.order.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientLockedTokens, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientLockedTokens; + return; + } + + // Calculate fees to refund (theoretical) + locals.feeOperator = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.feeNetwork = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + + // Start with order amount + locals.totalRefund = locals.order.amount; + + // Release reserved fees and deduct from earned (UNDERFLOW PROTECTION) + if (state._reservedFees >= locals.feeOperator && state._earnedFees >= locals.feeOperator) + { + state._reservedFees -= locals.feeOperator; + state._earnedFees -= locals.feeOperator; + locals.totalRefund += locals.feeOperator; + } + if (state._reservedFeesQubic >= locals.feeNetwork && state._earnedFeesQubic >= locals.feeNetwork) + { + state._reservedFeesQubic -= locals.feeNetwork; + state._earnedFeesQubic -= locals.feeNetwork; + locals.totalRefund += locals.feeNetwork; + } + + // Return tokens + fees to original sender + if (qpi.transfer(locals.order.qubicSender, locals.totalRefund) < 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::transferFailed, + input.orderId, + locals.totalRefund, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::transferFailed; + return; + } + + // Update locked tokens balance + state.lockedTokens -= locals.order.amount; + } + // Note: For EVM to Qubic orders, tokens were already transferred in completeOrder + // No refund needed on Qubic side (fees were paid on Ethereum side) + + // Mark as refunded + locals.order.status = 2; + state.orders.set(locals.i, locals.order); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; // Success + } + + // Transfer tokens to the contract + struct transferToContract_locals + { + EthBridgeLogger log; + TokensLogger logTokens; + BridgeOrder order; + bit orderFound; + uint64 i; + uint64 depositAmount; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(transferToContract) + { + if (input.amount == 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Find the order + locals.orderFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).orderId == input.orderId) + { + locals.order = state.orders.get(locals.i); + locals.orderFound = true; + break; + } + } + + if (!locals.orderFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::orderNotFound, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::orderNotFound; + return; + } + + // Verify sender is the original order creator + if (locals.order.qubicSender != qpi.invocator()) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + // Verify order state + if (locals.order.status != 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Verify tokens not already received + if (locals.order.tokensReceived) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Verify amount matches order + if (input.amount != locals.order.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Only for Qubic-to-Ethereum orders need to receive tokens + if (locals.order.fromQubicToEthereum) + { + // Tokens must be provided with the invocation (invocationReward) + locals.depositAmount = qpi.invocationReward(); + + // Check if user sent enough tokens + if (locals.depositAmount < input.amount) + { + // Not enough - refund everything and return error + if (locals.depositAmount > 0) + { + qpi.transfer(qpi.invocator(), locals.depositAmount); + } + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Lock only the required amount + state.lockedTokens += input.amount; + state.totalReceivedTokens += input.amount; + + // Refund excess if user sent too much + if (locals.depositAmount > input.amount) + { + qpi.transfer(qpi.invocator(), locals.depositAmount - input.amount); + } + + // Mark tokens as received AND locked + locals.order.tokensReceived = true; + locals.order.tokensLocked = true; + state.orders.set(locals.i, locals.order); + + locals.logTokens = TokensLogger{ + CONTRACT_INDEX, + state.lockedTokens, + state.totalReceivedTokens, + 0 }; + LOG_INFO(locals.logTokens); + } + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; + } + + struct withdrawFees_locals + { + EthBridgeLogger log; + uint64 availableFees; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(withdrawFees) + { + // DEPRECATED: Use createProposal/approveProposal with PROPOSAL_WITHDRAW_FEES instead + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + + PUBLIC_FUNCTION_WITH_LOCALS(getTotalLockedTokens) + { + output.totalLockedTokens = state.lockedTokens; + } + + // Structure for the input of the getOrderByDetails function + struct getOrderByDetails_input + { + Array ethAddress; // Ethereum address + uint64 amount; // Transaction amount + uint8 status; // Order status (0 = created, 1 = completed, 2 = refunded) + }; + + // Structure for the output of the getOrderByDetails function + struct getOrderByDetails_output + { + uint8 status; // Operation status (0 = success, other = error) + uint64 orderId; // ID of the found order + id qubicDestination; // Destination address on Qubic (for EVM to Qubic orders) + }; + + // Function to search for an order by details + struct getOrderByDetails_locals + { + uint64 i; + uint64 j; + bit addressMatch; // Flag to check if addresses match + BridgeOrder order; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getOrderByDetails) + { + // Validate input parameters + if (input.amount == 0) + { + output.status = 2; // Error: invalid amount + output.orderId = 0; + return; + } + + // Iterate through all orders + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + locals.order = state.orders.get(locals.i); + + // Check if the order matches the criteria + if (locals.order.status == 255) // Empty slot + continue; + + // Compare ethAddress arrays element by element + locals.addressMatch = true; + for (locals.j = 0; locals.j < 42; ++locals.j) + { + if (locals.order.ethAddress.get(locals.j) != input.ethAddress.get(locals.j)) + { + locals.addressMatch = false; + break; + } + } + + // Verify exact match + if (locals.addressMatch && + locals.order.amount == input.amount && + locals.order.status == input.status) + { + // Found an exact match + output.status = 0; // Success + output.orderId = locals.order.orderId; + return; + } + } + + // If no matching order was found + output.status = 1; // Not found + output.orderId = 0; + } + + // Add Liquidity structures + struct addLiquidity_input + { + // No input parameters - amount comes from qpi.invocationReward() + }; + + struct addLiquidity_output + { + uint8 status; // Operation status (0 = success, other = error) + uint64 addedAmount; // Amount of tokens added to liquidity + uint64 totalLocked; // Total locked tokens after addition + }; + + struct addLiquidity_locals + { + EthBridgeLogger log; + id invocatorAddress; + bit isManagerOperating; + bit isMultisigAdminResult; + uint64 depositAmount; + }; + + // Add liquidity to the bridge (for managers or multisig admins to provide initial/additional liquidity) + PUBLIC_PROCEDURE_WITH_LOCALS(addLiquidity) + { + locals.invocatorAddress = qpi.invocator(); + locals.isManagerOperating = false; + CALL(isManager, locals.invocatorAddress, locals.isManagerOperating); + + locals.isMultisigAdminResult = false; + CALL(isMultisigAdmin, locals.invocatorAddress, locals.isMultisigAdminResult); + + // Verify that the invocator is a manager or multisig admin + if (!locals.isManagerOperating && !locals.isMultisigAdminResult) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, // No order ID involved + 0, // No amount involved + 0 + }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + // Get the amount of tokens sent with this call + locals.depositAmount = qpi.invocationReward(); + + // Validate that some tokens were sent + if (locals.depositAmount == 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + 0, // No order ID involved + 0, // No amount involved + 0 + }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Add the deposited tokens to the locked tokens pool + state.lockedTokens += locals.depositAmount; + state.totalReceivedTokens += locals.depositAmount; + + // Log the successful liquidity addition + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + 0, // No order ID involved + locals.depositAmount, // Amount added + 0 + }; + LOG_INFO(locals.log); + + // Set output values + output.status = 0; // Success + output.addedAmount = locals.depositAmount; + output.totalLocked = state.lockedTokens; + } + + + PUBLIC_FUNCTION(getAvailableFees) + { + // Available fees exclude those reserved for pending orders + output.availableFees = (state._earnedFees > (state._distributedFees + state._reservedFees)) + ? (state._earnedFees - state._distributedFees - state._reservedFees) + : 0; + output.totalEarnedFees = state._earnedFees; + output.totalDistributedFees = state._distributedFees; + } + + + struct getContractInfo_locals + { + uint64 i; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getContractInfo) + { + output.managers = state.managers; + output.nextOrderId = state.nextOrderId; + output.lockedTokens = state.lockedTokens; + output.totalReceivedTokens = state.totalReceivedTokens; + output.earnedFees = state._earnedFees; + output.tradeFeeBillionths = state._tradeFeeBillionths; + output.sourceChain = state.sourceChain; + + + output.totalOrdersFound = 0; + output.emptySlots = 0; + + for (locals.i = 0; locals.i < 16 && locals.i < state.orders.capacity(); ++locals.i) + { + output.firstOrders.set(locals.i, state.orders.get(locals.i)); + } + + // Count real orders vs empty ones + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).status == 255) + { + output.emptySlots++; + } + else + { + output.totalOrdersFound++; + } + } + + // Multisig info + output.multisigAdmins = state.admins; + output.numberOfAdmins = state.numberOfAdmins; + output.requiredApprovals = state.requiredApprovals; + + // Count active proposals + output.totalProposals = 0; + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (state.proposals.get(locals.i).active && state.proposals.get(locals.i).proposalId > 0) + { + output.totalProposals++; + } + } + } + + // Called at the end of every tick to distribute earned fees + struct END_TICK_locals + { + uint64 feesToDistributeInThisTick; + uint64 amountPerComputor; + uint64 vottunFeesToDistribute; + }; + + END_TICK_WITH_LOCALS() + { + // Calculate available fees for distribution (earned - distributed - reserved for pending orders) + locals.feesToDistributeInThisTick = (state._earnedFeesQubic > (state._distributedFeesQubic + state._reservedFeesQubic)) + ? (state._earnedFeesQubic - state._distributedFeesQubic - state._reservedFeesQubic) + : 0; + + if (locals.feesToDistributeInThisTick > 0) + { + // Distribute fees to computors holding shares of this contract. + // NUMBER_OF_COMPUTORS is a Qubic global constant (typically 676). + locals.amountPerComputor = div(locals.feesToDistributeInThisTick, (uint64)NUMBER_OF_COMPUTORS); + + if (locals.amountPerComputor > 0) + { + if (qpi.distributeDividends(locals.amountPerComputor)) + { + state._distributedFeesQubic += locals.amountPerComputor * NUMBER_OF_COMPUTORS; + } + } + } + + // Distribution of Vottun fees to feeRecipient (excluding reserved fees) + locals.vottunFeesToDistribute = (state._earnedFees > (state._distributedFees + state._reservedFees)) + ? (state._earnedFees - state._distributedFees - state._reservedFees) + : 0; + + if (locals.vottunFeesToDistribute > 0 && state.feeRecipient != NULL_ID) + { + if (qpi.transfer(state.feeRecipient, locals.vottunFeesToDistribute)) + { + state._distributedFees += locals.vottunFeesToDistribute; + } + } + } + + // Register Functions and Procedures + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_FUNCTION(getOrder, 1); + REGISTER_USER_FUNCTION(isManager, 2); + REGISTER_USER_FUNCTION(getTotalReceivedTokens, 3); + REGISTER_USER_FUNCTION(getTotalLockedTokens, 4); + REGISTER_USER_FUNCTION(getOrderByDetails, 5); + REGISTER_USER_FUNCTION(getContractInfo, 6); + REGISTER_USER_FUNCTION(getAvailableFees, 7); + REGISTER_USER_FUNCTION(getProposal, 8); + + REGISTER_USER_PROCEDURE(createOrder, 1); + REGISTER_USER_PROCEDURE(addManager, 2); + REGISTER_USER_PROCEDURE(removeManager, 3); + REGISTER_USER_PROCEDURE(completeOrder, 4); + REGISTER_USER_PROCEDURE(refundOrder, 5); + REGISTER_USER_PROCEDURE(transferToContract, 6); + REGISTER_USER_PROCEDURE(withdrawFees, 7); + REGISTER_USER_PROCEDURE(addLiquidity, 8); + REGISTER_USER_PROCEDURE(createProposal, 9); + REGISTER_USER_PROCEDURE(approveProposal, 10); + REGISTER_USER_PROCEDURE(cancelProposal, 11); + } + + // Initialize the contract with SECURE ADMIN CONFIGURATION + struct INITIALIZE_locals + { + uint64 i; + BridgeOrder emptyOrder; + AdminProposal emptyProposal; + }; + + INITIALIZE_WITH_LOCALS() + { + // Initialize the wallet that receives operator fees (Vottun) + state.feeRecipient = ID(_W, _N, _J, _B, _D, _V, _U, _C, _V, _P, _I, _W, _X, _B, _M, _R, _C, _K, _Z, _E, _C, _Y, _L, _G, _E, _V, _A, _D, _S, _Q, _M, _Y, _S, _R, _F, _Q, _I, _U, _S, _V, _O, _G, _C, _G, _M, _K, _P, _I, _Y, _J, _F, _C, _Z, _F, _B, _A); + + // Initialize the orders array. Good practice to zero first. + locals.emptyOrder = {}; // Sets all fields to 0 (including orderId and status). + locals.emptyOrder.status = 255; // Then set your status for empty. + + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + state.orders.set(locals.i, locals.emptyOrder); + } + + // Initialize the managers array with NULL_ID to mark slots as empty + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + state.managers.set(locals.i, NULL_ID); + } + + // Add the initial manager + state.managers.set(0, ID(_X, _A, _B, _E, _F, _A, _B, _I, _H, _W, _R, _W, _B, _A, _I, _J, _Q, _J, _P, _W, _T, _I, _I, _Q, _B, _U, _C, _B, _H, _B, _V, _W, _Y, _Y, _G, _F, _F, _J, _A, _D, _Q, _B, _K, _W, _F, _B, _O, _R, _R, _V, _X, _W, _S, _C, _V, _B)); + + // Initialize the rest of the state variables + state.nextOrderId = 1; // Start from 1 to avoid ID 0 + state.lockedTokens = 0; + state.totalReceivedTokens = 0; + state.sourceChain = 0; // Arbitrary number. No-EVM chain + + // Initialize fee variables + state._tradeFeeBillionths = 5000000; // 0.5% == 5,000,000 / 1,000,000,000 + state._earnedFees = 0; + state._distributedFees = 0; + + state._earnedFeesQubic = 0; + state._distributedFeesQubic = 0; + + state._reservedFees = 0; + state._reservedFeesQubic = 0; + + // Initialize multisig admins (3 admins, requires 2 approvals) + state.numberOfAdmins = 3; + state.requiredApprovals = 2; // 2 of 3 threshold + + // Initialize admins array (REPLACE WITH ACTUAL ADMIN ADDRESSES) + state.admins.set(0, ID(_X, _A, _B, _E, _F, _A, _B, _I, _H, _W, _R, _W, _B, _A, _I, _J, _Q, _J, _P, _W, _T, _I, _I, _Q, _B, _U, _C, _B, _H, _B, _V, _W, _Y, _Y, _G, _F, _F, _J, _A, _D, _Q, _B, _K, _W, _F, _B, _O, _R, _R, _V, _X, _W, _S, _C, _V, _B)); // Admin 1 + state.admins.set(1, ID(_E, _Q, _M, _B, _B, _V, _Y, _G, _Z, _O, _F, _U, _I, _H, _E, _X, _F, _O, _X, _K, _T, _F, _T, _A, _N, _E, _K, _B, _X, _L, _B, _X, _H, _A, _Y, _D, _F, _F, _M, _R, _E, _E, _M, _R, _Q, _E, _V, _A, _D, _Y, _M, _M, _E, _W, _A, _C)); // Admin 2 (Manager) + state.admins.set(2, ID(_H, _Y, _J, _X, _E, _Z, _S, _E, _C, _W, _S, _K, _O, _D, _J, _A, _L, _R, _C, _K, _S, _L, _K, _V, _Y, _U, _E, _B, _M, _A, _H, _D, _O, _D, _Y, _Z, _U, _J, _I, _I, _Y, _D, _P, _A, _G, _F, _K, _L, _M, _O, _T, _H, _T, _J, _X, _E)); // Admin 3 (User) + + // Initialize remaining admin slots + for (locals.i = 3; locals.i < state.admins.capacity(); ++locals.i) + { + state.admins.set(locals.i, NULL_ID); + } + + // Initialize proposals array properly (like orders array) + state.nextProposalId = 1; + + // Initialize emptyProposal fields explicitly (avoid memset) + locals.emptyProposal.proposalId = 0; + locals.emptyProposal.proposalType = 0; + locals.emptyProposal.targetAddress = NULL_ID; + locals.emptyProposal.amount = 0; + locals.emptyProposal.approvalsCount = 0; + locals.emptyProposal.executed = false; + locals.emptyProposal.active = false; + // Initialize approvals array with NULL_ID + for (locals.i = 0; locals.i < locals.emptyProposal.approvals.capacity(); ++locals.i) + { + locals.emptyProposal.approvals.set(locals.i, NULL_ID); + } + + // Set all proposal slots with the empty proposal + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + state.proposals.set(locals.i, locals.emptyProposal); + } + } +}; diff --git a/src/contracts/math_lib.h b/src/contracts/math_lib.h index 2eaeafd8a..d0209a395 100644 --- a/src/contracts/math_lib.h +++ b/src/contracts/math_lib.h @@ -1,6 +1,7 @@ // Basic math functions (not optimized but with minimal dependencies) #pragma once +#include namespace math_lib { @@ -49,4 +50,120 @@ inline static unsigned char divUp(unsigned char a, unsigned char b) return b ? ((a + b - 1) / b) : 0; } +inline constexpr unsigned long long findNextPowerOf2(unsigned long long num) +{ + if (num == 0) + return 1; + + num--; + num |= num >> 1; + num |= num >> 2; + num |= num >> 4; + num |= num >> 8; + num |= num >> 16; + num |= num >> 32; + num++; + + return num; +} + +////////// +// safety multiplying a and b and then clamp + +inline static long long smul(long long a, long long b) +{ + long long hi, lo; + lo = _mul128(a, b, &hi); + if (hi != (lo >> 63)) + { + return ((a > 0) == (b > 0)) ? INT64_MAX : INT64_MIN; + } + return lo; +} + +inline static unsigned long long smul(unsigned long long a, unsigned long long b) +{ + unsigned long long hi, lo; + lo = _umul128(a, b, &hi); + if (hi != 0) + { + return UINT64_MAX; + } + return lo; +} + +inline static int smul(int a, int b) +{ + long long r = (long long)(a) * (long long)(b); + if (r < INT32_MIN) + { + return INT32_MIN; + } + else if (r > INT32_MAX) + { + return INT32_MAX; + } + else + { + return (int)r; + } +} + +inline static unsigned int smul(unsigned int a, unsigned int b) +{ + unsigned long long r = (unsigned long long)(a) * (unsigned long long)(b); + if (r > UINT32_MAX) + { + return UINT32_MAX; + } + return (unsigned int)r; +} + +////////// +// safety adding a and b and then clamp + +inline static long long sadd(long long a, long long b) +{ + long long sum = a + b; + if (a < 0 && b < 0 && sum > 0) // negative overflow + return INT64_MIN; + if (a > 0 && b > 0 && sum < 0) // positive overflow + return INT64_MAX; + return sum; +} + +inline static unsigned long long sadd(unsigned long long a, unsigned long long b) +{ + if (UINT64_MAX - a < b) + return UINT64_MAX; + return a + b; +} + +inline static int sadd(int a, int b) +{ + long long sum = (long long)(a)+(long long)(b); + if (sum < INT32_MIN) + { + return INT32_MIN; + } + else if (sum > INT32_MAX) + { + return INT32_MAX; + } + else + { + return (int)sum; + } +} + +inline static unsigned int sadd(unsigned int a, unsigned int b) +{ + unsigned long long sum = (unsigned long long)(a)+(unsigned long long)(b); + if (sum > UINT32_MAX) + { + return UINT32_MAX; + } + return (unsigned int)sum; +} + } diff --git a/src/contracts/qRWA.h b/src/contracts/qRWA.h new file mode 100644 index 000000000..0d41020b1 --- /dev/null +++ b/src/contracts/qRWA.h @@ -0,0 +1,2015 @@ +using namespace QPI; + +/***************************************************/ +/******************* CONSTANTS *********************/ +/***************************************************/ + +constexpr uint64 QRWA_MAX_QMINE_HOLDERS = 1048576 * 2 * X_MULTIPLIER; // 2^21 +constexpr uint64 QRWA_MAX_GOV_POLLS = 64; // 8 active polls * 8 epochs = 64 slots +constexpr uint64 QRWA_MAX_ASSET_POLLS = 64; // 8 active polls * 8 epochs = 64 slots +constexpr uint64 QRWA_MAX_ASSETS = 1024; // 2^10 + +constexpr uint64 QRWA_MAX_NEW_GOV_POLLS_PER_EPOCH = 8; +constexpr uint64 QRWA_MAX_NEW_ASSET_POLLS_PER_EPOCH = 8; + +// Dividend percentage constants +constexpr uint64 QRWA_QMINE_HOLDER_PERCENT = 900; // 90.0% +constexpr uint64 QRWA_QRWA_HOLDER_PERCENT = 100; // 10.0% +constexpr uint64 QRWA_PERCENT_DENOMINATOR = 1000; // 100.0% + +// Payout Timing Constants +constexpr uint64 QRWA_PAYOUT_DAY = FRIDAY; // Friday +constexpr uint64 QRWA_PAYOUT_HOUR = 12; // 12:00 PM UTC +constexpr uint64 QRWA_MIN_PAYOUT_INTERVAL_MS = 6 * 86400000LL; // 6 days in milliseconds + +// STATUS CODES for Procedures +constexpr uint64 QRWA_STATUS_SUCCESS = 1; +constexpr uint64 QRWA_STATUS_FAILURE_GENERAL = 0; +constexpr uint64 QRWA_STATUS_FAILURE_INSUFFICIENT_FEE = 2; +constexpr uint64 QRWA_STATUS_FAILURE_INVALID_INPUT = 3; +constexpr uint64 QRWA_STATUS_FAILURE_NOT_AUTHORIZED = 4; +constexpr uint64 QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE = 5; +constexpr uint64 QRWA_STATUS_FAILURE_LIMIT_REACHED = 6; +constexpr uint64 QRWA_STATUS_FAILURE_TRANSFER_FAILED = 7; +constexpr uint64 QRWA_STATUS_FAILURE_NOT_FOUND = 8; +constexpr uint64 QRWA_STATUS_FAILURE_ALREADY_VOTED = 9; +constexpr uint64 QRWA_STATUS_FAILURE_POLL_INACTIVE = 10; +constexpr uint64 QRWA_STATUS_FAILURE_WRONG_STATE = 11; + +constexpr uint64 QRWA_POLL_STATUS_EMPTY = 0; +constexpr uint64 QRWA_POLL_STATUS_ACTIVE = 1; // poll is live, can be voted +constexpr uint64 QRWA_POLL_STATUS_PASSED_EXECUTED = 2; // poll inactive, result is YES +constexpr uint64 QRWA_POLL_STATUS_FAILED_VOTE = 3; // poll inactive, result is NO +constexpr uint64 QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION = 4; // poll inactive, result is YES but failed to release asset + +// QX Fee for releasing management rights +constexpr sint64 QRWA_RELEASE_MANAGEMENT_FEE = 100; + +// LOG TYPES +constexpr uint64 QRWA_LOG_TYPE_DISTRIBUTION = 1; +constexpr uint64 QRWA_LOG_TYPE_GOV_VOTE = 2; +constexpr uint64 QRWA_LOG_TYPE_ASSET_POLL_CREATED = 3; +constexpr uint64 QRWA_LOG_TYPE_ASSET_VOTE = 4; +constexpr uint64 QRWA_LOG_TYPE_ASSET_POLL_EXECUTED = 5; +constexpr uint64 QRWA_LOG_TYPE_TREASURY_DONATION = 6; +constexpr uint64 QRWA_LOG_TYPE_ADMIN_ACTION = 7; +constexpr uint64 QRWA_LOG_TYPE_ERROR = 8; +constexpr uint64 QRWA_LOG_TYPE_INCOMING_REVENUE_A = 9; +constexpr uint64 QRWA_LOG_TYPE_INCOMING_REVENUE_B = 10; + + +/***************************************************/ +/**************** CONTRACT STATE *******************/ +/***************************************************/ + +struct QRWA : public ContractBase +{ + friend class ContractTestingQRWA; + /***************************************************/ + /******************** STRUCTS **********************/ + /***************************************************/ + + struct QRWAAsset + { + id issuer; + uint64 assetName; + + operator Asset() const + { + return { issuer, assetName }; + } + + bool operator==(const QRWAAsset other) const + { + return issuer == other.issuer && assetName == other.assetName; + } + + bool operator!=(const QRWAAsset other) const + { + return issuer != other.issuer || assetName != other.assetName; + } + + inline void setFrom(const Asset& asset) + { + issuer = asset.issuer; + assetName = asset.assetName; + } + }; + + // votable governance parameters for the contract. + struct QRWAGovParams + { + // Addresses + id mAdminAddress; // Only the admin can create release polls + // Addresses to receive the MINING FEEs + id electricityAddress; + id maintenanceAddress; + id reinvestmentAddress; + id qmineDevAddress; // Address to receive rewards for moved QMINE during epoch + + // MINING FEE Percentages + uint64 electricityPercent; + uint64 maintenancePercent; + uint64 reinvestmentPercent; + }; + + // Represents a governance poll in a rotating buffer + struct QRWAGovProposal + { + uint64 proposalId; // The unique, increasing ID + uint64 status; // 0=Empty, 1=Active, 2=Passed, 3=Failed + uint64 score; // Final score, count at END_EPOCH + QRWAGovParams params; // The actual proposal data + }; + + // Represents a poll to release assets from the treasury or dividend pool. + struct AssetReleaseProposal + { + uint64 proposalId; + id proposalName; + Asset asset; + uint64 amount; + id destination; + uint64 status; // 0=Empty, 1=Active, 2=Passed_Executed, 3=Failed, 4=Passed_Failed_Execution + uint64 votesYes; // Final score, count at END_EPOCH + uint64 votesNo; // Final score, count at END_EPOCH + }; + + // Logger for general contract events. + struct QRWALogger + { + uint64 contractId; + uint64 logType; + id primaryId; // voter, asset issuer, proposal creator + uint64 valueA; + uint64 valueB; + sint8 _terminator; + }; + +protected: + Asset mQmineAsset; + + // QMINE Shareholder Tracking + HashMap mBeginEpochBalances; + HashMap mEndEpochBalances; + uint64 mTotalQmineBeginEpoch; // Total QMINE shares at the start of the current epoch + + // PAYOUT SNAPSHOTS (for distribution) + // These hold the data from the last epoch, saved at END_EPOCH + HashMap mPayoutBeginBalances; + HashMap mPayoutEndBalances; + uint64 mPayoutTotalQmineBegin; // Total QMINE shares from the last epoch's beginning + + // Votable Parameters + QRWAGovParams mCurrentGovParams; // The live, active parameters + + // Voting state for governance parameters (voted by QMINE holders) + Array mGovPolls; + HashMap mShareholderVoteMap; // Maps QMINE holder -> Gov Poll slot index + uint64 mCurrentGovProposalId; + uint64 mNewGovPollsThisEpoch; + + // Asset Release Polls + Array mAssetPolls; + HashMap mAssetProposalVoterMap; // (Voter -> bitfield of poll slot indices) + HashMap mAssetVoteOptions; // (Voter -> bitfield of options (0=No, 1=Yes)) + uint64 mCurrentAssetProposalId; // Counter for creating new proposal ID + uint64 mNewAssetPollsThisEpoch; + + // Treasury & Asset Release + uint64 mTreasuryBalance; // QMINE token balance holds by SC + HashMap mGeneralAssetBalances; // Balances for other assets (e.g., SC shares) + + // Payouts and Dividend Accounting + DateAndTime mLastPayoutTime; // Tracks the last payout time + + // Dividend Pools + uint64 mRevenuePoolA; // Mined funds from Qubic farm (from SCs) + uint64 mRevenuePoolB; // Other dividend funds (from user wallets) + + // Processed dividend pools awaiting distribution + uint64 mQmineDividendPool; // QUs for QMINE holders + uint64 mQRWADividendPool; // QUs for qRWA shareholders + + // Total distributed tracking + uint64 mTotalQmineDistributed; + uint64 mTotalQRWADistributed; + +public: + /***************************************************/ + /**************** PUBLIC PROCEDURES ****************/ + /***************************************************/ + + // Treasury + struct DonateToTreasury_input + { + uint64 amount; + }; + struct DonateToTreasury_output + { + uint64 status; + }; + struct DonateToTreasury_locals + { + sint64 transferResult; + sint64 balance; + QRWALogger logger; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(DonateToTreasury) + { + // NOTE: This procedure transfers QMINE from the invoker's *managed* balance (managed by this SC) + // to the SC's internal treasury. + // A one-time setup by the donor is required: + // 1. Call QX::TransferShareManagementRights to give this SC management rights over the QMINE. + // 2. Call this DonateToTreasury procedure to transfer ownership to the SC. + + // This procedure has no fee, refund any invocation reward + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_TREASURY_DONATION; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.amount; + + if (state.mQmineAsset.issuer == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_WRONG_STATE; // QMINE asset not set + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + if (input.amount == 0) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Check if user has granted management rights to this SC + locals.balance = qpi.numberOfShares(state.mQmineAsset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX }); + + if (locals.balance < static_cast(input.amount)) + { + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; // Not enough managed shares + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Transfer QMINE from invoker (managed by SELF) to SELF (owned by SELF) + locals.transferResult = qpi.transferShareOwnershipAndPossession( + state.mQmineAsset.assetName, + state.mQmineAsset.issuer, + qpi.invocator(), // current owner + qpi.invocator(), // current possessor + input.amount, + SELF // new owner and possessor + ); + + if (locals.transferResult >= 0) // Transfer successful + { + state.mTreasuryBalance = sadd(state.mTreasuryBalance, input.amount); + output.status = QRWA_STATUS_SUCCESS; + } + else + { + output.status = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + + // Governance: Param Voting + struct VoteGovParams_input + { + QRWAGovParams proposal; + }; + struct VoteGovParams_output + { + uint64 status; + }; + struct VoteGovParams_locals + { + uint64 currentBalance; + uint64 i; + uint64 foundProposal; + uint64 proposalIndex; + QRWALogger logger; + QRWAGovProposal poll; + sint64 rawBalance; + QRWAGovParams existing; + uint64 status; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(VoteGovParams) + { + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_GOV_VOTE; + locals.logger.primaryId = qpi.invocator(); + + // Get voter's current QMINE balance + locals.rawBalance = qpi.numberOfShares(state.mQmineAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), AssetPossessionSelect::byPossessor(qpi.invocator())); + locals.currentBalance = (locals.rawBalance > 0) ? static_cast(locals.rawBalance) : 0; + + if (locals.currentBalance <= 0) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Validate proposal percentages + if (sadd(sadd(input.proposal.electricityPercent, input.proposal.maintenancePercent), input.proposal.reinvestmentPercent) > QRWA_PERCENT_DENOMINATOR) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + if (input.proposal.mAdminAddress == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Now process the new/updated vote + locals.foundProposal = 0; + locals.proposalIndex = NULL_INDEX; + + // Check if the current proposal matches any existing unique proposal + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + locals.existing = state.mGovPolls.get(locals.i).params; + locals.status = state.mGovPolls.get(locals.i).status; + + if (locals.status == QRWA_POLL_STATUS_ACTIVE && + locals.existing.electricityAddress == input.proposal.electricityAddress && + locals.existing.maintenanceAddress == input.proposal.maintenanceAddress && + locals.existing.reinvestmentAddress == input.proposal.reinvestmentAddress && + locals.existing.qmineDevAddress == input.proposal.qmineDevAddress && + locals.existing.mAdminAddress == input.proposal.mAdminAddress && + locals.existing.electricityPercent == input.proposal.electricityPercent && + locals.existing.maintenancePercent == input.proposal.maintenancePercent && + locals.existing.reinvestmentPercent == input.proposal.reinvestmentPercent) + { + locals.foundProposal = 1; + locals.proposalIndex = locals.i; // This is the proposal we are voting for + break; + } + } + + // If proposal not found, create it in a new slot + if (locals.foundProposal == 0) + { + if (state.mNewGovPollsThisEpoch >= QRWA_MAX_NEW_GOV_POLLS_PER_EPOCH) + { + output.status = QRWA_STATUS_FAILURE_LIMIT_REACHED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.proposalIndex = mod(state.mCurrentGovProposalId, QRWA_MAX_GOV_POLLS); + + // Clear old data at this slot + locals.poll = state.mGovPolls.get(locals.proposalIndex); + locals.poll.proposalId = state.mCurrentGovProposalId; + locals.poll.params = input.proposal; + locals.poll.score = 0; // Will be count at END_EPOCH + locals.poll.status = QRWA_POLL_STATUS_ACTIVE; + + state.mGovPolls.set(locals.proposalIndex, locals.poll); + + state.mCurrentGovProposalId++; + state.mNewGovPollsThisEpoch++; + } + + state.mShareholderVoteMap.set(qpi.invocator(), locals.proposalIndex); + output.status = QRWA_STATUS_SUCCESS; + + locals.logger.valueA = locals.proposalIndex; // Log the index voted for/added + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + + // Governance: Asset Release + struct CreateAssetReleasePoll_input + { + id proposalName; + Asset asset; + uint64 amount; + id destination; + }; + struct CreateAssetReleasePoll_output + { + uint64 status; + uint64 proposalId; + }; + struct CreateAssetReleasePoll_locals + { + uint64 newPollIndex; + AssetReleaseProposal newPoll; + QRWALogger logger; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(CreateAssetReleasePoll) + { + output.status = QRWA_STATUS_FAILURE_GENERAL; + output.proposalId = -1; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ASSET_POLL_CREATED; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = 0; + + if (qpi.invocator() != state.mCurrentGovParams.mAdminAddress) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Check poll limit + if (state.mNewAssetPollsThisEpoch >= QRWA_MAX_NEW_ASSET_POLLS_PER_EPOCH) + { + output.status = QRWA_STATUS_FAILURE_LIMIT_REACHED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + if (input.amount == 0 || input.destination == NULL_ID || input.asset.issuer == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + locals.newPollIndex = mod(state.mCurrentAssetProposalId, QRWA_MAX_ASSET_POLLS); + + // Create and store the new poll, overwriting the oldest one + locals.newPoll.proposalId = state.mCurrentAssetProposalId; + locals.newPoll.proposalName = input.proposalName; + locals.newPoll.asset = input.asset; + locals.newPoll.amount = input.amount; + locals.newPoll.destination = input.destination; + locals.newPoll.status = QRWA_POLL_STATUS_ACTIVE; + locals.newPoll.votesYes = 0; + locals.newPoll.votesNo = 0; + + state.mAssetPolls.set(locals.newPollIndex, locals.newPoll); + + output.proposalId = state.mCurrentAssetProposalId; + output.status = QRWA_STATUS_SUCCESS; + state.mCurrentAssetProposalId++; // Increment for the next proposal + state.mNewAssetPollsThisEpoch++; + + locals.logger.valueA = output.proposalId; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + + struct VoteAssetRelease_input + { + uint64 proposalId; + uint64 option; /* 0=No, 1=Yes */ + }; + struct VoteAssetRelease_output + { + uint64 status; + }; + struct VoteAssetRelease_locals + { + uint64 currentBalance; + AssetReleaseProposal poll; + uint64 pollIndex; + QRWALogger logger; + uint64 foundPoll; + bit_64 voterBitfield; + bit_64 voterOptions; + sint64 rawBalance; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(VoteAssetRelease) + { + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ASSET_VOTE; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.proposalId; + locals.logger.valueB = input.option; + + if (input.option > 1) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; // Overwrite valueB with status + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Get voter's current QMINE balance + locals.rawBalance = qpi.numberOfShares(state.mQmineAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), AssetPossessionSelect::byPossessor(qpi.invocator())); + locals.currentBalance = (locals.rawBalance > 0) ? static_cast(locals.rawBalance) : 0; + + + if (locals.currentBalance <= 0) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; // Not a QMINE holder or balance is zero + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Find the poll + locals.pollIndex = mod(input.proposalId, QRWA_MAX_ASSET_POLLS); + locals.poll = state.mAssetPolls.get(locals.pollIndex); + + if (locals.poll.proposalId != input.proposalId) + { + locals.foundPoll = 0; + } + else { + locals.foundPoll = 1; + } + + if (locals.foundPoll == 0) + { + output.status = QRWA_STATUS_FAILURE_NOT_FOUND; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + if (locals.poll.status != QRWA_POLL_STATUS_ACTIVE) // Check if poll is active + { + output.status = QRWA_STATUS_FAILURE_POLL_INACTIVE; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Now process the new vote + state.mAssetProposalVoterMap.get(qpi.invocator(), locals.voterBitfield); // Get or default (all 0s) + state.mAssetVoteOptions.get(qpi.invocator(), locals.voterOptions); + + // Record vote + locals.voterBitfield.set(locals.pollIndex, 1); + locals.voterOptions.set(locals.pollIndex, (input.option == 1) ? 1 : 0); + + // Update voter's maps + state.mAssetProposalVoterMap.set(qpi.invocator(), locals.voterBitfield); + state.mAssetVoteOptions.set(qpi.invocator(), locals.voterOptions); + + output.status = QRWA_STATUS_SUCCESS; + locals.logger.valueB = output.status; // Log final status + LOG_INFO(locals.logger); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + + // deposit general assets + struct DepositGeneralAsset_input + { + Asset asset; + uint64 amount; + }; + struct DepositGeneralAsset_output + { + uint64 status; + }; + struct DepositGeneralAsset_locals + { + sint64 transferResult; + sint64 balance; + uint64 currentAssetBalance; + QRWALogger logger; + QRWAAsset wrapper; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(DepositGeneralAsset) + { + // This procedure has no fee + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.status = QRWA_STATUS_FAILURE_GENERAL; + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ADMIN_ACTION; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.asset.assetName; + + if (qpi.invocator() != state.mCurrentGovParams.mAdminAddress) + { + output.status = QRWA_STATUS_FAILURE_NOT_AUTHORIZED; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + if (input.amount == 0 || input.asset.issuer == NULL_ID) + { + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Check if admin has granted management rights to this SC + locals.balance = qpi.numberOfShares(input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX }); + + if (locals.balance < static_cast(input.amount)) + { + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; // Not enough managed shares + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Transfer asset from admin (managed by SELF) to SELF (owned by SELF) + locals.transferResult = qpi.transferShareOwnershipAndPossession( + input.asset.assetName, + input.asset.issuer, + qpi.invocator(), // current owner + qpi.invocator(), // current possessor + input.amount, + SELF // new owner and possessor + ); + + if (locals.transferResult >= 0) // Transfer successful + { + locals.wrapper.setFrom(input.asset); + state.mGeneralAssetBalances.get(locals.wrapper, locals.currentAssetBalance); // 0 if not exist + locals.currentAssetBalance = sadd(locals.currentAssetBalance, input.amount); + state.mGeneralAssetBalances.set(locals.wrapper, locals.currentAssetBalance); + output.status = QRWA_STATUS_SUCCESS; + } + else + { + output.status = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + + struct RevokeAssetManagementRights_input + { + Asset asset; + sint64 numberOfShares; + }; + struct RevokeAssetManagementRights_output + { + sint64 transferredNumberOfShares; + uint64 status; + }; + struct RevokeAssetManagementRights_locals + { + QRWALogger logger; + sint64 managedBalance; + sint64 result; + }; + PUBLIC_PROCEDURE_WITH_LOCALS(RevokeAssetManagementRights) + { + // This procedure allows a user to revoke asset management rights from qRWA + // and transfer them back to QX, which is the default manager for trading + // Ref: MSVAULT + + output.status = QRWA_STATUS_FAILURE_GENERAL; + output.transferredNumberOfShares = 0; + + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.primaryId = qpi.invocator(); + locals.logger.valueA = input.asset.assetName; + locals.logger.valueB = input.numberOfShares; + + if (qpi.invocationReward() < (sint64)QRWA_RELEASE_MANAGEMENT_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_FEE; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)QRWA_RELEASE_MANAGEMENT_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)QRWA_RELEASE_MANAGEMENT_FEE); + } + + // must transfer a positive number of shares. + if (input.numberOfShares <= 0) + { + // Refund the fee if params are invalid + qpi.transfer(qpi.invocator(), (sint64)QRWA_RELEASE_MANAGEMENT_FEE); + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_INVALID_INPUT; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + return; + } + + // Check if qRWA actually manages the specified number of shares for the caller. + locals.managedBalance = qpi.numberOfShares( + input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX } + ); + + if (locals.managedBalance < input.numberOfShares) + { + // The user is trying to revoke more shares than are managed by qRWA. + qpi.transfer(qpi.invocator(), (sint64)QRWA_RELEASE_MANAGEMENT_FEE); + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + else + { + // The balance check passed. Proceed to release the management rights to QX. + locals.result = qpi.releaseShares( + input.asset, + qpi.invocator(), // owner + qpi.invocator(), // possessor + input.numberOfShares, + QX_CONTRACT_INDEX, // destination ownership managing contract + QX_CONTRACT_INDEX, // destination possession managing contract + QRWA_RELEASE_MANAGEMENT_FEE // offered fee to QX + ); + + if (locals.result < 0) + { + // Transfer failed + output.transferredNumberOfShares = 0; + output.status = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + } + else + { + // Success, the fee was spent. + output.transferredNumberOfShares = input.numberOfShares; + output.status = QRWA_STATUS_SUCCESS; + locals.logger.logType = QRWA_LOG_TYPE_ADMIN_ACTION; // Or a more specific type + + // Since the invocation reward (100 QU) was added to mRevenuePoolB + // via POST_INCOMING_TRANSFER, but we just spent it in releaseShares, + // we must remove it from the pool to keep the accountant in sync + // with the actual balance. + if (state.mRevenuePoolB >= QRWA_RELEASE_MANAGEMENT_FEE) + { + state.mRevenuePoolB -= QRWA_RELEASE_MANAGEMENT_FEE; + } + } + locals.logger.valueB = output.status; + LOG_INFO(locals.logger); + } + } + + /***************************************************/ + /***************** PUBLIC FUNCTIONS ****************/ + /***************************************************/ + + // Governance: Param Voting + struct GetGovParams_input {}; + struct GetGovParams_output + { + QRWAGovParams params; + }; + PUBLIC_FUNCTION(GetGovParams) + { + output.params = state.mCurrentGovParams; + } + + struct GetGovPoll_input + { + uint64 proposalId; + }; + struct GetGovPoll_output + { + QRWAGovProposal proposal; + uint64 status; // 0=NotFound, 1=Found + }; + struct GetGovPoll_locals + { + uint64 pollIndex; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetGovPoll) + { + output.status = QRWA_STATUS_FAILURE_NOT_FOUND; + + locals.pollIndex = mod(input.proposalId, QRWA_MAX_GOV_POLLS); + output.proposal = state.mGovPolls.get(locals.pollIndex); + + if (output.proposal.proposalId == input.proposalId) + { + output.status = QRWA_STATUS_SUCCESS; + } + else + { + // Clear output if not the poll we're looking for + setMemory(output.proposal, 0); + } + } + + // Governance: Asset Release + struct GetAssetReleasePoll_input + { + uint64 proposalId; + }; + struct GetAssetReleasePoll_output + { + AssetReleaseProposal proposal; + uint64 status; // 0=NotFound, 1=Found + }; + struct GetAssetReleasePoll_locals + { + uint64 pollIndex; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetAssetReleasePoll) + { + output.status = QRWA_STATUS_FAILURE_NOT_FOUND; + + locals.pollIndex = mod(input.proposalId, QRWA_MAX_ASSET_POLLS); + output.proposal = state.mAssetPolls.get(locals.pollIndex); + + if (output.proposal.proposalId == input.proposalId) + { + output.status = QRWA_STATUS_SUCCESS; + } + else + { + // Clear output if not the poll we're looking for + setMemory(output.proposal, 0); + } + } + + // Balances & Info + struct GetTreasuryBalance_input {}; + struct GetTreasuryBalance_output + { + uint64 balance; + }; + PUBLIC_FUNCTION(GetTreasuryBalance) + { + output.balance = state.mTreasuryBalance; + } + + struct GetDividendBalances_input {}; + struct GetDividendBalances_output + { + uint64 revenuePoolA; + uint64 revenuePoolB; + uint64 qmineDividendPool; + uint64 qrwaDividendPool; + }; + PUBLIC_FUNCTION(GetDividendBalances) + { + output.revenuePoolA = state.mRevenuePoolA; + output.revenuePoolB = state.mRevenuePoolB; + output.qmineDividendPool = state.mQmineDividendPool; + output.qrwaDividendPool = state.mQRWADividendPool; + } + + struct GetTotalDistributed_input {}; + struct GetTotalDistributed_output + { + uint64 totalQmineDistributed; + uint64 totalQRWADistributed; + }; + PUBLIC_FUNCTION(GetTotalDistributed) + { + output.totalQmineDistributed = state.mTotalQmineDistributed; + output.totalQRWADistributed = state.mTotalQRWADistributed; + } + + struct GetActiveAssetReleasePollIds_input {}; + + struct GetActiveAssetReleasePollIds_output + { + uint64 count; + Array ids; + }; + + struct GetActiveAssetReleasePollIds_locals + { + uint64 i; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAssetReleasePollIds) + { + output.count = 0; + for (locals.i = 0; locals.i < QRWA_MAX_ASSET_POLLS; locals.i++) + { + if (state.mAssetPolls.get(locals.i).status == QRWA_POLL_STATUS_ACTIVE) + { + output.ids.set(output.count, state.mAssetPolls.get(locals.i).proposalId); + output.count++; + } + } + } + + struct GetActiveGovPollIds_input {}; + struct GetActiveGovPollIds_output + { + uint64 count; + Array ids; + }; + struct GetActiveGovPollIds_locals + { + uint64 i; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetActiveGovPollIds) + { + output.count = 0; + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + if (state.mGovPolls.get(locals.i).status == QRWA_POLL_STATUS_ACTIVE) + { + output.ids.set(output.count, state.mGovPolls.get(locals.i).proposalId); + output.count++; + } + } + } + + struct GetGeneralAssetBalance_input + { + Asset asset; + }; + struct GetGeneralAssetBalance_output + { + uint64 balance; + uint64 status; + }; + struct GetGeneralAssetBalance_locals + { + uint64 balance; + QRWAAsset wrapper; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetGeneralAssetBalance) { + locals.balance = 0; + locals.wrapper.setFrom(input.asset); + if (state.mGeneralAssetBalances.get(locals.wrapper, locals.balance)) { + output.balance = locals.balance; + output.status = 1; + } + else { + output.balance = 0; + output.status = 0; + } + } + + struct GetGeneralAssets_input {}; + struct GetGeneralAssets_output + { + uint64 count; + Array assets; + Array balances; + }; + struct GetGeneralAssets_locals + { + sint64 iterIndex; + QRWAAsset currentAsset; + uint64 currentBalance; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetGeneralAssets) + { + output.count = 0; + locals.iterIndex = NULL_INDEX; + + while (true) + { + locals.iterIndex = state.mGeneralAssetBalances.nextElementIndex(locals.iterIndex); + + if (locals.iterIndex == NULL_INDEX) + { + break; + } + + locals.currentAsset = state.mGeneralAssetBalances.key(locals.iterIndex); + locals.currentBalance = state.mGeneralAssetBalances.value(locals.iterIndex); + + // Only return "active" assets (balance > 0) + if (locals.currentBalance > 0) + { + output.assets.set(output.count, locals.currentAsset); + output.balances.set(output.count, locals.currentBalance); + output.count++; + + if (output.count >= QRWA_MAX_ASSETS) + { + break; + } + } + } + } + + /***************************************************/ + /***************** SYSTEM PROCEDURES ***************/ + /***************************************************/ + + INITIALIZE() + { + // QMINE Asset Constant + // Issuer: QMINEQQXYBEGBHNSUPOUYDIQKZPCBPQIIHUUZMCPLBPCCAIARVZBTYKGFCWM + // Name: 297666170193 ("QMINE") + state.mQmineAsset.assetName = 297666170193ULL; + state.mQmineAsset.issuer = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); + state.mTreasuryBalance = 0; + state.mCurrentAssetProposalId = 0; + setMemory(state.mLastPayoutTime, 0); + + // Initialize default governance parameters + state.mCurrentGovParams.mAdminAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Admin set to QMINE Issuer by default, subject to change via Gov Voting + state.mCurrentGovParams.electricityAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Electricity address set to QMINE Issuer by default, subject to change via Gov Voting + state.mCurrentGovParams.maintenanceAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Maintenance address set to QMINE Issuer by default, subject to change via Gov Voting + state.mCurrentGovParams.reinvestmentAddress = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G + ); // Reinvestment address set to QMINE Issuer by default, subject to change via Gov Voting + + // QMINE DEV's Address for receiving rewards from moved QMINE tokens + // ZOXXIDCZIMGCECCFAXDDCMBBXCDAQJIHGOOATAFPSBFIOFOYECFKUFPBEMWC + state.mCurrentGovParams.qmineDevAddress = ID( + _Z, _O, _X, _X, _I, _D, _C, _Z, _I, _M, _G, _C, _E, _C, _C, _F, + _A, _X, _D, _D, _C, _M, _B, _B, _X, _C, _D, _A, _Q, _J, _I, _H, + _G, _O, _O, _A, _T, _A, _F, _P, _S, _B, _F, _I, _O, _F, _O, _Y, + _E, _C, _F, _K, _U, _F, _P, _B + ); // Default QMINE_DEV address + state.mCurrentGovParams.electricityPercent = 350; + state.mCurrentGovParams.maintenancePercent = 50; + state.mCurrentGovParams.reinvestmentPercent = 100; + + state.mCurrentGovProposalId = 0; + state.mCurrentAssetProposalId = 0; + state.mNewGovPollsThisEpoch = 0; + state.mNewAssetPollsThisEpoch = 0; + + // Initialize revenue pools + state.mRevenuePoolA = 0; + state.mRevenuePoolB = 0; + state.mQmineDividendPool = 0; + state.mQRWADividendPool = 0; + + // Initialize total distributed + state.mTotalQmineDistributed = 0; + state.mTotalQRWADistributed = 0; + + // Initialize maps/arrays + state.mBeginEpochBalances.reset(); + state.mEndEpochBalances.reset(); + state.mPayoutBeginBalances.reset(); + state.mPayoutEndBalances.reset(); + state.mGeneralAssetBalances.reset(); + state.mShareholderVoteMap.reset(); + state.mAssetProposalVoterMap.reset(); + state.mAssetVoteOptions.reset(); + + setMemory(state.mAssetPolls, 0); + setMemory(state.mGovPolls, 0); + } + + struct BEGIN_EPOCH_locals + { + AssetPossessionIterator iter; + uint64 balance; + QRWALogger logger; + id holder; + uint64 existingBalance; + }; + BEGIN_EPOCH_WITH_LOCALS() + { + // Reset new poll counters + state.mNewGovPollsThisEpoch = 0; + state.mNewAssetPollsThisEpoch = 0; + + state.mEndEpochBalances.reset(); + + // Take snapshot of begin balances for QMINE holders + state.mBeginEpochBalances.reset(); + state.mTotalQmineBeginEpoch = 0; + + if (state.mQmineAsset.issuer != NULL_ID) + { + for (locals.iter.begin(state.mQmineAsset); !locals.iter.reachedEnd(); locals.iter.next()) + { + // Exclude SELF (Treasury) from dividend snapshot + if (locals.iter.possessor() == SELF) + { + continue; + } + locals.balance = locals.iter.numberOfPossessedShares(); + locals.holder = locals.iter.possessor(); + + if (locals.balance > 0) + { + // Check if holder already exists in the map (e.g. from a different manager) + // If so, add to existing balance. + locals.existingBalance = 0; + state.mBeginEpochBalances.get(locals.holder, locals.existingBalance); + + locals.balance = sadd(locals.existingBalance, locals.balance); + + if (state.mBeginEpochBalances.set(locals.holder, locals.balance) != NULL_INDEX) + { + state.mTotalQmineBeginEpoch = sadd(state.mTotalQmineBeginEpoch, (uint64)locals.iter.numberOfPossessedShares()); + } + else + { + // Log error - Max holders reached for snapshot + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = 11; // Error code: Begin Epoch Snapshot full + locals.logger.valueB = state.mBeginEpochBalances.population(); + LOG_INFO(locals.logger); + } + } + } + } + } + + struct END_EPOCH_locals + { + AssetPossessionIterator iter; + uint64 balance; + + sint64 iterIndex; + id iterVoter; + uint64 beginBalance; + uint64 endBalance; + uint64 votingPower; + uint64 proposalIndex; + uint64 currentScore; + bit_64 voterBitfield; + bit_64 voterOptions; + uint64 i_asset; + + // Gov Params Voting + uint64 i; + uint64 topScore; + uint64 topProposalIndex; + uint64 totalQminePower; + Array govPollScores; + uint64 govPassed; + uint64 quorumThreshold; + + // Asset Release Voting + uint64 pollIndex; + AssetReleaseProposal poll; + uint64 yesVotes; + uint64 noVotes; + uint64 totalVotes; + uint64 transferSuccess; + sint64 transferResult; + uint64 currentAssetBalance; + Array assetPollVotesYes; + Array assetPollVotesNo; + uint64 assetPassed; + + sint64 releaseFeeResult; // For releaseShares fee + + uint64 ownershipTransferred; + uint64 managementTransferred; + uint64 feePaid; + uint64 sufficientFunds; + + QRWALogger logger; + uint64 epoch; + + sint64 copyIndex; + id copyHolder; + uint64 copyBalance; + + QRWAAsset wrapper; + + QRWAGovProposal govPoll; + + id holder; + uint64 existingBalance; + }; + END_EPOCH_WITH_LOCALS() + { + locals.epoch = qpi.epoch(); // Get current epoch for history records + + // Take snapshot of end balances for QMINE holders + if (state.mQmineAsset.issuer != NULL_ID) + { + for (locals.iter.begin(state.mQmineAsset); !locals.iter.reachedEnd(); locals.iter.next()) + { + // Exclude SELF (Treasury) from dividend snapshot + if (locals.iter.possessor() == SELF) + { + continue; + } + locals.balance = locals.iter.numberOfPossessedShares(); + locals.holder = locals.iter.possessor(); + + if (locals.balance > 0) + { + // Check if holder already exists (multiple SC management) + locals.existingBalance = 0; + state.mEndEpochBalances.get(locals.holder, locals.existingBalance); + + locals.balance = sadd(locals.existingBalance, locals.balance); + + if (state.mEndEpochBalances.set(locals.holder, locals.balance) == NULL_INDEX) + { + // Log error - Max holders reached for snapshot + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = 12; // Error code: End Epoch Snapshot full + locals.logger.valueB = state.mEndEpochBalances.population(); + LOG_INFO(locals.logger); + } + } + } + } + + // Process Governance Parameter Voting (voted by QMINE holders) + // Recount all votes from scratch using snapshots + + locals.totalQminePower = state.mTotalQmineBeginEpoch; + locals.govPollScores.setAll(0); // Reset scores to zero. + + locals.iterIndex = NULL_INDEX; // Iterate all voters + while (true) + { + locals.iterIndex = state.mShareholderVoteMap.nextElementIndex(locals.iterIndex); + if (locals.iterIndex == NULL_INDEX) + { + break; + } + + locals.iterVoter = state.mShareholderVoteMap.key(locals.iterIndex); + + // Get true voting power from snapshots + locals.beginBalance = 0; + locals.endBalance = 0; + state.mBeginEpochBalances.get(locals.iterVoter, locals.beginBalance); + state.mEndEpochBalances.get(locals.iterVoter, locals.endBalance); + + locals.votingPower = (locals.beginBalance < locals.endBalance) ? locals.beginBalance : locals.endBalance; // min(begin, end) + + if (locals.votingPower > 0) // Apply voting power + { + state.mShareholderVoteMap.get(locals.iterVoter, locals.proposalIndex); + if (locals.proposalIndex < QRWA_MAX_GOV_POLLS) + { + if (state.mGovPolls.get(locals.proposalIndex).status == QRWA_POLL_STATUS_ACTIVE) + { + locals.currentScore = locals.govPollScores.get(locals.proposalIndex); + locals.govPollScores.set(locals.proposalIndex, sadd(locals.currentScore, locals.votingPower)); + } + } + } + } + + // Find the winning proposal (max votes) + locals.topScore = 0; + locals.topProposalIndex = NULL_INDEX; + + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + if (state.mGovPolls.get(locals.i).status == QRWA_POLL_STATUS_ACTIVE) + { + locals.currentScore = locals.govPollScores.get(locals.i); + if (locals.currentScore > locals.topScore) + { + locals.topScore = locals.currentScore; + locals.topProposalIndex = locals.i; + } + } + } + + // Calculate 2/3 quorum threshold + locals.quorumThreshold = 0; + if (locals.totalQminePower > 0) + { + locals.quorumThreshold = div(sadd(smul(locals.totalQminePower, 2ULL), 2ULL), 3ULL); + } + + // Finalize Gov Vote (check against 2/3 quorum) + locals.govPassed = 0; + if (locals.topScore >= locals.quorumThreshold && locals.topProposalIndex != NULL_INDEX) + { + // Proposal passes + locals.govPassed = 1; + state.mCurrentGovParams = state.mGovPolls.get(locals.topProposalIndex).params; + + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_GOV_VOTE; + locals.logger.primaryId = NULL_ID; // System event + locals.logger.valueA = state.mGovPolls.get(locals.topProposalIndex).proposalId; + locals.logger.valueB = QRWA_STATUS_SUCCESS; // Indicate params updated + LOG_INFO(locals.logger); + } + + // Update status for all active gov polls (for history) + for (locals.i = 0; locals.i < QRWA_MAX_GOV_POLLS; locals.i++) + { + locals.govPoll = state.mGovPolls.get(locals.i); + if (locals.govPoll.status == QRWA_POLL_STATUS_ACTIVE) + { + locals.govPoll.score = locals.govPollScores.get(locals.i); + if (locals.govPassed == 1 && locals.i == locals.topProposalIndex) + { + locals.govPoll.status = QRWA_POLL_STATUS_PASSED_EXECUTED; + } + else + { + locals.govPoll.status = QRWA_POLL_STATUS_FAILED_VOTE; + } + state.mGovPolls.set(locals.i, locals.govPoll); + } + } + + // Reset governance voter map for the next epoch + state.mShareholderVoteMap.reset(); + + // --- Process Asset Release Voting --- + locals.assetPollVotesYes.setAll(0); + locals.assetPollVotesNo.setAll(0); + + locals.iterIndex = NULL_INDEX; + while (true) + { + locals.iterIndex = state.mAssetProposalVoterMap.nextElementIndex(locals.iterIndex); + if (locals.iterIndex == NULL_INDEX) + { + break; + } + + locals.iterVoter = state.mAssetProposalVoterMap.key(locals.iterIndex); + + // Get true voting power + locals.beginBalance = 0; + locals.endBalance = 0; + state.mBeginEpochBalances.get(locals.iterVoter, locals.beginBalance); + state.mEndEpochBalances.get(locals.iterVoter, locals.endBalance); + locals.votingPower = (locals.beginBalance < locals.endBalance) ? locals.beginBalance : locals.endBalance; // min(begin, end) + + if (locals.votingPower > 0) // Apply voting power + { + state.mAssetProposalVoterMap.get(locals.iterVoter, locals.voterBitfield); + state.mAssetVoteOptions.get(locals.iterVoter, locals.voterOptions); + + for (locals.i_asset = 0; locals.i_asset < QRWA_MAX_ASSET_POLLS; locals.i_asset++) + { + if (state.mAssetPolls.get(locals.i_asset).status == QRWA_POLL_STATUS_ACTIVE && + locals.voterBitfield.get(locals.i_asset) == 1) + { + if (locals.voterOptions.get(locals.i_asset) == 1) // Voted Yes + { + locals.yesVotes = locals.assetPollVotesYes.get(locals.i_asset); + locals.assetPollVotesYes.set(locals.i_asset, sadd(locals.yesVotes, locals.votingPower)); + } + else // Voted No + { + locals.noVotes = locals.assetPollVotesNo.get(locals.i_asset); + locals.assetPollVotesNo.set(locals.i_asset, sadd(locals.noVotes, locals.votingPower)); + } + } + } + } + } + + // Finalize Asset Polls + for (locals.pollIndex = 0; locals.pollIndex < QRWA_MAX_ASSET_POLLS; ++locals.pollIndex) + { + locals.poll = state.mAssetPolls.get(locals.pollIndex); + + if (locals.poll.status == QRWA_POLL_STATUS_ACTIVE) + { + locals.yesVotes = locals.assetPollVotesYes.get(locals.pollIndex); + locals.noVotes = locals.assetPollVotesNo.get(locals.pollIndex); + + // Store final scores in the poll struct + locals.poll.votesYes = locals.yesVotes; + locals.poll.votesNo = locals.noVotes; + + locals.ownershipTransferred = 0; + locals.managementTransferred = 0; + locals.feePaid = 0; + locals.sufficientFunds = 0; + + + if (locals.yesVotes >= locals.quorumThreshold) // YES wins + { + // Check if asset is QMINE treasury + if (locals.poll.asset.issuer == state.mQmineAsset.issuer && locals.poll.asset.assetName == state.mQmineAsset.assetName) + { + // Release from treasury + if (state.mTreasuryBalance >= locals.poll.amount) + { + locals.sufficientFunds = 1; + locals.transferResult = qpi.transferShareOwnershipAndPossession( + state.mQmineAsset.assetName, state.mQmineAsset.issuer, + SELF, SELF, locals.poll.amount, locals.poll.destination); + + if (locals.transferResult >= 0) // ownership transfer succeeded + { + locals.ownershipTransferred = 1; + // Decrement internal balance + state.mTreasuryBalance = (state.mTreasuryBalance > locals.poll.amount) ? (state.mTreasuryBalance - locals.poll.amount) : 0; + + if (state.mRevenuePoolA >= QRWA_RELEASE_MANAGEMENT_FEE) + { + // Release management rights from this SC to QX + locals.releaseFeeResult = qpi.releaseShares( + locals.poll.asset, + locals.poll.destination, // new owner + locals.poll.destination, // new possessor + locals.poll.amount, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + QRWA_RELEASE_MANAGEMENT_FEE + ); + + if (locals.releaseFeeResult >= 0) // management transfer succeeded + { + locals.managementTransferred = 1; + locals.feePaid = 1; + state.mRevenuePoolA = (state.mRevenuePoolA > (uint64)locals.releaseFeeResult) ? (state.mRevenuePoolA - (uint64)locals.releaseFeeResult) : 0; + } + // else: Management transfer failed (shares are "stuck"). + // The destination ID still owns the transferred asset, but the SC management is currently under qRWA. + // The destination ID must use revokeAssetManagementRights to transfer the asset's SC management to QX + } + } + } + } + else // Asset is from mGeneralAssetBalances + { + locals.wrapper.setFrom(locals.poll.asset); + if (state.mGeneralAssetBalances.get(locals.wrapper, locals.currentAssetBalance)) + { + if (locals.currentAssetBalance >= locals.poll.amount) + { + locals.sufficientFunds = 1; + // Ownership Transfer + locals.transferResult = qpi.transferShareOwnershipAndPossession( + locals.poll.asset.assetName, locals.poll.asset.issuer, + SELF, SELF, locals.poll.amount, locals.poll.destination); + + if (locals.transferResult >= 0) // Ownership transfer tucceeded + { + locals.ownershipTransferred = 1; + // Decrement internal balance + locals.currentAssetBalance = (locals.currentAssetBalance > locals.poll.amount) ? (locals.currentAssetBalance - locals.poll.amount) : 0; + state.mGeneralAssetBalances.set(locals.wrapper, locals.currentAssetBalance); + + if (state.mRevenuePoolA >= QRWA_RELEASE_MANAGEMENT_FEE) + { + // Management Transfer + locals.releaseFeeResult = qpi.releaseShares( + locals.poll.asset, + locals.poll.destination, // new owner + locals.poll.destination, // new possessor + locals.poll.amount, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + QRWA_RELEASE_MANAGEMENT_FEE + ); + + if (locals.releaseFeeResult >= 0) // management transfer succeeded + { + locals.managementTransferred = 1; + locals.feePaid = 1; + state.mRevenuePoolA = (state.mRevenuePoolA > (uint64)locals.releaseFeeResult) ? (state.mRevenuePoolA - (uint64)locals.releaseFeeResult) : 0; + } + } + } + } + } + } + + locals.transferSuccess = locals.ownershipTransferred & locals.managementTransferred & locals.feePaid; + if (locals.transferSuccess == 1) // All steps succeeded + { + locals.logger.logType = QRWA_LOG_TYPE_ASSET_POLL_EXECUTED; + locals.logger.valueB = QRWA_STATUS_SUCCESS; + locals.poll.status = QRWA_POLL_STATUS_PASSED_EXECUTED; + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + if (locals.sufficientFunds == 0) + { + locals.logger.valueB = QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE; + } + else if (locals.ownershipTransferred == 0) + { + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + else + { + // This is the stuck shares case + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + } + locals.poll.status = QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION; + } + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.primaryId = locals.poll.destination; + locals.logger.valueA = locals.poll.proposalId; + LOG_INFO(locals.logger); + } + else // Vote failed (NO wins or < quorum) + { + locals.poll.status = QRWA_POLL_STATUS_FAILED_VOTE; + } + state.mAssetPolls.set(locals.pollIndex, locals.poll); + } + } + // Reset voter tracking map for asset polls + state.mAssetProposalVoterMap.reset(); + state.mAssetVoteOptions.reset(); + + // Copy the finalized epoch snapshots to the payout buffers + state.mPayoutBeginBalances.reset(); + state.mPayoutEndBalances.reset(); + state.mPayoutTotalQmineBegin = state.mTotalQmineBeginEpoch; + + // Copy mBeginEpochBalances -> mPayoutBeginBalances + locals.copyIndex = NULL_INDEX; + while (true) + { + locals.copyIndex = state.mBeginEpochBalances.nextElementIndex(locals.copyIndex); + if (locals.copyIndex == NULL_INDEX) + { + break; + } + locals.copyHolder = state.mBeginEpochBalances.key(locals.copyIndex); + locals.copyBalance = state.mBeginEpochBalances.value(locals.copyIndex); + state.mPayoutBeginBalances.set(locals.copyHolder, locals.copyBalance); + } + + // Copy mEndEpochBalances -> mPayoutEndBalances + locals.copyIndex = NULL_INDEX; + while (true) + { + locals.copyIndex = state.mEndEpochBalances.nextElementIndex(locals.copyIndex); + if (locals.copyIndex == NULL_INDEX) + { + break; + } + locals.copyHolder = state.mEndEpochBalances.key(locals.copyIndex); + locals.copyBalance = state.mEndEpochBalances.value(locals.copyIndex); + state.mPayoutEndBalances.set(locals.copyHolder, locals.copyBalance); + } + } + + + struct END_TICK_locals + { + DateAndTime now; + uint64 durationMicros; + uint64 msSinceLastPayout; + + uint64 totalGovPercent; + uint64 totalFeeAmount; + uint64 electricityPayout; + uint64 maintenancePayout; + uint64 reinvestmentPayout; + uint64 Y_revenue; + uint64 totalDistribution; + uint64 qminePayout; + uint64 qrwaPayout; + uint64 amountPerQRWAShare; + uint64 distributedAmount; + + sint64 qminePayoutIndex; + id holder; + uint64 beginBalance; + uint64 endBalance; + uint64 eligibleBalance; + // Use uint128 for all payout accounting + uint128 scaledPayout_128; + uint128 eligiblePayout_128; + uint128 totalEligiblePaid_128; + uint128 movedSharesPayout_128; + uint128 qmineDividendPool_128; + uint64 payout_u64; + uint64 foundEnd; + QRWALogger logger; + }; + END_TICK_WITH_LOCALS() + { + locals.now = qpi.now(); + + // Check payout conditions: Correct day, correct hour, and enough time passed + if (qpi.dayOfWeek((uint8)mod(locals.now.getYear(), (uint16)100), locals.now.getMonth(), locals.now.getDay()) == QRWA_PAYOUT_DAY && + locals.now.getHour() == QRWA_PAYOUT_HOUR) + { + // check if mLastPayoutTime is 0 (never initialized) + if (state.mLastPayoutTime.getYear() == 0) + { + // If never paid, treat as if enough time has passed + locals.msSinceLastPayout = QRWA_MIN_PAYOUT_INTERVAL_MS; + } + else + { + locals.durationMicros = state.mLastPayoutTime.durationMicrosec(locals.now); + + if (locals.durationMicros != UINT64_MAX) + { + locals.msSinceLastPayout = div(locals.durationMicros, 1000); + } + else + { + // If it's invalid but NOT zero, something is wrong, so we prevent payout + locals.msSinceLastPayout = 0; + } + } + + if (locals.msSinceLastPayout >= QRWA_MIN_PAYOUT_INTERVAL_MS) + { + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_DISTRIBUTION; + + // Calculate and pay out governance fees from Pool A (mined funds) + // gov_percentage = electricity_percent + maintenance_percent + reinvestment_percent + locals.totalGovPercent = sadd(sadd(state.mCurrentGovParams.electricityPercent, state.mCurrentGovParams.maintenancePercent), state.mCurrentGovParams.reinvestmentPercent); + locals.totalFeeAmount = 0; + + if (locals.totalGovPercent > 0 && locals.totalGovPercent <= QRWA_PERCENT_DENOMINATOR && state.mRevenuePoolA > 0) + { + locals.electricityPayout = div(smul(state.mRevenuePoolA, state.mCurrentGovParams.electricityPercent), QRWA_PERCENT_DENOMINATOR); + if (locals.electricityPayout > 0 && state.mCurrentGovParams.electricityAddress != NULL_ID) + { + if (qpi.transfer(state.mCurrentGovParams.electricityAddress, locals.electricityPayout) >= 0) + { + locals.totalFeeAmount = sadd(locals.totalFeeAmount, locals.electricityPayout); + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.electricityAddress; + locals.logger.valueA = locals.electricityPayout; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + + locals.maintenancePayout = div(smul(state.mRevenuePoolA, state.mCurrentGovParams.maintenancePercent), QRWA_PERCENT_DENOMINATOR); + if (locals.maintenancePayout > 0 && state.mCurrentGovParams.maintenanceAddress != NULL_ID) + { + if (qpi.transfer(state.mCurrentGovParams.maintenanceAddress, locals.maintenancePayout) >= 0) + { + locals.totalFeeAmount = sadd(locals.totalFeeAmount, locals.maintenancePayout); + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.maintenanceAddress; + locals.logger.valueA = locals.maintenancePayout; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + + locals.reinvestmentPayout = div(smul(state.mRevenuePoolA, state.mCurrentGovParams.reinvestmentPercent), QRWA_PERCENT_DENOMINATOR); + if (locals.reinvestmentPayout > 0 && state.mCurrentGovParams.reinvestmentAddress != NULL_ID) + { + if (qpi.transfer(state.mCurrentGovParams.reinvestmentAddress, locals.reinvestmentPayout) >= 0) + { + locals.totalFeeAmount = sadd(locals.totalFeeAmount, locals.reinvestmentPayout); + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.reinvestmentAddress; + locals.logger.valueA = locals.reinvestmentPayout; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + state.mRevenuePoolA = (state.mRevenuePoolA > locals.totalFeeAmount) ? (state.mRevenuePoolA - locals.totalFeeAmount) : 0; + } + + // Calculate total distribution pool + locals.Y_revenue = state.mRevenuePoolA; // Remaining Pool A after fees + locals.totalDistribution = sadd(locals.Y_revenue, state.mRevenuePoolB); + + // Allocate to QMINE and qRWA pools + if (locals.totalDistribution > 0) + { + locals.qminePayout = div(smul(locals.totalDistribution, QRWA_QMINE_HOLDER_PERCENT), QRWA_PERCENT_DENOMINATOR); + locals.qrwaPayout = locals.totalDistribution - locals.qminePayout; // Avoid potential rounding errors + + state.mQmineDividendPool = sadd(state.mQmineDividendPool, locals.qminePayout); + state.mQRWADividendPool = sadd(state.mQRWADividendPool, locals.qrwaPayout); + + // Reset revenue pools after allocation + state.mRevenuePoolA = 0; + state.mRevenuePoolB = 0; + } + + // Distribute QMINE rewards + if (state.mQmineDividendPool > 0 && state.mPayoutTotalQmineBegin > 0) + { + locals.totalEligiblePaid_128 = 0; + locals.qminePayoutIndex = NULL_INDEX; // Start iteration + locals.qmineDividendPool_128 = state.mQmineDividendPool; // Create 128-bit copy for accounting + + // pay eligible holders + while (true) + { + locals.qminePayoutIndex = state.mPayoutBeginBalances.nextElementIndex(locals.qminePayoutIndex); + if (locals.qminePayoutIndex == NULL_INDEX) + { + break; + } + + locals.holder = state.mPayoutBeginBalances.key(locals.qminePayoutIndex); + locals.beginBalance = state.mPayoutBeginBalances.value(locals.qminePayoutIndex); + + locals.foundEnd = state.mPayoutEndBalances.get(locals.holder, locals.endBalance) ? 1 : 0; + if (locals.foundEnd == 0) + { + locals.endBalance = 0; + } + + locals.eligibleBalance = (locals.beginBalance < locals.endBalance) ? locals.beginBalance : locals.endBalance; + + if (locals.eligibleBalance > 0) + { + // Payout = (EligibleBalance * DividendPool) / PayoutBase + locals.scaledPayout_128 = (uint128)locals.eligibleBalance * (uint128)state.mQmineDividendPool; + locals.eligiblePayout_128 = div(locals.scaledPayout_128, state.mPayoutTotalQmineBegin); + + if (locals.eligiblePayout_128 > (uint128)0 && locals.eligiblePayout_128 <= locals.qmineDividendPool_128) + { + // Cast to uint64 ONLY at the moment of transfer + locals.payout_u64 = locals.eligiblePayout_128.low; + + // Check if the cast truncated the value (if high part was set) + if (locals.eligiblePayout_128.high == 0 && locals.payout_u64 > 0) + { + if (qpi.transfer(locals.holder, (sint64)locals.payout_u64) >= 0) + { + locals.qmineDividendPool_128 -= locals.eligiblePayout_128; + state.mTotalQmineDistributed = sadd(state.mTotalQmineDistributed, locals.payout_u64); + locals.totalEligiblePaid_128 += locals.eligiblePayout_128; + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = locals.payout_u64; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + } + else if (locals.eligiblePayout_128 > locals.qmineDividendPool_128) + { + // Payout is larger than the remaining pool + locals.payout_u64 = locals.qmineDividendPool_128.low; // Get remaining pool + + if (locals.qmineDividendPool_128.high == 0 && locals.payout_u64 > 0) + { + if (qpi.transfer(locals.holder, (sint64)locals.payout_u64) >= 0) + { + state.mTotalQmineDistributed = sadd(state.mTotalQmineDistributed, locals.payout_u64); + locals.totalEligiblePaid_128 += locals.qmineDividendPool_128; + locals.qmineDividendPool_128 = 0; // Pool exhausted + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = locals.holder; + locals.logger.valueA = locals.payout_u64; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + break; + } + } + } + + // Pay QMINE DEV the entire remainder of the pool + locals.movedSharesPayout_128 = locals.qmineDividendPool_128; + if (locals.movedSharesPayout_128 > (uint128)0 && state.mCurrentGovParams.qmineDevAddress != NULL_ID) + { + locals.payout_u64 = locals.movedSharesPayout_128.low; + if (locals.movedSharesPayout_128.high == 0 && locals.payout_u64 > 0) + { + if (qpi.transfer(state.mCurrentGovParams.qmineDevAddress, (sint64)locals.payout_u64) >= 0) + { + state.mTotalQmineDistributed = sadd(state.mTotalQmineDistributed, locals.payout_u64); + locals.qmineDividendPool_128 = 0; + } + else + { + locals.logger.logType = QRWA_LOG_TYPE_ERROR; + locals.logger.primaryId = state.mCurrentGovParams.qmineDevAddress; + locals.logger.valueA = locals.payout_u64; + locals.logger.valueB = QRWA_STATUS_FAILURE_TRANSFER_FAILED; + LOG_INFO(locals.logger); + } + } + } + + // Update the 64-bit state variable from the 128-bit local + // If transfers failed, funds remain in qmineDividendPool_128 and will be preserved here. + state.mQmineDividendPool = locals.qmineDividendPool_128.low; + } // End QMINE distribution + + // Distribute qRWA shareholder rewards + if (state.mQRWADividendPool > 0) + { + locals.amountPerQRWAShare = div(state.mQRWADividendPool, NUMBER_OF_COMPUTORS); + if (locals.amountPerQRWAShare > 0) + { + if (qpi.distributeDividends(static_cast(locals.amountPerQRWAShare))) + { + locals.distributedAmount = smul(locals.amountPerQRWAShare, static_cast(NUMBER_OF_COMPUTORS)); + state.mQRWADividendPool -= locals.distributedAmount; + state.mTotalQRWADistributed = sadd(state.mTotalQRWADistributed, locals.distributedAmount); + } + } + } + + // Update last payout time + state.mLastPayoutTime = qpi.now(); + locals.logger.logType = QRWA_LOG_TYPE_DISTRIBUTION; + locals.logger.primaryId = NULL_ID; + locals.logger.valueA = 1; // Indicate success + locals.logger.valueB = 0; + LOG_INFO(locals.logger); + } + } + } + + struct POST_INCOMING_TRANSFER_locals + { + QRWALogger logger; + }; + POST_INCOMING_TRANSFER_WITH_LOCALS() + { + // Differentiate revenue streams based on source type + if (input.sourceId.u64._1 == 0 && input.sourceId.u64._2 == 0 && input.sourceId.u64._3 == 0 && input.sourceId.u64._0 != 0) + { + // Source is likely a contract (e.g., QX transfer) -> Pool A + state.mRevenuePoolA = sadd(state.mRevenuePoolA, static_cast(input.amount)); + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_INCOMING_REVENUE_A; + locals.logger.primaryId = input.sourceId; + locals.logger.valueA = input.amount; + locals.logger.valueB = input.type; + LOG_INFO(locals.logger); + } + else if (input.sourceId != NULL_ID) + { + // Source is likely a user (EOA) -> Pool B + state.mRevenuePoolB = sadd(state.mRevenuePoolB, static_cast(input.amount)); + locals.logger.contractId = CONTRACT_INDEX; + locals.logger.logType = QRWA_LOG_TYPE_INCOMING_REVENUE_B; + locals.logger.primaryId = input.sourceId; + locals.logger.valueA = input.amount; + locals.logger.valueB = input.type; + LOG_INFO(locals.logger); + } + } + + PRE_ACQUIRE_SHARES() + { + // Allow any entity to transfer asset management rights to this contract + output.requestedFee = 0; + output.allowTransfer = true; + } + + POST_ACQUIRE_SHARES() + { + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + // PROCEDURES + REGISTER_USER_PROCEDURE(DonateToTreasury, 3); + REGISTER_USER_PROCEDURE(VoteGovParams, 4); + REGISTER_USER_PROCEDURE(CreateAssetReleasePoll, 5); + REGISTER_USER_PROCEDURE(VoteAssetRelease, 6); + REGISTER_USER_PROCEDURE(DepositGeneralAsset, 7); + REGISTER_USER_PROCEDURE(RevokeAssetManagementRights, 8); + + // FUNCTIONS + REGISTER_USER_FUNCTION(GetGovParams, 1); + REGISTER_USER_FUNCTION(GetGovPoll, 2); + REGISTER_USER_FUNCTION(GetAssetReleasePoll, 3); + REGISTER_USER_FUNCTION(GetTreasuryBalance, 4); + REGISTER_USER_FUNCTION(GetDividendBalances, 5); + REGISTER_USER_FUNCTION(GetTotalDistributed, 6); + REGISTER_USER_FUNCTION(GetActiveAssetReleasePollIds, 7); + REGISTER_USER_FUNCTION(GetActiveGovPollIds, 8); + REGISTER_USER_FUNCTION(GetGeneralAssetBalance, 9); + REGISTER_USER_FUNCTION(GetGeneralAssets, 10); + } +}; diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index 652ba9cfd..cfd2e697a 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -49,65 +49,87 @@ namespace QPI typedef signed long long sint64; typedef unsigned long long uint64; + // Error codes for inter-contract calls (used when calling other contracts fails) + // These are returned to the calling contract so it can handle the error + enum InterContractCallError : uint8 + { + NoCallError = 0, + CallErrorContractInErrorState = 1, // Called contract is already in error state + CallErrorInsufficientFees = 2, // Called contract has no execution fee reserve + CallErrorAllocationFailed = 3, // Failed to allocate context on stack + }; + typedef uint128_t uint128; typedef m256i id; +#define STATIC_ASSERT(condition, identifier) static_assert(condition, #identifier); + #define NULL_ID id::zero() + constexpr sint64 NULL_INDEX = -1; constexpr sint64 INVALID_AMOUNT = 0x8000000000000000; -#define _A 0 -#define _B 1 -#define _C 2 -#define _D 3 -#define _E 4 -#define _F 5 -#define _G 6 -#define _H 7 -#define _I 8 -#define _J 9 -#define _K 10 -#define _L 11 -#define _M 12 -#define _N 13 -#define _O 14 -#define _P 15 -#define _Q 16 -#define _R 17 -#define _S 18 -#define _T 19 -#define _U 20 -#define _V 21 -#define _W 22 -#define _X 23 -#define _Y 24 -#define _Z 25 -#define ID(_00, _01, _02, _03, _04, _05, _06, _07, _08, _09, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55) _mm256_set_epi64x(((((((((((((((uint64)_55) * 26 + _54) * 26 + _53) * 26 + _52) * 26 + _51) * 26 + _50) * 26 + _49) * 26 + _48) * 26 + _47) * 26 + _46) * 26 + _45) * 26 + _44) * 26 + _43) * 26 + _42, ((((((((((((((uint64)_41) * 26 + _40) * 26 + _39) * 26 + _38) * 26 + _37) * 26 + _36) * 26 + _35) * 26 + _34) * 26 + _33) * 26 + _32) * 26 + _31) * 26 + _30) * 26 + _29) * 26 + _28, ((((((((((((((uint64)_27) * 26 + _26) * 26 + _25) * 26 + _24) * 26 + _23) * 26 + _22) * 26 + _21) * 26 + _20) * 26 + _19) * 26 + _18) * 26 + _17) * 26 + _16) * 26 + _15) * 26 + _14, ((((((((((((((uint64)_13) * 26 + _12) * 26 + _11) * 26 + _10) * 26 + _09) * 26 + _08) * 26 + _07) * 26 + _06) * 26 + _05) * 26 + _04) * 26 + _03) * 26 + _02) * 26 + _01) * 26 + _00) + constexpr long long _A = 0; + constexpr long long _B = 1; + constexpr long long _C = 2; + constexpr long long _D = 3; + constexpr long long _E = 4; + constexpr long long _F = 5; + constexpr long long _G = 6; + constexpr long long _H = 7; + constexpr long long _I = 8; + constexpr long long _J = 9; + constexpr long long _K = 10; + constexpr long long _L = 11; + constexpr long long _M = 12; + constexpr long long _N = 13; + constexpr long long _O = 14; + constexpr long long _P = 15; + constexpr long long _Q = 16; + constexpr long long _R = 17; + constexpr long long _S = 18; + constexpr long long _T = 19; + constexpr long long _U = 20; + constexpr long long _V = 21; + constexpr long long _W = 22; + constexpr long long _X = 23; + constexpr long long _Y = 24; + constexpr long long _Z = 25; + + inline id ID(long long _00, long long _01, long long _02, long long _03, long long _04, long long _05, long long _06, long long _07, long long _08, long long _09, + long long _10, long long _11, long long _12, long long _13, long long _14, long long _15, long long _16, long long _17, long long _18, long long _19, + long long _20, long long _21, long long _22, long long _23, long long _24, long long _25, long long _26, long long _27, long long _28, long long _29, + long long _30, long long _31, long long _32, long long _33, long long _34, long long _35, long long _36, long long _37, long long _38, long long _39, + long long _40, long long _41, long long _42, long long _43, long long _44, long long _45, long long _46, long long _47, long long _48, long long _49, + long long _50, long long _51, long long _52, long long _53, long long _54, long long _55) + { + return _mm256_set_epi64x(((((((((((((((uint64)_55) * 26 + _54) * 26 + _53) * 26 + _52) * 26 + _51) * 26 + _50) * 26 + _49) * 26 + _48) * 26 + _47) * 26 + _46) * 26 + _45) * 26 + _44) * 26 + _43) * 26 + _42, ((((((((((((((uint64)_41) * 26 + _40) * 26 + _39) * 26 + _38) * 26 + _37) * 26 + _36) * 26 + _35) * 26 + _34) * 26 + _33) * 26 + _32) * 26 + _31) * 26 + _30) * 26 + _29) * 26 + _28, ((((((((((((((uint64)_27) * 26 + _26) * 26 + _25) * 26 + _24) * 26 + _23) * 26 + _22) * 26 + _21) * 26 + _20) * 26 + _19) * 26 + _18) * 26 + _17) * 26 + _16) * 26 + _15) * 26 + _14, ((((((((((((((uint64)_13) * 26 + _12) * 26 + _11) * 26 + _10) * 26 + _09) * 26 + _08) * 26 + _07) * 26 + _06) * 26 + _05) * 26 + _04) * 26 + _03) * 26 + _02) * 26 + _01) * 26 + _00); + } #define NUMBER_OF_COMPUTORS 676 #define QUORUM (NUMBER_OF_COMPUTORS * 2 / 3 + 1) -#define JANUARY 1 -#define FEBRUARY 2 -#define MARCH 3 -#define APRIL 4 -#define MAY 5 -#define JUNE 6 -#define JULY 7 -#define AUGUST 8 -#define SEPTEMBER 9 -#define OCTOBER 10 -#define NOVEMBER 11 -#define DECEMBER 12 - -#define WEDNESDAY 0 -#define THURSDAY 1 -#define FRIDAY 2 -#define SATURDAY 3 -#define SUNDAY 4 -#define MONDAY 5 -#define TUESDAY 6 + constexpr int JANUARY = 1; + constexpr int FEBRUARY = 2; + constexpr int MARCH = 3; + constexpr int APRIL = 4; + constexpr int MAY = 5; + constexpr int JUNE = 6; + constexpr int JULY = 7; + constexpr int AUGUST = 8; + constexpr int SEPTEMBER = 9; + constexpr int OCTOBER = 10; + constexpr int NOVEMBER = 11; + constexpr int DECEMBER = 12; + + constexpr int WEDNESDAY = 0; + constexpr int THURSDAY = 1; + constexpr int FRIDAY = 2; + constexpr int SATURDAY = 3; + constexpr int SUNDAY = 4; + constexpr int MONDAY = 5; + constexpr int TUESDAY = 6; constexpr unsigned long long X_MULTIPLIER = 1ULL; @@ -115,191 +137,628 @@ namespace QPI template inline void copyMemory(T1& dst, const T2& src); + // Copy object src into buffer dst. The size of the dst buffer must be grater or equal to the size of src object. + // If dst size is greater than src size and setTailToZero is true, set the part of dst to zero that follows + // behind the copy of src. + template + inline void copyToBuffer(T1& dst, const T2& src, bool setTailToZero = false); + + // Set object dst from buffer src. The size of the src buffer must be grater or equal to the size of dst object. + template + inline void copyFromBuffer(T1& dst, const T2& src); + // Set all memory of dst to byte value. template inline void setMemory(T& dst, uint8 value); + /** + * Date and time (up to microsecond precision, year range from 0 to 65535, 8-byte representation) + * + * May contain invalid dates. Follows Gregorian calendar, implementing leap years but no leap seconds. + */ struct DateAndTime { - // --- Member Variables --- - unsigned short millisecond; - unsigned char second; - unsigned char minute; - unsigned char hour; - unsigned char day; - unsigned char month; - unsigned char year; + /// Init with value 0 (no valid date). + DateAndTime() + { + value = 0; + } + + /// Init object with date/time. See `set()` for info about parameters. + DateAndTime(uint64 year, uint64 month, uint64 day, uint64 hour = 0, uint64 minute = 0, uint64 second = 0, uint64 millisec = 0, uint64 microsecDuringMillisec = 0) + { + set(year, month, day, hour, minute, second, millisec, microsecDuringMillisec); + } - // --- Public Member Operators --- + /// Copy object + DateAndTime(const DateAndTime& other) + { + value = other.value; + } + + /// Assign object + DateAndTime& operator = (const DateAndTime& other) + { + value = other.value; + return *this; + } /** - * @brief Checks if this date is earlier than the 'other' date. - */ - bool operator<(const DateAndTime& other) const + * @brief Set date and time value without checking if it is valid. + * @param year Year of the date (without offset). Should be in range 0 to 65335. + * @param month Month of the date. Should be in range 1 to 12 to be valid. + * @param day Day of the month. Should be in range 1 to 31/30/29/28 to be valid, depending on year and month. + * @param hour Hour during the day. Should be in range 0 to 23 to be valid. + * @param minute Minute during the hour. Should be in range 0 to 59 to be valid. + * @param second Second during the minute. Should be in range 0 to 59 to be valid. + * @param millisec Millisecond during the second. Should be in range 0 to 999 to be valid. + * @param microsecDuringMillisec Microsecond during the millisecond. Should be in range 0 to 999 to be valid. + */ + inline void set(uint64 year, uint64 month, uint64 day, uint64 hour, uint64 minute, uint64 second, uint64 millisec = 0, uint64 microsecDuringMillisec = 0) + { + value = (year << 46) | (month << 42) | (day << 37) + | (hour << 32) | (minute << 26) | (second << 20) + | (millisec << 10) | (microsecDuringMillisec); + } + + /// Set date/time if valid (returns true). Otherwise returns false. + bool setIfValid(uint64 year, uint64 month, uint64 day, uint64 hour, uint64 minute, uint64 second, uint64 millisec = 0, uint64 microsecDuringMillisec = 0) { - if (year != other.year) return year < other.year; - if (month != other.month) return month < other.month; - if (day != other.day) return day < other.day; - if (hour != other.hour) return hour < other.hour; - if (minute != other.minute) return minute < other.minute; - if (second != other.second) return second < other.second; - return millisecond < other.millisecond; + if (!isValid(year, month, day, hour, minute, second, millisec, microsecDuringMillisec)) + return false; + set(year, month, day, hour, minute, second, millisec, microsecDuringMillisec); + return true; } /** - * @brief Checks if this date is later than the 'other' date. - */ - bool operator>(const DateAndTime& other) const + * @brief Set date value without checking if it is valid. + * @param year Year of the date (without offset). Should be in range 0 to 65335. + * @param month Month of the date. Should be in range 1 to 12 to be valid. + * @param day Day of the month. Should be in range 1 to 31/30/29/28 to be valid, depending on year and month. + */ + inline void setDate(uint64 year, uint64 month, uint64 day) { - return other < *this; // Reuses the operator< on the 'other' object + // clear bits of current date (only keep 37 bits of time) before setting new date + value &= 0x1fffffffff; + value |= (year << 46) | (month << 42) | (day << 37); } /** - * @brief Checks if this date is identical to the 'other' date. - */ + * @brief Set time without checking if it is valid. + * @param hour Hour during the day. Should be in range 0 to 23 to be valid. + * @param minute Minute during the hour. Should be in range 0 to 59 to be valid. + * @param second Second during the minute. Should be in range 0 to 59 to be valid. + * @param millisec Millisecond during the second. Should be in range 0 to 999 to be valid. + * @param microsecDuringMillisec Microsecond during the millisecond. Should be in range 0 to 999 to be valid. + */ + inline void setTime(uint64 hour, uint64 minute, uint64 second, uint64 millisec = 0, uint64 microsecDuringMillisec = 0) + { + // clear bits of current time (only keep 25 bits of date) before setting new time + value &= (0x1ffffffllu << 37llu); + value |= (hour << 32) | (minute << 26) | (second << 20) + | (millisec << 10) | (microsecDuringMillisec); + } + + /// Return year of date/time (range 0 to 65535). + uint16 getYear() const + { + return static_cast(value >> 46); + } + + /// Return month of date/time (range 1 to 12 if valid). + uint8 getMonth() const + { + return static_cast(value >> 42) & 0b1111; + } + + /// Return month of date/time (range 1 to 31/30/29/28 depending on month if valid). + uint8 getDay() const + { + return static_cast(value >> 37) & 0b11111; + } + + /// Return hour of date/time (range 0 to 23 if valid). + uint8 getHour() const + { + return static_cast(value >> 32) & 0b11111; + } + + /// Return minute of date/time (range 0 to 59 if valid). + uint8 getMinute() const + { + return static_cast(value >> 26) & 0b111111; + } + + /// Return second of date/time (range 0 to 59 if valid). + uint8 getSecond() const + { + return static_cast(value >> 20) & 0b111111; + } + + /// Return millisecond of date/time (range 0 to 999 if valid). + uint16 getMillisec() const + { + return static_cast(value >> 10) & 0b1111111111; + } + + /// Return microsecond in current millisecond of date/time (range 0 to 999 if valid). + uint16 getMicrosecDuringMillisec() const + { + return static_cast(value) & 0b1111111111; + } + + /// Check if this instance contains a valid date and time. + bool isValid() const + { + return isValid(getYear(), getMonth(), getDay(), getHour(), getMinute(), getSecond(), getMillisec(), getMicrosecDuringMillisec()); + } + + /// Check if a year is a leap year. + static bool isLeapYear(uint64 year) + { + if (year % 4 != 0) + return false; + if (year % 100 == 0) + { + if (year % 400 == 0) + return true; + else + return false; + } + return true; + } + + /// Return the number of days in a month of a specific year. + static uint8 daysInMonth(uint64 year, uint64 month) + { + const int daysInMonth[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + if (month < 1 || month > 12) + return 0; + if (month == 2 && DateAndTime::isLeapYear(year)) + return 29; + else + return daysInMonth[month]; + }; + + /// Check if the date and time given by parameters is valid. + static inline bool isValid(uint64 year, uint64 month, uint64 day, uint64 hour, uint64 minute, uint64 second, uint64 millisec, uint64 microsecDuringMillisec) + { + if (year > 0xffffu) + return false; + if (month > 12 || month == 0) + return false; + if (day > 31 || day == 0) + return false; + if ((day == 31) && (month != 1) && (month != 3) && (month != 5) && (month != 7) && (month != 8) && (month != 10) && (month != 12)) + return false; + if ((day == 30) && (month == 2)) + return false; + if ((day == 29) && (month == 2) && !isLeapYear(year)) + return false; + if (hour >= 24) + return false; + if (minute >= 60) + return false; + if (second >= 60) + return false; + if (millisec >= 1000) + return false; + if (microsecDuringMillisec >= 1000) + return false; + return true; + } + + /// Checks if this date is earlier than the `other` date. + bool operator<(const DateAndTime& other) const + { + return value < other.value; + } + + /// Checks if this date is later than the `other` date. + bool operator>(const DateAndTime& other) const + { + return value > other.value; + } + + /// Checks if this date is identical to the `other` date. bool operator==(const DateAndTime& other) const { - return year == other.year && - month == other.month && - day == other.day && - hour == other.hour && - minute == other.minute && - second == other.second && - millisecond == other.millisecond; + return value == other.value; + } + + /// Checks if this date is different from the `other` date. + bool operator!=(const DateAndTime& other) const + { + return value != other.value; } /** - * @brief Computes the difference between this date and 'other' in milliseconds. - */ - long long operator-(const DateAndTime& other) const + * Change date and time by adding a combination of time units. + * @param years Number of years to add. May be negative. + * @param months Number of months to add. May be negative and abs(months) may be > 12. + * @param days Number of days to add. May be negative and abs(days) may be > 365. + * @param hours Number of hours to add. May be negative and abs(hours) may be > 24. + * @param minutes Number of minutes to add. May be negative and abs(minutes) may be > 60. + * @param seconds Number of seconds to add. May be negative and abs(seconds) may be > 60. + * @param millisecs Number of millisecs to add. May be negative and abs(millisecs) may be > 1000. + * @param microsecsDuringMillisec Number of millisecs to add. May be negative and abs(microsecsDuringMillisec) may be > 1000. + * @return Returns if update of date and time was successful. Error cases: Overflow, starting with invalid date (see below). + * + * This function requires a valid date to start with if it needs to change the date. If less than 1 day is added/subtracted + * and the date does not flip due to the added/subtracted time (hours/minutes/seconds etc.), `add()` succeeds even with an + * invalid date. Thus, for example if you want to accumulate short periods of time, you may use `add()` with an invalid date + * such as 0000-00-00. However, it will fail and return false if you the accumulated time exceeds 23:59:59.999'999. + */ + bool add(sint64 years, sint64 months, sint64 days, sint64 hours, sint64 minutes, sint64 seconds, sint64 millisecs = 0, sint64 microsecsDuringMillisec = 0) { - // A member function can access private members of other instances of the same class. - return this->toMilliseconds() - other.toMilliseconds(); + sint64 newMicrosec = getMicrosecDuringMillisec(); + sint64 newMillisec = getMillisec(); + sint64 newSec = getSecond(); + sint64 newMinute = getMinute(); + sint64 newHour = getHour(); + sint64 millisecCarry = 0, secCarry = 0, minuteCarry = 0, hourCarry = 0, dayCarry = 0; + + // update microseconds (checking for overflow) + if (microsecsDuringMillisec && + !addAndComputeCarry(newMicrosec, millisecCarry, 1000, microsecsDuringMillisec)) + { + return false; + } + + // update milliseconds (checking for overflow) + if ((millisecs || millisecCarry) && + !addAndComputeCarry(newMillisec, secCarry, 1000, millisecs, millisecCarry)) + { + return false; + } + + // update seconds (checking for overflow) + if ((seconds || secCarry) && + !addAndComputeCarry(newSec, minuteCarry, 60, seconds, secCarry)) + { + return false; + } + + // update minutes (checking for overflow) + if ((minutes | minuteCarry) && + !addAndComputeCarry(newMinute, hourCarry, 60, minutes, minuteCarry)) + { + return false; + } + + // update hours (checking for overflow) + if ((hours | hourCarry) && + !addAndComputeCarry(newHour, dayCarry, 24, hours, hourCarry)) + { + return false; + } + + // set time + if (this->isValid()) + { + ASSERT(isValid(getYear(), getMonth(), getDay(), newHour, newMinute, newSec, newMillisec, newMicrosec)); + } + setTime(newHour, newMinute, newSec, newMillisec, newMicrosec); + + // update date if needed + if (dayCarry || days || months || years) + { + if (dayCarry && !addWithoutOverflow(days, dayCarry)) + return false; + return add(years, months, days); + } + + return true; } /** - * @brief Adds a duration in milliseconds to the current date/time. - * @param msToAdd The number of milliseconds to add. Can be negative. - * @return A new DateAndTime object representing the result. - */ - DateAndTime operator+(long long msToAdd) const - { - long long totalMs = this->toMilliseconds() + msToAdd; - - DateAndTime result = { 0,0,0,0,0,0,0 }; - - // Handle negative totalMs (dates before the epoch) if necessary - // For this implementation, we assume resulting dates are >= year 2000 - if (totalMs < 0) totalMs = 0; - - long long days = totalMs / 86400000LL; - long long msInDay = totalMs % 86400000LL; - - // Calculate time part - result.hour = (unsigned char)(msInDay / 3600000LL); - msInDay %= 3600000LL; - result.minute = (unsigned char)(msInDay / 60000LL); - msInDay %= 60000LL; - result.second = (unsigned char)(msInDay / 1000LL); - result.millisecond = (unsigned short)(msInDay % 1000LL); - - // Calculate date part from total days since epoch - unsigned char currentYear = 0; - while (true) + * Change date by adding number of days, months, and years. + * @param years Number of years to add. May be negative. + * @param months Number of months to add. May be negative and abs(months) may be > 12. + * @param days Number of days to add. May be negative and abs(days) may be > 365. + * @return Returns if update of date was successful. Error cases: starting with invalid date, overflow. + */ + bool add(sint64 years, sint64 months, sint64 days) + { + sint64 newDay = getDay(); + sint64 newMonth = getMonth(); + sint64 newYear = getYear(); + + if (!isValid()) + return false; + + // speed-up processing large number of days (400 years and more) + // (400 years always have the same number of leap years and days) + constexpr sint64 daysPer400years = 97ll * 366ll + 303ll * 365ll; + if (days >= daysPer400years || days <= -daysPer400years) + { + sint64 factor400years = days / daysPer400years; + sint64 daysProcessed = factor400years * daysPer400years; + newYear += factor400years * 400; + days -= daysProcessed; + } + + // speed-up processing large number of days (more than 1 year) + if (days >= 365) { - long long daysThisYear = isLeap(currentYear) ? 366 : 365; - if (days >= daysThisYear) + sint64 yShift = (newMonth >= 3) ? 1 : 0; + while (days >= 365) { - days -= daysThisYear; - currentYear++; + sint64 daysInYear = DateAndTime::isLeapYear(newYear + yShift) ? 366 : 365; + if (days < daysInYear) + break; + ++newYear; + days -= daysInYear; } - else + } + else if (days <= -365) + { + sint64 yShift = (newMonth >= 3) ? 0 : -1; + while (days <= -365) { - break; + sint64 daysInYear = DateAndTime::isLeapYear(newYear + yShift) ? -366 : -365; + if (days > daysInYear) + break; + --newYear; + days -= daysInYear; } } - result.year = currentYear; - unsigned char currentMonth = 1; - const int daysInMonth[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; - while (true) + // general processing of any number of days + while (days > 0) { - long long daysThisMonth = daysInMonth[currentMonth]; - if (currentMonth == 2 && isLeap(result.year)) + // update day and month + const sint64 monthDays = daysInMonth(newYear, newMonth); + if (days >= monthDays) { - daysThisMonth = 29; + // 1 month or more -> skip current month + ++newMonth; + days -= monthDays; } - if (days >= daysThisMonth) + else + { + // less than one month -> update day (and month if needed) + newDay += days; + days = 0; + if (newDay > monthDays) + { + ++newMonth; + newDay -= monthDays; + } + } + // update year if needed + if (newMonth > 12) { - days -= daysThisMonth; - currentMonth++; + newMonth = 1; + ++newYear; + } + // check if day exists in month + if (newDay > 28) + { + const sint64 monthDays = daysInMonth(newYear, newMonth); + if (newDay > monthDays) + { + ASSERT(newDay <= 31); + ASSERT(newMonth < 12); + newDay -= monthDays; + newMonth += 1; + } + } + } + while (days < 0) + { + // update day and month + if (-days < newDay) + { + // new date is in current month + newDay += days; + days = 0; } else { - break; + // new date is before current month + --newMonth; + if (newMonth < 1) + { + --newYear; + newMonth = 12; + } + const sint64 monthDays = daysInMonth(newYear, newMonth); + if (-days >= monthDays) + { + // at least one month back -> keep day (month was already updated before) + days += monthDays; + } + else + { + // less than one month -> update day + newDay += monthDays + days; + days = 0; + } + } + } + + // add month that were passed to this function + if (months) + { + // Add months to newMonth, getting years carry. Months are computed in 0-11 instead of 1-12. + sint64 yearsCarry = 0; + --newMonth; + if (!addAndComputeCarry(newMonth, yearsCarry, 12, months)) + return false; + ++newMonth; + if (yearsCarry && !addWithoutOverflow(newYear, yearsCarry)) + return false; + } + + // add years passed to function + if (years && !addWithoutOverflow(newYear, years)) + return false; + + // check that day exists in final month + if (newDay > 28) + { + const sint64 monthDays = daysInMonth(newYear, newMonth); + if (newDay > monthDays) + { + ASSERT(newDay <= 31); + ASSERT(newMonth < 12); + newDay -= monthDays; + newMonth += 1; } } - ASSERT(days <= 31); - result.month = currentMonth; - result.day = (unsigned char)(days) + 1; // days is 0-indexed, day is 1-indexed - return result; - } + // check if year is outside supported range + if (newYear < 0 || newYear > 0xffff) + return false; - DateAndTime& operator+=(long long msToAdd) - { - *this = *this + msToAdd; // Reuse operator+ and assign the result back to this object - return *this; + ASSERT(isValid(newYear, newMonth, newDay, getHour(), getMinute(), getSecond(), getMillisec(), getMicrosecDuringMillisec())); + setDate(newYear, newMonth, newDay); + + return true; } - DateAndTime& operator-=(long long msToSubtract) + /** + * Convenience function for adding a number of days. + * @param days Number of days to add. May be negative and abs(days) may be > 365. + * @return Returns if update of date was successful. Error cases: starting with invalid date, overflow. + */ + bool addDays(sint64 days) { - *this = *this + (-msToSubtract); // Reuse operator+ with a negative value - return *this; + return add(0, 0, days); } - private: - // --- Private Helper Functions --- - /** - * @brief A static helper to check if a year (yy format) is a leap year. - */ - static bool isLeap(unsigned char yr) { - // here we only handle the case where yr is in range [00 to 99] - return (2000 + yr) % 4 == 0; + * Convenience function for adding a number of microseconds. + * @param days Number of microsecs to add. May be negative and abs(microsecs) may be > 1000000. + * @return Returns if update of date was successful. Error cases: starting with invalid date, overflow. + */ + bool addMicrosec(sint64 microsec) + { + return add(0, 0, 0, 0, 0, 0, 0, microsec); } /** - * @brief Helper to convert this specific DateAndTime instance to total milliseconds since Jan 1, 2000. - */ - long long toMilliseconds() const { - long long totalDays = 0; - - // Add days for full years passed since 2000 - for (unsigned char y = 0; y < year; ++y) { - totalDays += isLeap(y) ? 366 : 365; + * Compute duration between this and dt in microseconds. Returns UINT64_MAX if dt or this is invalid. + */ + uint64 durationMicrosec(const DateAndTime& dt) const + { + if (!isValid() || !dt.isValid()) + return UINT64_MAX; + + if (value == dt.value) + return 0; + + DateAndTime begin = *this; + DateAndTime end = dt; + if (begin > end) + { + begin = dt; + end = *this; } - // Add days for full months passed in the current year - const int daysInMonth[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; - for (unsigned char m = 1; m < month; ++m) { - totalDays += daysInMonth[m]; - if (m == 2 && isLeap(year)) { - totalDays += 1; + sint64 microDiff = end.getMicrosecDuringMillisec() - begin.getMicrosecDuringMillisec(); + sint64 milliDiff = end.getMillisec() - begin.getMillisec(); + sint64 secondDiff = end.getSecond() - begin.getSecond(); + sint64 minuteDiff = end.getMinute() - begin.getMinute(); + sint64 hourDiff = end.getHour() - begin.getHour(); + + // compute the microsec offset needed to sync the time of t0 and t1 (may be negative) + sint64 totalMicrosec = ((((((hourDiff * 60) + minuteDiff) * 60) + secondDiff) * 1000) + milliDiff) * 1000 + microDiff; + bool okay = begin.add(0, 0, 0, hourDiff, minuteDiff, secondDiff, milliDiff, microDiff); + ASSERT(okay); + ASSERT((begin.value & 0x1fffffffff) == (end.value & 0x1fffffffff)); + ASSERT(begin.value <= end.value); + + // heuristic iterative algorithm for computing the days + if (begin.value != end.value) + { + sint64 totalDays = 0; + for (int i = 0; i < 10 && begin.value != end.value; ++i) + { + sint64 dayDiff = end.getDay() - begin.getDay(); + sint64 monthDiff = end.getMonth() - begin.getMonth(); + sint64 yearDiff = end.getYear() - begin.getYear(); + sint64 days = yearDiff * 365 + monthDiff * 28; // days may be negative + if (!days) + days = dayDiff; + totalDays += days; + okay = begin.add(0, 0, days); + ASSERT(okay); } + if (begin.value != end.value) + return UINT64_MAX; + ASSERT(totalDays >= 0); + totalMicrosec += totalDays * (24llu * 60llu * 60 * 1000 * 1000); } - // Add days in the current month - totalDays += day - 1; + ASSERT(totalMicrosec >= 0); + return totalMicrosec; + } + + /** + * Compute duration between this and dt in full days. Returns UINT64_MAX if dt or this is invalid. + */ + uint64 durationDays(const DateAndTime& dt) const + { + uint64 ret = durationMicrosec(dt); + if (ret != UINT64_MAX) + ret /= (24llu * 60llu * 60llu * 1000llu * 1000llu); + return ret; + } + + protected: + // condensed binary 8-byte representation supporting fast comparison: + // - padding/reserved: 2 bits (most significant bits in 8-byte number, bits 62-63) + // - year: 16 bits (bits 46-61) + // - month: 4 bits (bits 42-45) + // - day: 5 bits (bits 37-41) + // - hour: 5 bits (bits 32-36) + // - minute: 6 bits (bits 26-31) + // - second: 6 bits (bits 20-25) + // - millisecond: 10 bits (bits 10-19) + // - microsecondDuringMillisecond: 10 bits (lowest significance in 8-byte number, bits 0-9) + uint64 value; + + /// Adds valToAdd to valInAndOut and returns true if there is no overflow. Otherwise, returns false. + static bool addWithoutOverflow(sint64& valInAndOut, sint64 valToAdd) + { + sint64 sum = valInAndOut + valToAdd; + if (valInAndOut < 0 && valToAdd < 0 && sum > 0) // negative overflow + return false; + if (valInAndOut > 0 && valToAdd > 0 && sum < 0) // positive overflow + return false; + valInAndOut = sum; + return true; + } + + // Add of up to 2 values to low significance value (such as milliseconds) and split it + // into high significance carry (for example in seconds) and low significance value (milliseconds). + // All low and high values may be positive or negative. + static bool addAndComputeCarry(sint64& low, sint64& highCarryOut, sint64 lowToHighFactor, sint64 lowAdd1, sint64 lowAdd2 = 0) + { + if (!addWithoutOverflow(low, lowAdd1)) + return false; - // Convert total days and the time part to milliseconds - long long totalMs = totalDays * 86400000LL; // 24 * 60 * 60 * 1000 - totalMs += hour * 3600000LL; // 60 * 60 * 1000 - totalMs += minute * 60000LL; // 60 * 1000 - totalMs += second * 1000LL; - totalMs += millisecond; + if (lowAdd2 != 0 && !addWithoutOverflow(low, lowAdd2)) + return false; - return totalMs; + if (low == 0) + { + highCarryOut = 0; + } + else if (low > 0) + { + highCarryOut = low / lowToHighFactor; + low %= lowToHighFactor; + } + else // (low < 0) + { + highCarryOut = (low - lowToHighFactor + 1) / lowToHighFactor; + low = low - highCarryOut * lowToHighFactor; + ASSERT(low < lowToHighFactor); + } + return true; } }; @@ -883,17 +1342,31 @@ namespace QPI }; ////////// + // safety multiplying a and b and then clamp + + inline static sint64 smul(sint64 a, sint64 b); + inline static uint64 smul(uint64 a, uint64 b); + inline static sint32 smul(sint32 a, sint32 b); + inline static uint32 smul(uint32 a, uint32 b); + + ////////// + // safety adding a and b and then clamp + + inline static sint64 sadd(sint64 a, sint64 b); + inline static uint64 sadd(uint64 a, uint64 b); + inline static sint32 sadd(sint32 a, sint32 b); + inline static uint32 sadd(uint32 a, uint32 b); // Divide a by b, but return 0 if b is 0 (rounding to lower magnitude in case of integers) template - inline static T div(T a, T b) + inline static constexpr T div(T a, T b) { return b ? (a / b) : T(0); } // Return remainder of dividing a by b, but return 0 if b is 0 (requires modulo % operator) template - inline static T mod(T a, T b) + inline static constexpr T mod(T a, T b) { return b ? (a % b) : 0; } @@ -1133,7 +1606,7 @@ namespace QPI ////////// constexpr uint16 INVALID_PROPOSAL_INDEX = 0xffff; - constexpr uint32 INVALID_VOTER_INDEX = 0xffffffff; + constexpr uint32 INVALID_VOTE_INDEX = 0xffffffff; constexpr sint64 NO_VOTE_VALUE = 0x8000000000000000; // Single vote for all types of proposals defined in August 2024. @@ -1156,6 +1629,35 @@ namespace QPI }; static_assert(sizeof(ProposalSingleVoteDataV1) == 16, "Unexpected struct size."); + // For casting multiple votes for all types of proposals defined in August 2024. + // This makes sense for shareholder voting, where a single shareholder may own multiple shares, allowing to cast + // multiple votes. With this structs, the votes may be individually distributed to multiple options/values. + // Input data for contract procedure call, compatible with ProposalSingleVoteDataV1. That is, to cast all votes + // of a shareholder with the same value, just set element 0 of voteValues and leave/set the rest to zero (including + // the voteCounts). + struct ProposalMultiVoteDataV1 + { + // Index of proposal the vote is about (can be requested with proposal voting API) + uint16 proposalIndex; + + // Type of proposal, see ProposalTypes + uint16 proposalType; + + // Tick when proposal has been set (to make sure that proposal version known by the voter matches the current version). + uint32 proposalTick; + + // Value of vote. NO_VOTE_VALUE means no vote for every type. + // For proposals types with multiple options, 0 is no, 1 to N are the other options in order of definition in proposal. + // For scalar proposal types the value is passed directly. + Array voteValues; + + // Count of votes to cast for the corresponding voteValues. + // For compatibility with ProposalSingleVoteDataV1, voteCounts.get(0) == 0 means all votes of the voter. In + // the other elements, 0 means no votes for the given value. + Array voteCounts; + }; + static_assert(sizeof(ProposalMultiVoteDataV1) == 104, "Unexpected struct size."); + // Voting result summary for all types of proposals defined in August 2024. // Output data for contract function call for getting voting results. struct ProposalSummarizedVotingDataV1 @@ -1169,11 +1671,11 @@ namespace QPI // Tick when proposal has been set (useful for checking if cached ProposalData is still up to date). uint32 proposalTick; - // Number of voter who have the right to vote - uint32 authorizedVoters; + // Maximal number of votes (number of voters who have the right to vote if there aren't multiple votes per voter) + uint32 totalVotesAuthorized; // Number of total votes casted - uint32 totalVotes; + uint32 totalVotesCasted; // Voting results union @@ -1185,11 +1687,47 @@ namespace QPI sint64 scalarVotingResult; }; + // Return index of most voted option or -1 if this is scalar voting + sint32 getMostVotedOption() const + { + if (optionCount == 0) + return -1; + sint32 mostVotedOptionIndex = 0; + uint32 mostVotedOptionVotes = optionVoteCount.get(0); + for (sint32 optionIndex = 1; optionIndex < optionCount; ++optionIndex) + { + uint32 optionVotes = optionVoteCount.get(optionIndex); + if (mostVotedOptionVotes < optionVotes) + { + mostVotedOptionVotes = optionVotes; + mostVotedOptionIndex = optionIndex; + } + } + return mostVotedOptionIndex; + } + + // Return index of option accepted by quorum or -1 if none is accepted + sint32 getAcceptedOption(uint32 totalVotesThresh = QUORUM, uint32 mostVotedThreshold = QUORUM/2) const + { + if (totalVotesCasted >= totalVotesThresh) + { + sint32 opt = getMostVotedOption(); + if (opt >= 0 && optionVoteCount.get(opt) > mostVotedThreshold) + return opt; + } + return -1; + } + ProposalSummarizedVotingDataV1() = default; ProposalSummarizedVotingDataV1(const ProposalSummarizedVotingDataV1& src) { copyMemory(*this, src); } + ProposalSummarizedVotingDataV1& operator=(const ProposalSummarizedVotingDataV1& src) + { + copyMemory(*this, src); + return *this; + } }; static_assert(sizeof(ProposalSummarizedVotingDataV1) == 16 + 8*4, "Unexpected struct size."); @@ -1211,10 +1749,16 @@ namespace QPI // Propose to set variable to a value. Supported options: 2 <= N <= 5 with ProposalDataV1; N == 0 means scalar voting. static constexpr uint16 Variable = 0x200; + // Propose to set multiple variables. Supported options: 2 <= N <= 8 with ProposalDataV1 + static constexpr uint16 MultiVariables = 0x300; + // Propose to transfer amount to address in a specific epoch. Supported options: 1 with ProposalDataV1. static constexpr uint16 TransferInEpoch = 0x400; }; + // Invalid proposal type returned to encode error in some interfaces + static constexpr uint16 Invalid = 0; + // Options yes and no without extra data -> result is histogram of options static constexpr uint16 YesNo = Class::GeneralOptions | 2; @@ -1254,8 +1798,20 @@ namespace QPI // Set given variable to value, allowing to vote with scalar value, voting result is mean value static constexpr uint16 VariableScalarMean = Class::Variable | 0; + // TODO: support quorum value max / min as voting result + + // Set multiple variables with options yes/no (data stored by contract) -> result is histogram of options + static constexpr uint16 MultiVariablesYesNo = Class::MultiVariables | 2; + + // Set multiple variables with 3 options "no change" / "values A" / "values B" (data stored by contract) + // -> result is histogram of options + static constexpr uint16 MultiVariablesThreeOptions = Class::MultiVariables | 3; + + // Set multiple variables with 4 options "no change" / "values A" / "values B" / "values C" (data stored by + // contract) -> result is histogram of options + static constexpr uint16 MultiVariablesFourOptions = Class::MultiVariables | 4; - // Contruct type from class + number of options (no checking if type is valid) + // Construct type from class + number of options (no checking if type is valid) static constexpr uint16 type(uint16 cls, uint16 options) { return cls | options; @@ -1285,8 +1841,8 @@ namespace QPI struct ProposalDataV1 { // URL explaining proposal, zero-terminated string. - Array url; - + Array url; + // Epoch, when proposal is active. For setProposal(), 0 means to clear proposal and non-zero means the current epoch. uint16 epoch; @@ -1345,6 +1901,7 @@ namespace QPI switch (cls) { case ProposalTypes::Class::GeneralOptions: + case ProposalTypes::Class::MultiVariables: okay = options >= 2 && options <= 8; break; case ProposalTypes::Class::Transfer: @@ -1398,6 +1955,11 @@ namespace QPI { copyMemory(*this, src); } + ProposalDataV1& operator=(const ProposalDataV1& src) + { + copyMemory(*this, src); + return *this; + } }; static_assert(sizeof(ProposalDataV1) == 256 + 8 + 64, "Unexpected struct size."); @@ -1446,7 +2008,8 @@ namespace QPI switch (cls) { case ProposalTypes::Class::GeneralOptions: - okay = options >= 2 && options <= 3; + case ProposalTypes::Class::MultiVariables: + okay = options >= 2 && options <= 3; // 3 options can be encoded in the yes/no type of storage as well break; case ProposalTypes::Class::Transfer: okay = (options == 2 && !isZero(transfer.destination) && transfer.amount >= 0); @@ -1460,6 +2023,17 @@ namespace QPI // Whether to support scalar votes next to option votes. static constexpr bool supportScalarVotes = false; + + ProposalDataYesNo() = default; + ProposalDataYesNo(const ProposalDataYesNo& src) + { + copyMemory(*this, src); + } + ProposalDataYesNo& operator=(const ProposalDataYesNo& src) + { + copyMemory(*this, src); + return *this; + } }; static_assert(sizeof(ProposalDataYesNo) == 256 + 8 + 40, "Unexpected struct size."); @@ -1473,11 +2047,12 @@ namespace QPI template struct ProposalAndVotingByComputors; - // Option for ProposerAndVoterHandlingT in ProposalVoting that allows both voting for computors only and creating/chaning proposals for anyone. + // Option for ProposerAndVoterHandlingT in ProposalVoting that allows both voting for computors only and creating/changing proposals for anyone. template struct ProposalByAnyoneVotingByComputors; - template + // Option for ProposerAndVoterHandlingT in ProposalVoting that allows both voting and setting proposals for contract shareholders only. + template struct ProposalAndVotingByShareholders; template @@ -1500,17 +2075,17 @@ namespace QPI { public: static constexpr uint16 maxProposals = ProposerAndVoterHandlingT::maxProposals; - static constexpr uint32 maxVoters = ProposerAndVoterHandlingT::maxVoters; + static constexpr uint32 maxVotes = ProposerAndVoterHandlingT::maxVotes; typedef ProposerAndVoterHandlingT ProposerAndVoterHandlingType; typedef ProposalDataT ProposalDataType; typedef ProposalWithAllVoteData< ProposalDataT, - maxVoters + maxVotes > ProposalAndVotesDataType; static_assert(maxProposals <= INVALID_PROPOSAL_INDEX); - static_assert(maxVoters <= INVALID_VOTER_INDEX); + static_assert(maxVotes <= INVALID_VOTE_INDEX); // Handling of who has the right to propose and to vote + proposal / voter indices ProposerAndVoterHandlingType proposersAndVoters; @@ -1528,13 +2103,16 @@ namespace QPI template struct QpiContextProposalFunctionCall { - // Get proposal with given index if index is valid and proposal is set (epoch > 0) + // Get proposal with given index if index is valid and proposal is set (epoch > 0). On error returns false and sets proposal.type = 0. bool getProposal(uint16 proposalIndex, ProposalDataType& proposal) const; - // Get data of single vote - bool getVote(uint16 proposalIndex, uint32 voterIndex, ProposalSingleVoteDataV1& vote) const; + // Get data of single vote. On error returns false and sets vote.proposalType = 0. + bool getVote(uint16 proposalIndex, uint32 voteIndex, ProposalSingleVoteDataV1& vote) const; + + // Get data of votes of a given voter. On error returns false and sets votes.proposalType = 0. + bool getVotes(uint16 proposalIndex, const id& voter, ProposalMultiVoteDataV1& votes) const; - // Get summary of all votes casted + // Get summary of all votes casted. On error returns false and sets votingSummary.totalVotesAuthorized = 0. bool getVotingSummary(uint16 proposalIndex, ProposalSummarizedVotingDataV1& votingSummary) const; // Return index of existing proposal or INVALID_PROPOSAL_INDEX if there is no proposal by given proposer @@ -1543,11 +2121,19 @@ namespace QPI // Return proposer ID of given proposal index or NULL_ID if there is no proposal at this index id proposerId(uint16 proposalIndex) const; - // Return voter index for given ID or INVALID_VOTER_INDEX if ID has no right to vote - uint32 voterIndex(const id& voterId) const; + // Return vote index for given ID or INVALID_VOTE_INDEX if ID has no right to vote. If the voter has multiple + // votes, this returns the first index. All votes of a voter are stored consecutively. + // If voters are shareholders, proposalIndex must be passed. If voters are computors, proposalIndex is ignored. + uint32 voteIndex(const id& voterId, uint16 proposalIndex = 0) const; - // Return ID for given voter index or NULL_ID if index is invalid - id voterId(uint32 voterIndex) const; + // Return ID for given vote index or NULL_ID if index is invalid. + // If voters are shareholders, proposalIndex must be passed. If voters are computors, proposalIndex is ignored. + id voterId(uint32 voteIndex, uint16 proposalIndex = 0) const; + + // Return count of votes of a voter if his first vote index is passed. Otherwise return the number of votes + // including this and the following indices. Returns 0 if an invalid index is passed. + // If voters are shareholders, proposalIndex must be passed. If voters are computors, proposalIndex is ignored. + uint32 voteCount(uint32 voteIndex, uint16 proposalIndex = 0) const; // Return next proposal index of proposals of given epoch (default: current epoch) // or -1 if there are not any more such proposals behind the passed index. @@ -1596,6 +2182,7 @@ namespace QPI // Cast vote for proposal with index vote.proposalIndex if voter has right to vote, the proposal's epoch // is the current epoch, vote.proposalType and vote.proposalTick match the corresponding proposal's values, // and vote.voteValue is valid for the proposal type. + // If voter has multiple votes (possible in shareholder voting), cast all votes of voter with the same value. // This can be used to remove a previous vote by vote.voteValue = NO_VOTE_VALUE. // Return whether vote has been casted. bool vote( @@ -1603,6 +2190,21 @@ namespace QPI const ProposalSingleVoteDataV1& vote ); + // Cast votes for proposal with index votes.proposalIndex if voter has right to vote, the proposal's epoch + // is the current epoch, votes.proposalType and votes.proposalTick match the corresponding proposal's values, + // the votes.voteValues are valid for the proposal type, and the sum of votes.voteCounts does not exceed the + // number of votes available to the voter. + // If any vote value is invalid, all votes of the voter are set to NO_VOTE_VALUE. + // This can be used to remove previous votes by using a vote value of NO_VOTE_VALUE or a total vote count less + // than the number of votes available to the voter. + // For compatibility with ProposalSingleVoteDataV1, all votes are set with votes.voteValues.get(0) if the sum + // of votes.voteCounts is 0. + // Return whether the votes have been casted. + bool vote( + const id& voter, + const ProposalMultiVoteDataV1& votes + ); + // ProposalVoting type to work with typedef ProposalVoting ProposalVotingType; @@ -1768,6 +2370,18 @@ namespace QPI // return current datetime (year, month, day, hour, minute, second, millisec) inline DateAndTime now() const; + // return last spectrum digest on etalonTick + inline m256i getPrevSpectrumDigest() const; + + // return last universe digest on etalonTick + inline m256i getPrevUniverseDigest() const; + + // return last computer digest on etalonTick + inline m256i getPrevComputerDigest() const; + + // run the score function (in qubic mining) and return first 256 bit of output + inline m256i computeMiningFunction(const m256i miningSeed, const m256i publicKey, const m256i nonce) const; + inline bit signatureValidity( const id& entity, const id& digest, @@ -1780,6 +2394,10 @@ namespace QPI inline uint8 year( ) const; // [0..99] (0 = 2000, 1 = 2001, ..., 99 = 2099) + // Return the amount of Qu in the fee reserve for the specified contract. + // If the provided index is invalid (< 1 or >= contractCount) the currentContractIndex is used instead. + inline sint64 queryFeeReserve(uint32 contractIndex = 0) const; + // Access proposal functions with qpi(proposalVotingObject).func(). template inline QpiContextProposalFunctionCall operator()( @@ -1789,7 +2407,7 @@ namespace QPI // Internal functions, calling not allowed in contracts inline void* __qpiAllocLocals(unsigned int sizeOfLocals) const; inline void __qpiFreeLocals() const; - inline const QpiContextFunctionCall& __qpiConstructContextOtherContractFunctionCall(unsigned int otherContractIndex) const; + inline const QpiContextFunctionCall* __qpiConstructContextOtherContractFunctionCall(unsigned int otherContractIndex, InterContractCallError& callError) const; inline void __qpiFreeContext() const; inline void * __qpiAcquireStateForReading(unsigned int contractIndex) const; inline void __qpiReleaseStateForReading(unsigned int contractIndex) const; @@ -1813,9 +2431,13 @@ namespace QPI sint64 offeredTransferFee ) const; // Returns payed fee on success (>= 0), -requestedFee if offeredTransferFee or contract balance is not sufficient, INVALID_AMOUNT in case of other error. + // Burns Qus from the current contract's balance to fill the contract fee reserve of the contract specified via contractIndexBurnedFor. + // If the provided index is invalid (< 1 or >= contractCount), the Qus are burned for the currentContractIndex. + // Returns the remaining balance (>= 0) of the current contract if the burning is successful. A negative return value indicates failure. inline sint64 burn( - sint64 amount - ) const; + sint64 amount, + uint32 contractIndexBurnedFor = 0 + ) const; inline bool distributeDividends( // Attempts to pay dividends sint64 amountPerShare // Total amount will be 676x of this @@ -1849,6 +2471,34 @@ namespace QPI sint64 offeredTransferFee ) const; // Returns payed fee on success (>= 0), -requestedFee if offeredTransferFee or contract balance is not sufficient, INVALID_AMOUNT in case of other error. + /** + * @brief Add/change/cancel shareholder proposal as shareholder of another contract. + * @param contractIndex Index of the other contract, that SELF is shareholder of and that the proposal is about. + * @param proposalDataBuffer Buffer for passing the contract-dependent proposal data. You may use copyToBuffer() to fill it. + * @param invocationReward Invocation reward sent to contractIndex when invoking it's procedure. + * @return Proposal index on success, INVALID_PROPOSAL_INDEX on error. + * @note Invokes SET_SHAREHOLDER_PROPOSAL of contractIndex without checking shareholder status and proposalDataBuffer. + */ + inline uint16 setShareholderProposal( + uint16 contractIndex, + const Array& proposalDataBuffer, + sint64 invocationReward + ) const; + + /** + * @brief Add/change/cancel shareholder vote(s) in another contract. + * @param contractIndex Index of the other contract, that SELF is shareholder of and that the proposal is about. + * @param shareholderVoteData Vote(s) to cast. See ProposalMultiVoteDataV1 for details. + * @param invocationReward Invocation reward sent to contractIndex when invoking it's procedure. + * @return Proposal index on success, INVALID_PROPOSAL_INDEX on error. + * @note Invokes SET_SHAREHOLDER_VOTES of contractIndex without checking shareholder status and shareholderVoteData. + */ + inline bool setShareholderVotes( + uint16 contractIndex, + const ProposalMultiVoteDataV1& shareholderVoteData, + sint64 invocationReward + ) const; + inline sint64 transfer( // Attempts to transfer energy from this qubic const id& destination, // Destination to transfer to, use NULL_ID to destroy the transferred energy sint64 amount // Energy amount to transfer, must be in [0..1'000'000'000'000'000] range @@ -1871,13 +2521,20 @@ namespace QPI // Internal functions, calling not allowed in contracts - inline const QpiContextProcedureCall& __qpiConstructProcedureCallContext(unsigned int otherContractIndex, sint64 invocationReward) const; + inline const QpiContextProcedureCall* __qpiConstructProcedureCallContext(unsigned int otherContractIndex, sint64 invocationReward, InterContractCallError& callError, bool skipFeeCheck = false) const; inline void* __qpiAcquireStateForWriting(unsigned int contractIndex) const; inline void __qpiReleaseStateForWriting(unsigned int contractIndex) const; template - void __qpiCallSystemProc(unsigned int otherContractIndex, InputType& input, OutputType& output, sint64 invocationReward) const; + bool __qpiCallSystemProc(unsigned int otherContractIndex, InputType& input, OutputType& output, sint64 invocationReward) const; inline void __qpiNotifyPostIncomingTransfer(const id& source, const id& dest, sint64 amount, uint8 type) const; + // Internal version of transfer() that takes the TransferType as additional argument. + inline sint64 __transfer( // Attempts to transfer energy from this qubic + const id& destination, // Destination to transfer to, use NULL_ID to destroy the transferred energy + sint64 amount, // Energy amount to transfer, must be in [0..1'000'000'000'000'000] range + uint8 transferType // the type of transfer + ) const; // Returns remaining energy amount; if the value is less than 0 then the attempt has failed, in this case the absolute value equals to the insufficient amount + protected: // Construction is done in core, not allowed in contracts QpiContextProcedureCall(unsigned int contractIndex, const m256i& originator, long long invocationReward, unsigned char entryPoint) : QpiContextFunctionCall(contractIndex, originator, invocationReward, entryPoint) {} @@ -1933,6 +2590,7 @@ namespace QPI constexpr uint8 qpiDistributeDividends = 3; constexpr uint8 revenueDonation = 4; constexpr uint8 ipoBidRefund = 5; + constexpr uint8 procedureInvocationByOtherContract = 6; }; // Input of POST_INCOMING_TRANSFER notification system call @@ -1943,6 +2601,18 @@ namespace QPI uint8 type; }; + // Input of SET_SHAREHOLDER_PROPOSAL system procedure (buffer for passing the contract-dependent proposal data) + typedef Array SET_SHAREHOLDER_PROPOSAL_input; + + // Output of SET_SHAREHOLDER_PROPOSAL system procedure (proposal index, or INVALID_PROPOSAL_INDEX on error) + typedef uint16 SET_SHAREHOLDER_PROPOSAL_output; + + // Input of SET_SHAREHOLDER_VOTES system procedure (vote data) + typedef ProposalMultiVoteDataV1 SET_SHAREHOLDER_VOTES_input; + + // Output of SET_SHAREHOLDER_VOTES system procedure (success flag) + typedef bit SET_SHAREHOLDER_VOTES_output; + ////////// struct ContractBase @@ -1967,6 +2637,10 @@ namespace QPI static void __postReleaseShares(const QpiContextProcedureCall&, void*, void*, void*) {} enum { __postIncomingTransferEmpty = 1, __postIncomingTransferLocalsSize = sizeof(NoData) }; static void __postIncomingTransfer(const QpiContextProcedureCall&, void*, void*, void*) {} + enum { __setShareholderProposalEmpty = 1, __setShareholderProposalLocalsSize = sizeof(NoData) }; + static void __setShareholderProposal(const QpiContextProcedureCall&, void*, void*, void*) {} + enum { __setShareholderVotesEmpty = 1, __setShareholderVotesLocalsSize = sizeof(NoData) }; + static void __setShareholderVotes(const QpiContextProcedureCall&, void*, void*, void*) {} enum { __acceptOracleTrueReplyEmpty = 1, __acceptOracleTrueReplyLocalsSize = sizeof(NoData) }; static void __acceptOracleTrueReply(const QpiContextProcedureCall&, void*, void*, void*) {} enum { __acceptOracleFalseReplyEmpty = 1, __acceptOracleFalseReplyLocalsSize = sizeof(NoData) }; @@ -2082,6 +2756,31 @@ namespace QPI NO_IO_SYSTEM_PROC_WITH_LOCALS(POST_INCOMING_TRANSFER, __postIncomingTransfer, PostIncomingTransfer_input, \ NoData) + // Define contract system procedure called when another contract tries to set/change/cancel a proposal through + // qpi.setShareholderProposal(). See `doc/contracts.md` for details. + #define SET_SHAREHOLDER_PROPOSAL() \ + NO_IO_SYSTEM_PROC(SET_SHAREHOLDER_PROPOSAL, __setShareholderProposal, SET_SHAREHOLDER_PROPOSAL_input, \ + SET_SHAREHOLDER_PROPOSAL_output) + + // Define contract system procedure called when another contract tries to set/change/cancel a proposal through + // qpi.setShareholderProposal(). Provides zeroed instance of SET_SHAREHOLDER_PROPOSAL_locals struct. See + // `doc/contracts.md` for details. + #define SET_SHAREHOLDER_PROPOSAL_WITH_LOCALS() \ + NO_IO_SYSTEM_PROC_WITH_LOCALS(SET_SHAREHOLDER_PROPOSAL, __setShareholderProposal, SET_SHAREHOLDER_PROPOSAL_input, \ + SET_SHAREHOLDER_PROPOSAL_output) + + // Define contract system procedure called when another contract tries to set/change/cancel a vote through + // qpi.setShareholderVotes(). See `doc/contracts.md` for details. + #define SET_SHAREHOLDER_VOTES() \ + NO_IO_SYSTEM_PROC(SET_SHAREHOLDER_VOTES, __setShareholderVotes, SET_SHAREHOLDER_VOTES_input, \ + SET_SHAREHOLDER_VOTES_output) + + // Define contract system procedure called when another contract tries to set/change/cancel a vote through + // qpi.setShareholderVotes(). Provides zeroed instance of SET_SHAREHOLDER_VOTES_locals struct. See + // `doc/contracts.md` for details. + #define SET_SHAREHOLDER_VOTES_WITH_LOCALS() \ + NO_IO_SYSTEM_PROC_WITH_LOCALS(SET_SHAREHOLDER_VOTES, __setShareholderVotes, SET_SHAREHOLDER_VOTES_input, \ + SET_SHAREHOLDER_VOTES_output) #define EXPAND() \ public: \ @@ -2097,6 +2796,10 @@ namespace QPI #define LOG_WARNING(message) __logContractWarningMessage(CONTRACT_INDEX, message); + #define LOG_PAUSE() __pauseLogMessage(); + + #define LOG_RESUME() __resumeLogMessage(); + #define PRIVATE_FUNCTION(function) \ private: \ typedef QPI::NoData function##_locals; \ @@ -2174,41 +2877,197 @@ namespace QPI // WARNING: input may be changed by called function // TODO: INVOKE - // Call function of other contract + // Call function of other contract with custom error variable name + // Use this variant when making multiple inter-contract calls in the same scope // WARNING: input may be changed by called function - #define CALL_OTHER_CONTRACT_FUNCTION(contractStateType, function, input, output) \ + #define CALL_OTHER_CONTRACT_FUNCTION_E(contractStateType, function, input, output, errorVar) \ static_assert(sizeof(contractStateType::function##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #function "_locals size too large"); \ - static_assert(contractStateType::__is_function_##function, "CALL_OTHER_CONTRACT_FUNCTION() cannot be used to invoke procedures."); \ + static_assert(contractStateType::__is_function_##function, "CALL_OTHER_CONTRACT_FUNCTION_E() cannot be used to invoke procedures."); \ static_assert(!(contractStateType::__contract_index == CONTRACT_STATE_TYPE::__contract_index), "Use CALL() to call a function of this contract."); \ static_assert(contractStateType::__contract_index < CONTRACT_STATE_TYPE::__contract_index, "You can only call contracts with lower index."); \ - contractStateType::function( \ - qpi.__qpiConstructContextOtherContractFunctionCall(contractStateType::__contract_index), \ - *(contractStateType*)qpi.__qpiAcquireStateForReading(contractStateType::__contract_index), \ - input, output, \ - *(contractStateType::function##_locals*)qpi.__qpiAllocLocals(sizeof(contractStateType::function##_locals))); \ - qpi.__qpiFreeContext(); \ - qpi.__qpiReleaseStateForReading(contractStateType::__contract_index); \ - qpi.__qpiFreeLocals() + InterContractCallError errorVar; \ + do { \ + const QpiContextFunctionCall* __ctx = qpi.__qpiConstructContextOtherContractFunctionCall(contractStateType::__contract_index, errorVar); \ + if (__ctx) { \ + contractStateType* __state = (contractStateType*)qpi.__qpiAcquireStateForReading(contractStateType::__contract_index); \ + contractStateType::function##_locals* __locals = (contractStateType::function##_locals*)qpi.__qpiAllocLocals(sizeof(contractStateType::function##_locals)); \ + contractStateType::function(*__ctx, *__state, input, output, *__locals); \ + qpi.__qpiFreeLocals(); \ + qpi.__qpiReleaseStateForReading(contractStateType::__contract_index); \ + qpi.__qpiFreeContext(); \ + } \ + } while(0) - // Transfer invocation reward and invoke of other contract (procedure only) + // Call function of other contract // WARNING: input may be changed by called function - #define INVOKE_OTHER_CONTRACT_PROCEDURE(contractStateType, procedure, input, output, invocationReward) \ + #define CALL_OTHER_CONTRACT_FUNCTION(contractStateType, function, input, output) \ + CALL_OTHER_CONTRACT_FUNCTION_E(contractStateType, function, input, output, interContractCallError) + + // Transfer invocation reward and invoke of other contract (procedure only) with custom error variable name + // Use this variant when making multiple inter-contract calls in the same scope + // WARNING: input may be changed by called function + #define INVOKE_OTHER_CONTRACT_PROCEDURE_E(contractStateType, procedure, input, output, invocationReward, errorVar) \ static_assert(sizeof(contractStateType::procedure##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #procedure "_locals size too large"); \ - static_assert(!contractStateType::__is_function_##procedure, "INVOKE_OTHER_CONTRACT_PROCEDURE() cannot be used to call functions."); \ + static_assert(!contractStateType::__is_function_##procedure, "INVOKE_OTHER_CONTRACT_PROCEDURE_E() cannot be used to call functions."); \ static_assert(!(contractStateType::__contract_index == CONTRACT_STATE_TYPE::__contract_index), "Use CALL() to call a function/procedure of this contract."); \ static_assert(contractStateType::__contract_index < CONTRACT_STATE_TYPE::__contract_index, "You can only call contracts with lower index."); \ - contractStateType::procedure( \ - qpi.__qpiConstructProcedureCallContext(contractStateType::__contract_index, invocationReward), \ - *(contractStateType*)qpi.__qpiAcquireStateForWriting(contractStateType::__contract_index), \ - input, output, \ - *(contractStateType::procedure##_locals*)qpi.__qpiAllocLocals(sizeof(contractStateType::procedure##_locals))); \ - qpi.__qpiFreeContext(); \ - qpi.__qpiReleaseStateForWriting(contractStateType::__contract_index); \ - qpi.__qpiFreeLocals() + InterContractCallError errorVar; \ + do { \ + const QpiContextProcedureCall* __ctx = qpi.__qpiConstructProcedureCallContext(contractStateType::__contract_index, invocationReward, errorVar); \ + if (__ctx) { \ + contractStateType* __state = (contractStateType*)qpi.__qpiAcquireStateForWriting(contractStateType::__contract_index); \ + contractStateType::procedure##_locals* __locals = (contractStateType::procedure##_locals*)qpi.__qpiAllocLocals(sizeof(contractStateType::procedure##_locals)); \ + contractStateType::procedure(*__ctx, *__state, input, output, *__locals); \ + qpi.__qpiFreeLocals(); \ + qpi.__qpiReleaseStateForWriting(contractStateType::__contract_index); \ + qpi.__qpiFreeContext(); \ + } \ + } while(0) + + // Transfer invocation reward and invoke of other contract (procedure only) + // WARNING: input may be changed by called function + #define INVOKE_OTHER_CONTRACT_PROCEDURE(contractStateType, procedure, input, output, invocationReward) \ + INVOKE_OTHER_CONTRACT_PROCEDURE_E(contractStateType, procedure, input, output, invocationReward, interContractCallError) #define QUERY_ORACLE(oracle, query) // TODO #define SELF id(CONTRACT_INDEX, 0, 0, 0) #define SELF_INDEX CONTRACT_INDEX + + ////////// + + #define DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(numProposalSlots, assetNameInt64) \ + public: \ + typedef ProposalDataYesNo ProposalDataT; \ + typedef ProposalAndVotingByShareholders ProposersAndVotersT; \ + typedef ProposalVoting ProposalVotingT; \ + protected: \ + ProposalVotingT proposals + + #define IMPLEMENT_SetShareholderProposal(numFeeStateVariables, setProposalFeeVarOrValue) \ + typedef ProposalDataT SetShareholderProposal_input; \ + typedef uint16 SetShareholderProposal_output; \ + PUBLIC_PROCEDURE(SetShareholderProposal) { \ + if (qpi.invocationReward() < setProposalFeeVarOrValue || (input.epoch \ + && (input.type != ProposalTypes::VariableYesNo || input.variableOptions.variable >= numFeeStateVariables \ + || input.variableOptions.value < 0))) { \ + qpi.transfer(qpi.invocator(), qpi.invocationReward()); \ + output = INVALID_PROPOSAL_INDEX; \ + return; } \ + output = qpi(state.proposals).setProposal(qpi.invocator(), input); \ + if (output == INVALID_PROPOSAL_INDEX) { \ + qpi.transfer(qpi.invocator(), qpi.invocationReward()); \ + return; } \ + qpi.burn(setProposalFeeVarOrValue); \ + if (qpi.invocationReward() > setProposalFeeVarOrValue) { \ + qpi.transfer(qpi.invocator(), qpi.invocationReward() - setProposalFeeVarOrValue); } } + + #define IMPLEMENT_GetShareholderProposal() \ + struct GetShareholderProposal_input { uint16 proposalIndex; }; \ + struct GetShareholderProposal_output { ProposalDataT proposal; id proposerPubicKey; }; \ + PUBLIC_FUNCTION(GetShareholderProposal) { \ + output.proposerPubicKey = qpi(state.proposals).proposerId(input.proposalIndex); \ + qpi(state.proposals).getProposal(input.proposalIndex, output.proposal); } + + #define IMPLEMENT_GetShareholderProposalIndices() \ + struct GetShareholderProposalIndices_input { bit activeProposals; sint32 prevProposalIndex; }; \ + struct GetShareholderProposalIndices_output { uint16 numOfIndices; Array indices; }; \ + PUBLIC_FUNCTION(GetShareholderProposalIndices) {\ + if (input.activeProposals) { \ + while ((input.prevProposalIndex = qpi(state.proposals).nextProposalIndex(input.prevProposalIndex, qpi.epoch())) >= 0) { \ + output.indices.set(output.numOfIndices, input.prevProposalIndex); \ + ++output.numOfIndices; \ + if (output.numOfIndices == output.indices.capacity()) break; } } \ + else { \ + while ((input.prevProposalIndex = qpi(state.proposals).nextFinishedProposalIndex(input.prevProposalIndex)) >= 0) { \ + output.indices.set(output.numOfIndices, input.prevProposalIndex); \ + ++output.numOfIndices; \ + if (output.numOfIndices == output.indices.capacity()) break; } } } + + #define IMPLEMENT_GetShareholderProposalFees(setProposalFeeVarOrValue) \ + typedef NoData GetShareholderProposalFees_input; \ + struct GetShareholderProposalFees_output { sint64 setProposalFee; sint64 setVoteFee; }; \ + PUBLIC_FUNCTION(GetShareholderProposalFees) { \ + output.setProposalFee = setProposalFeeVarOrValue; \ + output.setVoteFee = 0; } + + #define IMPLEMENT_SetShareholderVotes() \ + typedef ProposalMultiVoteDataV1 SetShareholderVotes_input; \ + typedef bit SetShareholderVotes_output; \ + PUBLIC_PROCEDURE(SetShareholderVotes) { \ + output = qpi(state.proposals).vote(qpi.invocator(), input); } \ + + #define IMPLEMENT_GetShareholderVotes() \ + struct GetShareholderVotes_input { id voter; uint16 proposalIndex; }; \ + typedef ProposalMultiVoteDataV1 GetShareholderVotes_output; \ + PUBLIC_FUNCTION(GetShareholderVotes) { \ + qpi(state.proposals).getVotes(input.proposalIndex, input.voter, output); } + + #define IMPLEMENT_GetShareholderVotingResults() \ + struct GetShareholderVotingResults_input { uint16 proposalIndex; }; \ + typedef ProposalSummarizedVotingDataV1 GetShareholderVotingResults_output; \ + PUBLIC_FUNCTION(GetShareholderVotingResults) { \ + qpi(state.proposals).getVotingSummary(input.proposalIndex, output); } + + #define IMPLEMENT_SET_SHAREHOLDER_PROPOSAL() \ + struct SET_SHAREHOLDER_PROPOSAL_locals { SetShareholderProposal_input userProcInput; }; \ + SET_SHAREHOLDER_PROPOSAL_WITH_LOCALS() { \ + copyFromBuffer(locals.userProcInput, input); \ + CALL(SetShareholderProposal, locals.userProcInput, output); } + + #define IMPLEMENT_SET_SHAREHOLDER_VOTES() \ + SET_SHAREHOLDER_VOTES() { \ + CALL(SetShareholderVotes, input, output); } + + // Define procedures for easily implementing END_EPOCH + #define IMPLEMENT_FinalizeShareholderStateVarProposals() \ + struct FinalizeShareholderProposalSetStateVar_input { \ + sint32 proposalIndex; ProposalDataT proposal; ProposalSummarizedVotingDataV1 results; \ + sint32 acceptedOption; sint64 acceptedValue; }; \ + typedef NoData FinalizeShareholderProposalSetStateVar_output; \ + typedef NoData FinalizeShareholderStateVarProposals_input; \ + typedef NoData FinalizeShareholderStateVarProposals_output; \ + struct FinalizeShareholderStateVarProposals_locals { \ + FinalizeShareholderProposalSetStateVar_input p; uint16 proposalClass; }; \ + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeShareholderStateVarProposals) { \ + locals.p.proposalIndex = -1; \ + while ((locals.p.proposalIndex = qpi(state.proposals).nextProposalIndex(locals.p.proposalIndex, qpi.epoch())) >= 0) { \ + if (!qpi(state.proposals).getProposal(locals.p.proposalIndex, locals.p.proposal)) \ + continue; \ + locals.proposalClass = ProposalTypes::cls(locals.p.proposal.type); \ + if (locals.proposalClass == ProposalTypes::Class::Variable || locals.proposalClass == ProposalTypes::Class::MultiVariables) { \ + if (!qpi(state.proposals).getVotingSummary(locals.p.proposalIndex, locals.p.results)) \ + continue; \ + locals.p.acceptedOption = locals.p.results.getAcceptedOption(); \ + if (locals.p.acceptedOption <= 0) \ + continue; \ + locals.p.acceptedValue = locals.p.proposal.variableOptions.value; \ + CALL(FinalizeShareholderProposalSetStateVar, locals.p, output); } } } \ + PRIVATE_PROCEDURE(FinalizeShareholderProposalSetStateVar) + + #define IMPLEMENT_DEFAULT_SHAREHOLDER_PROPOSAL_VOTING(numFeeStateVariables, setProposalFeeVarOrValue) \ + IMPLEMENT_SetShareholderProposal(numFeeStateVariables, setProposalFeeVarOrValue) \ + IMPLEMENT_GetShareholderProposal() \ + IMPLEMENT_GetShareholderProposalIndices() \ + IMPLEMENT_GetShareholderProposalFees(setProposalFeeVarOrValue) \ + IMPLEMENT_SetShareholderVotes() \ + IMPLEMENT_GetShareholderVotes() \ + IMPLEMENT_GetShareholderVotingResults() \ + IMPLEMENT_SET_SHAREHOLDER_PROPOSAL() \ + IMPLEMENT_SET_SHAREHOLDER_VOTES() + + #define REGISTER_GetShareholderProposalFees() REGISTER_USER_FUNCTION(GetShareholderProposalFees, 65531) + #define REGISTER_GetShareholderProposalIndices() REGISTER_USER_FUNCTION(GetShareholderProposalIndices, 65532) + #define REGISTER_GetShareholderProposal() REGISTER_USER_FUNCTION(GetShareholderProposal, 65533) + #define REGISTER_GetShareholderVotes() REGISTER_USER_FUNCTION(GetShareholderVotes, 65534) + #define REGISTER_GetShareholderVotingResults() REGISTER_USER_FUNCTION(GetShareholderVotingResults, 65535) + #define REGISTER_SetShareholderProposal() REGISTER_USER_PROCEDURE(SetShareholderProposal, 65534) + #define REGISTER_SetShareholderVotes() REGISTER_USER_PROCEDURE(SetShareholderVotes, 65535) + + #define REGISTER_SHAREHOLDER_PROPOSAL_VOTING() REGISTER_GetShareholderProposalFees() \ + REGISTER_GetShareholderProposalIndices(); REGISTER_GetShareholderProposal(); \ + REGISTER_GetShareholderVotes(); REGISTER_GetShareholderVotingResults(); \ + REGISTER_SetShareholderProposal(); REGISTER_SetShareholderVotes() + } diff --git a/src/four_q.h b/src/four_q.h index dcfffd6d3..3e65680ac 100644 --- a/src/four_q.h +++ b/src/four_q.h @@ -644,27 +644,49 @@ static void table_lookup_fixed_base(point_precomp_t P, unsigned int digit, unsig static void multiply(const unsigned long long* a, const unsigned long long* b, unsigned long long* c) { - unsigned long long u, v, uv; + unsigned long long u, v, uv, tmp; + // The intended operation is: _addcarry_u64(0, _umul128(a[0], b[1], &uv), u, &c[1]) + uv. + // However, MSVC (VC2022 17.14 specifically) does not strictly preserve left-to-right evaluation order. + // A temporary variable is introduced to ensure that 'uv' is _umul128 before addition. + // The same behavior are applied for all following code c[0] = _umul128(a[0], b[0], &u); - u = _addcarry_u64(0, _umul128(a[0], b[1], &uv), u, &c[1]) + uv; - u = _addcarry_u64(0, _umul128(a[0], b[2], &uv), u, &c[2]) + uv; - c[4] = _addcarry_u64(0, _umul128(a[0], b[3], &uv), u, &c[3]) + uv; - - u = _addcarry_u64(0, c[1], _umul128(a[1], b[0], &uv), &c[1]) + uv; - u = _addcarry_u64(0, _umul128(a[1], b[1], &uv), u, &v) + uv; - u = _addcarry_u64(_addcarry_u64(0, c[2], v, &c[2]), _umul128(a[1], b[2], &uv), u, &v) + uv; - c[5] = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), _umul128(a[1], b[3], &uv), u, &v) + uv + _addcarry_u64(0, c[4], v, &c[4]); - - u = _addcarry_u64(0, c[2], _umul128(a[2], b[0], &uv), &c[2]) + uv; - u = _addcarry_u64(0, _umul128(a[2], b[1], &uv), u, &v) + uv; - u = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), _umul128(a[2], b[2], &uv), u, &v) + uv; - c[6] = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), _umul128(a[2], b[3], &uv), u, &v) + uv + _addcarry_u64(0, c[5], v, &c[5]); - - u = _addcarry_u64(0, c[3], _umul128(a[3], b[0], &uv), &c[3]) + uv; - u = _addcarry_u64(0, _umul128(a[3], b[1], &uv), u, &v) + uv; - u = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), _umul128(a[3], b[2], &uv), u, &v) + uv; - c[7] = _addcarry_u64(_addcarry_u64(0, c[5], v, &c[5]), _umul128(a[3], b[3], &uv), u, &v) + uv + _addcarry_u64(0, c[6], v, &c[6]); + tmp = _umul128(a[0], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &c[1]) + uv; + tmp = _umul128(a[0], b[2], &uv); + u = _addcarry_u64(0, tmp, u, &c[2]) + uv; + tmp = _umul128(a[0], b[3], &uv); + c[4] = _addcarry_u64(0, tmp, u, &c[3]) + uv; + + tmp = _umul128(a[1], b[0], &uv); + u = _addcarry_u64(0, c[1], tmp, &c[1]) + uv; + tmp = _umul128(a[1], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; + tmp = _umul128(a[1], b[2], &uv); + u = _addcarry_u64(_addcarry_u64(0, c[2], v, &c[2]), tmp, u, &v) + uv; + tmp = _umul128(a[1], b[3], &uv); + tmp = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), tmp, u, &v); + c[5] = tmp + uv + _addcarry_u64(0, c[4], v, &c[4]); + + tmp = _umul128(a[2], b[0], &uv); + u = _addcarry_u64(0, c[2], tmp, &c[2]) + uv; + tmp = _umul128(a[2], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; + tmp = _umul128(a[2], b[2], &uv); + u = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), tmp, u, &v) + uv; + tmp = _umul128(a[2], b[3], &uv); + tmp = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), tmp, u, &v); + c[6] = tmp + uv + _addcarry_u64(0, c[5], v, &c[5]); + + tmp = _umul128(a[3], b[0], &uv); + u = _addcarry_u64(0, c[3], tmp, &c[3]) + uv; + tmp = _umul128(a[3], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; + tmp = _umul128(a[3], b[2], &uv); + u = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), tmp, u, &v) + uv; + tmp = _umul128(a[3], b[3], &uv); + tmp = _addcarry_u64(_addcarry_u64(0, c[5], v, &c[5]), tmp, u, &v); + c[7] = tmp + uv + _addcarry_u64(0, c[6], v, &c[6]); } static void Montgomery_multiply_mod_order(const unsigned long long* ma, const unsigned long long* mb, unsigned long long* mc) @@ -683,16 +705,21 @@ static void Montgomery_multiply_mod_order(const unsigned long long* ma, const un multiply(ma, mb, P); // P = ma * mb } - unsigned long long u, v, uv; + unsigned long long u, v, uv, tmp; Q[0] = _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_0, &u); - u = _addcarry_u64(0, _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_1, &uv), u, &Q[1]) + uv; - u = _addcarry_u64(0, _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_2, &uv), u, &Q[2]) + uv; + tmp = _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_1, &uv); + u = _addcarry_u64(0, tmp, u, &Q[1]) + uv; + tmp = _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_2, &uv); + u = _addcarry_u64(0, tmp, u, &Q[2]) + uv; _addcarry_u64(0, P[0] * MONTGOMERY_SMALL_R_PRIME_3, u, &Q[3]); - u = _addcarry_u64(0, Q[1], _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_0, &uv), &Q[1]) + uv; - u = _addcarry_u64(0, _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_1, &uv), u, &v) + uv; + tmp = _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_0, &uv); + u = _addcarry_u64(0, Q[1], tmp, &Q[1]) + uv; + tmp = _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_1, &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; _addcarry_u64(_addcarry_u64(0, Q[2], v, &Q[2]), P[1] * MONTGOMERY_SMALL_R_PRIME_2, u, &v); _addcarry_u64(0, Q[3], v, &Q[3]); - u = _addcarry_u64(0, Q[2], _umul128(P[2], MONTGOMERY_SMALL_R_PRIME_0, &uv), &Q[2]) + uv; + tmp = _umul128(P[2], MONTGOMERY_SMALL_R_PRIME_0, &uv); + u = _addcarry_u64(0, Q[2], tmp, &Q[2]) + uv; _addcarry_u64(0, P[2] * MONTGOMERY_SMALL_R_PRIME_1, u, &v); _addcarry_u64(0, Q[3], v, &Q[3]); _addcarry_u64(0, Q[3], P[3] * MONTGOMERY_SMALL_R_PRIME_0, &Q[3]); diff --git a/src/logging/logging.h b/src/logging/logging.h index c200a0ad1..c0ed6f6d7 100644 --- a/src/logging/logging.h +++ b/src/logging/logging.h @@ -56,8 +56,13 @@ struct Peer; #define SPECTRUM_STATS 10 #define ASSET_OWNERSHIP_MANAGING_CONTRACT_CHANGE 11 #define ASSET_POSSESSION_MANAGING_CONTRACT_CHANGE 12 +#define CONTRACT_RESERVE_DEDUCTION 13 #define CUSTOM_MESSAGE 255 +#define CUSTOM_MESSAGE_OP_START_DISTRIBUTE_DIVIDENDS 6217575821008262227ULL // STA_DDIV +#define CUSTOM_MESSAGE_OP_END_DISTRIBUTE_DIVIDENDS 6217575821008457285ULL //END_DDIV +#define CUSTOM_MESSAGE_OP_START_EPOCH 4850183582582395987ULL // STA_EPOC +#define CUSTOM_MESSAGE_OP_END_EPOCH 4850183582582591045ULL //END_EPOC /* * STRUCTS FOR LOGGING */ @@ -73,6 +78,7 @@ struct AssetIssuance { m256i issuerPublicKey; long long numberOfShares; + long long managingContractIndex; char name[7]; char numberOfDecimalPlaces; char unitOfMeasurement[7]; @@ -86,6 +92,7 @@ struct AssetOwnershipChange m256i destinationPublicKey; m256i issuerPublicKey; long long numberOfShares; + long long managingContractIndex; char name[7]; char numberOfDecimalPlaces; char unitOfMeasurement[7]; @@ -99,6 +106,7 @@ struct AssetPossessionChange m256i destinationPublicKey; m256i issuerPublicKey; long long numberOfShares; + long long managingContractIndex; char name[7]; char numberOfDecimalPlaces; char unitOfMeasurement[7]; @@ -184,6 +192,7 @@ struct Burning { m256i sourcePublicKey; long long amount; + unsigned int contractIndexBurnedFor; char _terminator; // Only data before "_terminator" are logged }; @@ -223,15 +232,18 @@ struct SpectrumStats unsigned int entityCategoryPopulations[48]; }; +struct ContractReserveDeduction +{ + unsigned long long deductedAmount; + long long remainingAmount; + unsigned int contractIndex; +}; + /* * LOGGING IMPLEMENTATION + * For definition of virtual memory sizes, see private_settings.h */ -#define LOG_BUFFER_PAGE_SIZE 300000000ULL -#define PMAP_LOG_PAGE_SIZE 30000000ULL -#define IMAP_LOG_PAGE_SIZE 10000ULL -#define VM_NUM_CACHE_PAGE 8 - // Virtual memory with 100'000'000 items per page and 4 pages on cache #ifdef NO_UEFI #define TEXT_LOGS_AS_NUMBER 0 #define TEXT_PMAP_AS_NUMBER 0 @@ -279,6 +291,7 @@ class qLogger inline static unsigned int lastUpdatedTick; // tick number that the system has generated all log inline static unsigned int currentTxId; inline static unsigned int currentTick; + inline static bool isPausing; static unsigned long long getLogId(const char* ptr) { @@ -324,6 +337,7 @@ class qLogger static void logMessage(unsigned int messageSize, unsigned char messageType, const void* message) { #if ENABLED_LOGGING + if (isPausing) return; char buffer[LOG_HEADER_SIZE]; tx.addLogId(); logBuf.set(logId, logBufferTail, LOG_HEADER_SIZE + messageSize); @@ -573,15 +587,18 @@ class qLogger m256i zeroHash = m256i::zero(); XKCP::KangarooTwelve_Update(&k12, zeroHash.m256i_u8, 32); // init tick, feed zero hash #endif + isPausing = false; #endif } + // updateTick is called right after _tick is processed static void updateTick(unsigned int _tick) { #if ENABLED_LOGGING ASSERT((_tick == lastUpdatedTick + 1) || (_tick == tickBegin)); + ASSERT(_tick >= tickBegin); #if LOG_STATE_DIGEST - unsigned long long index = lastUpdatedTick - tickBegin; + unsigned long long index = _tick - tickBegin; XKCP::KangarooTwelve_Final(&k12, digests[index].m256i_u8, (const unsigned char*)"", 0); XKCP::KangarooTwelve_Initialize(&k12, 128, 32); // init new k12 XKCP::KangarooTwelve_Update(&k12, digests[index].m256i_u8, 32); // feed the prev hash back to this @@ -589,6 +606,7 @@ class qLogger tx.commitAndCleanCurrentTxToLogId(); ASSERT(mapTxToLogId.size() == (_tick - tickBegin + 1)); lastUpdatedTick = _tick; + isPausing = false; #endif } @@ -837,6 +855,13 @@ class qLogger #endif } + void logContractReserveDeduction(const ContractReserveDeduction& message) + { +#if LOG_SPECTRUM + logMessage(sizeof(ContractReserveDeduction), CONTRACT_RESERVE_DEDUCTION, &message); +#endif + } + template void logCustomMessage(T message) { @@ -847,6 +872,16 @@ class qLogger #endif } + void pause() + { + isPausing = true; + } + + void resume() + { + isPausing = false; + } + // get logging content from log ID static void processRequestLog(unsigned long long processorNumber, Peer* peer, RequestResponseHeader* header); @@ -885,4 +920,14 @@ template static void __logContractWarningMessage(unsigned int size, T& msg) { logger.__logContractWarningMessage(size, msg); +} + +static void __pauseLogMessage() +{ + logger.pause(); +} + +static void __resumeLogMessage() +{ + logger.resume(); } \ No newline at end of file diff --git a/src/logging/net_msg_impl.h b/src/logging/net_msg_impl.h index 51cea8122..63bbdba1b 100644 --- a/src/logging/net_msg_impl.h +++ b/src/logging/net_msg_impl.h @@ -37,21 +37,21 @@ void qLogger::processRequestLog(unsigned long long processorNumber, Peer* peer, { char* rBuffer = responseBuffers[processorNumber]; logBuffer.getMany(rBuffer, startFrom, length); - enqueueResponse(peer, (unsigned int)(length), RespondLog::type, header->dejavu(), rBuffer); + enqueueResponse(peer, (unsigned int)(length), RespondLog::type(), header->dejavu(), rBuffer); } else { - enqueueResponse(peer, 0, RespondLog::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } } else { - enqueueResponse(peer, 0, RespondLog::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } return; } #endif - enqueueResponse(peer, 0, RespondLog::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } void qLogger::processRequestTxLogInfo(unsigned long long processorNumber, Peer* peer, RequestResponseHeader* header) @@ -65,7 +65,7 @@ void qLogger::processRequestTxLogInfo(unsigned long long processorNumber, Peer* && request->tick >= tickBegin ) { - ResponseLogIdRangeFromTx resp; + RespondLogIdRangeFromTx resp; if (request->tick <= lastUpdatedTick) { BlobInfo info = tx.getLogIdInfo(processorNumber, request->tick, request->txId); @@ -79,11 +79,11 @@ void qLogger::processRequestTxLogInfo(unsigned long long processorNumber, Peer* resp.length = -3; } - enqueueResponse(peer, sizeof(ResponseLogIdRangeFromTx), ResponseLogIdRangeFromTx::type, header->dejavu(), &resp); + enqueueResponse(peer, sizeof(RespondLogIdRangeFromTx), RespondLogIdRangeFromTx::type(), header->dejavu(), &resp); return; } #endif - enqueueResponse(peer, 0, ResponseLogIdRangeFromTx::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } void qLogger::processRequestTickTxLogInfo(unsigned long long processorNumber, Peer* peer, RequestResponseHeader* header) @@ -97,7 +97,7 @@ void qLogger::processRequestTickTxLogInfo(unsigned long long processorNumber, Pe && request->tick >= tickBegin ) { - ResponseAllLogIdRangesFromTick* resp = (ResponseAllLogIdRangesFromTick * )responseBuffers[processorNumber]; + RespondAllLogIdRangesFromTick* resp = (RespondAllLogIdRangesFromTick*)responseBuffers[processorNumber]; int txId = 0; if (request->tick <= lastUpdatedTick) { @@ -112,11 +112,11 @@ void qLogger::processRequestTickTxLogInfo(unsigned long long processorNumber, Pe resp->length[txId] = -3; } } - enqueueResponse(peer, sizeof(ResponseAllLogIdRangesFromTick), ResponseAllLogIdRangesFromTick::type, header->dejavu(), resp); + enqueueResponse(peer, sizeof(RespondAllLogIdRangesFromTick), RespondAllLogIdRangesFromTick::type(), header->dejavu(), resp); return; } #endif - enqueueResponse(peer, 0, ResponseAllLogIdRangesFromTick::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } void qLogger::processRequestPrunePageFile(Peer* peer, RequestResponseHeader* header) @@ -128,7 +128,7 @@ void qLogger::processRequestPrunePageFile(Peer* peer, RequestResponseHeader* hea && request->passcode[2] == logReaderPasscodes[2] && request->passcode[3] == logReaderPasscodes[3]) { - ResponsePruningLog resp; + RespondPruningLog resp; bool isValidRange = mapLogIdToBufferIndex.isIndexValid(request->fromLogId) && mapLogIdToBufferIndex.isIndexValid(request->toLogId); isValidRange &= (request->toLogId >= mapLogIdToBufferIndex.pageCap()); isValidRange &= (request->toLogId >= request->fromLogId + mapLogIdToBufferIndex.pageCap()); @@ -166,11 +166,11 @@ void qLogger::processRequestPrunePageFile(Peer* peer, RequestResponseHeader* hea } } - enqueueResponse(peer, sizeof(ResponsePruningLog), ResponsePruningLog::type, header->dejavu(), &resp); + enqueueResponse(peer, sizeof(RespondPruningLog), RespondPruningLog::type(), header->dejavu(), &resp); return; } #endif - enqueueResponse(peer, 0, ResponsePruningLog::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } void qLogger::processRequestGetLogDigest(Peer* peer, RequestResponseHeader* header) @@ -184,11 +184,11 @@ void qLogger::processRequestGetLogDigest(Peer* peer, RequestResponseHeader* head && request->requestedTick >= tickBegin && request->requestedTick <= lastUpdatedTick) { - ResponseLogStateDigest resp; + RespondLogStateDigest resp; resp.digest = digests[request->requestedTick - tickBegin]; - enqueueResponse(peer, sizeof(ResponseLogStateDigest), ResponseLogStateDigest::type, header->dejavu(), &resp); + enqueueResponse(peer, sizeof(RespondLogStateDigest), RespondLogStateDigest::type(), header->dejavu(), &resp); return; } #endif - enqueueResponse(peer, 0, ResponseLogStateDigest::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } \ No newline at end of file diff --git a/src/mining/mining.h b/src/mining/mining.h index bef2fe872..58fe4200f 100644 --- a/src/mining/mining.h +++ b/src/mining/mining.h @@ -52,48 +52,63 @@ struct CustomMiningSolutionTransaction : public Transaction } }; -struct CustomMiningTask -{ - unsigned long long taskIndex; // ever increasing number (unix timestamp in ms) - unsigned short firstComputorIndex; // the first computor index assigned by this task - unsigned short lastComputorIndex; // the last computor index assigned by this task - unsigned int padding; - - unsigned char blob[408]; // Job data from pool - unsigned long long size; // length of the blob - unsigned long long target; // Pool difficulty - unsigned long long height; // Block height - unsigned char seed[32]; // Seed hash for XMR -}; - struct CustomMiningTaskV2 { unsigned long long taskIndex; unsigned char m_template[896]; - unsigned long long m_extraNonceOffset; + unsigned long long m_extraNonceOffset;// offset to place extra nonce unsigned long long m_size; unsigned long long m_target; unsigned long long m_height; unsigned char m_seed[32]; }; -struct CustomMiningSolution -{ - unsigned long long taskIndex; // should match the index from task - unsigned short firstComputorIndex; // should match the index from task - unsigned short lastComputorIndex; // should match the index from task - unsigned int nonce; // xmrig::JobResult.nonce - m256i result; // xmrig::JobResult.result, 32 bytes -}; - struct CustomMiningSolutionV2 { - unsigned long long taskIndex; - unsigned long long nonce; // (extraNonce<<32) | nonce - unsigned long long reserve0; - unsigned long long reserve1; - unsigned long long reserve2; + unsigned long long taskIndex; // should match the index from task + unsigned long long nonce; // (extraNonce<<32) | nonce + unsigned long long encryptionLevel; // 0 = no encryption. `0` is allowed for legacy purposes + unsigned long long computorRandom; // random number which fullfils the condition computorRandom % 676 == ComputorIndex.`0` is allowed for legacy purposes + unsigned long long reserve2; // reserved m256i result; }; +static unsigned short customMiningGetComputorID(const CustomMiningSolutionV2* pSolution) +{ + // Check the computor idx of this solution. + unsigned short computorID = 0; + if (pSolution->encryptionLevel == 0) + { + computorID = (unsigned short)((pSolution->nonce >> 32ULL) % (unsigned long long)NUMBER_OF_COMPUTORS); + } + else + { + computorID = (unsigned short)(pSolution->computorRandom % (unsigned long long)NUMBER_OF_COMPUTORS); + } + return computorID; +} + +static CustomMiningSolutionV2 customMiningVerificationRequestToSolution(RequestCustomMiningSolutionVerification* pRequest) +{ + CustomMiningSolutionV2 solution; + solution.taskIndex = pRequest->taskIndex; + solution.nonce = pRequest->nonce; + solution.encryptionLevel = pRequest->encryptionLevel; + solution.computorRandom = pRequest->computorRandom; + solution.reserve2 = pRequest->reserve2; + return solution; +} + +static RespondCustomMiningSolutionVerification customMiningVerificationRequestToRespond(RequestCustomMiningSolutionVerification* pRequest) +{ + RespondCustomMiningSolutionVerification respond; + respond.taskIndex = pRequest->taskIndex; + respond.nonce = pRequest->nonce; + respond.encryptionLevel = pRequest->encryptionLevel; + respond.computorRandom = pRequest->computorRandom; + respond.reserve2 = pRequest->reserve2; + + return respond; +} + #define CUSTOM_MINING_SHARES_COUNT_SIZE_IN_BYTES 848 #define CUSTOM_MINING_SOLUTION_NUM_BIT_PER_COMP 10 #define TICK_VOTE_COUNTER_PUBLICATION_OFFSET 2 // Must be 2 @@ -115,7 +130,7 @@ struct BroadcastCustomMiningTransaction bool isBroadcasted; }; -BroadcastCustomMiningTransaction gCustomMiningBroadcastTxBuffer[NUMBER_OF_COMPUTORS]; +static BroadcastCustomMiningTransaction gCustomMiningBroadcastTxBuffer[NUMBER_OF_COMPUTORS]; class CustomMiningSharesCounter { @@ -368,9 +383,8 @@ class CustomMininingCache return retVal; } - // Try to fetch data from cacheIndex, also checking a few following entries in case of collisions (may update cacheIndex), - // increments counter of hits, misses, or collisions - int tryFetchingAndUpdate(T& rData, int updateCondition) + // Try to fetch data from cacheIndex, also checking a few following entries in case of collisions, + bool tryFetchingAndUpdateHitData(T& rData) { int retVal; unsigned int tryFetchIdx = rData.getHashIndex() % capacity(); @@ -380,16 +394,12 @@ class CustomMininingCache const T& cacheData = cache[tryFetchIdx]; if (cacheData.isEmpty()) { - // miss: data not available in cache yet (entry is empty) - misses++; retVal = CUSTOM_MINING_CACHE_MISS; break; } if (cacheData.isMatched(rData)) { - // hit: data available in cache -> return score - hits++; retVal = CUSTOM_MINING_CACHE_HIT; break; } @@ -398,19 +408,15 @@ class CustomMininingCache retVal = CUSTOM_MINING_CACHE_COLLISION; tryFetchIdx = (tryFetchIdx + 1) % capacity(); } - if (retVal == updateCondition) + + // This allow update data with additional field beside the key + if (retVal == CUSTOM_MINING_CACHE_HIT) { cache[tryFetchIdx] = rData; } RELEASE(lock); - if (retVal == CUSTOM_MINING_CACHE_COLLISION) - { - ACQUIRE(lock); - collisions++; - RELEASE(lock); - } - return retVal; + return (retVal == CUSTOM_MINING_CACHE_HIT); } @@ -522,103 +528,6 @@ class CustomMininingCache unsigned int invalid; }; -class CustomMiningSolutionCacheEntry -{ -public: - void reset() - { - _solution.taskIndex = 0; - _isHashed = false; - _isVerification = false; - _isValid = true; - } - - void set(const CustomMiningSolution* pCustomMiningSolution) - { - reset(); - _solution = *pCustomMiningSolution; - } - - void set(const unsigned long long taskIndex, unsigned int nonce, unsigned short firstComputorIndex, unsigned short lastComputorIndex) - { - reset(); - _solution.taskIndex = taskIndex; - _solution.nonce = nonce; - _solution.firstComputorIndex = firstComputorIndex; - _solution.lastComputorIndex = lastComputorIndex; - } - - void get(CustomMiningSolution& rCustomMiningSolution) - { - rCustomMiningSolution = _solution; - } - - bool isEmpty() const - { - return (_solution.taskIndex == 0); - } - bool isMatched(const CustomMiningSolutionCacheEntry& rOther) const - { - return (_solution.taskIndex == rOther.getTaskIndex()) && (_solution.nonce == rOther.getNonce()); - } - unsigned long long getHashIndex() - { - // TODO: reserve each computor ID a limited slot. - // This will avoid them spawning invalid solutions without verification - if (!_isHashed) - { - copyMem(_buffer, &_solution.taskIndex, sizeof(_solution.taskIndex)); - copyMem(_buffer + sizeof(_solution.taskIndex), &_solution.nonce, sizeof(_solution.nonce)); - KangarooTwelve(_buffer, sizeof(_solution.taskIndex) + sizeof(_solution.nonce), &_digest, sizeof(_digest)); - _isHashed = true; - } - return _digest; - } - - unsigned long long getTaskIndex() const - { - return _solution.taskIndex; - } - - unsigned long long getNonce() const - { - return _solution.nonce; - } - - bool isValid() - { - return _isValid; - } - - bool isVerified() - { - return _isVerification; - } - - void setValid(bool val) - { - _isValid = val; - } - - void setVerified(bool val) - { - _isVerification = true; - } - - void setEmpty() - { - _solution.taskIndex = 0; - } - -private: - CustomMiningSolution _solution; - unsigned long long _digest; - unsigned char _buffer[sizeof(_solution.taskIndex) + sizeof(_solution.nonce)]; - bool _isHashed; - bool _isVerification; - bool _isValid; -}; - class CustomMiningSolutionV2CacheEntry { public: @@ -709,11 +618,10 @@ class CustomMiningSolutionV2CacheEntry // In charge of storing custom mining -constexpr unsigned int NUMBER_OF_TASK_PARTITIONS = 4; -constexpr unsigned long long MAX_NUMBER_OF_CUSTOM_MINING_SOLUTIONS = (200ULL << 20) / NUMBER_OF_TASK_PARTITIONS / sizeof(CustomMiningSolutionCacheEntry); +constexpr unsigned long long MAX_NUMBER_OF_CUSTOM_MINING_SOLUTIONS = (200ULL << 20) / sizeof(CustomMiningSolutionV2CacheEntry); constexpr unsigned long long CUSTOM_MINING_INVALID_INDEX = 0xFFFFFFFFFFFFFFFFULL; constexpr unsigned long long CUSTOM_MINING_TASK_STORAGE_COUNT = 60 * 60 * 24 * 8 / 2 / 10; // All epoch tasks in 7 (+1) days, 10s per task, idle phases only -constexpr unsigned long long CUSTOM_MINING_TASK_STORAGE_SIZE = CUSTOM_MINING_TASK_STORAGE_COUNT * sizeof(CustomMiningTask); // ~16.6MB +constexpr unsigned long long CUSTOM_MINING_TASK_STORAGE_SIZE = CUSTOM_MINING_TASK_STORAGE_COUNT * sizeof(CustomMiningTaskV2); // ~16.6MB constexpr unsigned long long CUSTOM_MINING_SOLUTION_STORAGE_COUNT = MAX_NUMBER_OF_CUSTOM_MINING_SOLUTIONS; constexpr unsigned long long CUSTOM_MINING_STORAGE_PROCESSOR_MAX_STORAGE = 10 * 1024 * 1024; // 10MB constexpr unsigned long long CUSTOM_MINING_RESPOND_MESSAGE_MAX_SIZE = 1 * 1024 * 1024; // 1MB @@ -756,18 +664,12 @@ struct CustomMiningStats long long maxCollisionShareCount; // Max number of shares that are not save in cached because of collision // Stats of current custom mining phase - Counter phase[NUMBER_OF_TASK_PARTITIONS]; Counter phaseV2; // Asume at begining of epoch. void epochReset() { lastPhases.reset(); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - phase[i].reset(); - } - phaseV2.reset(); ATOMIC_STORE64(maxOverflowShareCount, 0); ATOMIC_STORE64(maxCollisionShareCount, 0); @@ -782,14 +684,11 @@ struct CustomMiningStats long long valid = 0; long long invalid = 0; long long duplicated = 0; - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - tasks += ATOMIC_LOAD64(phase[i].tasks); - shares += ATOMIC_LOAD64(phase[i].shares); - valid += ATOMIC_LOAD64(phase[i].valid); - invalid += ATOMIC_LOAD64(phase[i].invalid); - duplicated += ATOMIC_LOAD64(phase[i].duplicated); - } + tasks += ATOMIC_LOAD64(phaseV2.tasks); + shares += ATOMIC_LOAD64(phaseV2.shares); + valid += ATOMIC_LOAD64(phaseV2.valid); + invalid += ATOMIC_LOAD64(phaseV2.invalid); + duplicated += ATOMIC_LOAD64(phaseV2.duplicated); // Accumulate the phase into last phases ATOMIC_ADD64(lastPhases.tasks, tasks); @@ -798,11 +697,6 @@ struct CustomMiningStats ATOMIC_ADD64(lastPhases.invalid, invalid); ATOMIC_ADD64(lastPhases.duplicated, duplicated); - // Reset phase number - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - phase[i].reset(); - } phaseV2.reset(); } @@ -813,14 +707,11 @@ struct CustomMiningStats long long customMiningValidShares = 0; long long customMiningInvalidShares = 0; long long customMiningDuplicated = 0; - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - customMiningTasks += ATOMIC_LOAD64(phase[i].tasks); - customMiningShares += ATOMIC_LOAD64(phase[i].shares); - customMiningValidShares += ATOMIC_LOAD64(phase[i].valid); - customMiningInvalidShares += ATOMIC_LOAD64(phase[i].invalid); - customMiningDuplicated += ATOMIC_LOAD64(phase[i].duplicated); - } + customMiningTasks = ATOMIC_LOAD64(phaseV2.tasks); + customMiningShares = ATOMIC_LOAD64(phaseV2.shares); + customMiningValidShares = ATOMIC_LOAD64(phaseV2.valid); + customMiningInvalidShares = ATOMIC_LOAD64(phaseV2.invalid); + customMiningDuplicated = ATOMIC_LOAD64(phaseV2.duplicated); appendText(message, L"Phase:"); appendText(message, L" Tasks: "); @@ -834,25 +725,13 @@ struct CustomMiningStats appendText(message, L" | Duplicated: "); appendNumber(message, customMiningDuplicated, true); - appendText(message, L"Phase V2:"); - appendText(message, L" Tasks: "); - appendNumber(message, phaseV2.tasks, true); - appendText(message, L" | Shares: "); - appendNumber(message, phaseV2.shares, true); - appendText(message, L" | Valid: "); - appendNumber(message, phaseV2.valid, true); - appendText(message, L" | InValid: "); - appendNumber(message, phaseV2.invalid, true); - appendText(message, L" | Duplicated: "); - appendNumber(message, phaseV2.duplicated, true); - long long customMiningEpochTasks = customMiningTasks + ATOMIC_LOAD64(lastPhases.tasks); long long customMiningEpochShares = customMiningShares + ATOMIC_LOAD64(lastPhases.shares); long long customMiningEpochInvalidShares = customMiningInvalidShares + ATOMIC_LOAD64(lastPhases.invalid); long long customMiningEpochValidShares = customMiningValidShares + ATOMIC_LOAD64(lastPhases.valid); long long customMiningEpochDuplicated = customMiningDuplicated + ATOMIC_LOAD64(lastPhases.duplicated); - appendText(message, L". Epoch (not count v2):"); + appendText(message, L". Epoch:"); appendText(message, L" Tasks: "); appendNumber(message, customMiningEpochTasks, false); appendText(message, L" | Shares: "); @@ -1231,7 +1110,6 @@ struct CustomMiningSolutionStorageEntry unsigned long long cacheEntryIndex; }; -typedef CustomMiningSortedStorage CustomMiningTaskStorage; typedef CustomMiningSortedStorage CustomMiningSolutionStorage; typedef CustomMiningSortedStorage CustomMiningTaskV2Storage; @@ -1241,11 +1119,6 @@ class CustomMiningStorage public: void init() { - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _taskStorage[i].init(); - _solutionStorage[i].init(); - } _taskV2Storage.init(); _solutionV2Storage.init(); // Buffer allocation for each processors. It is limited to 10MB each @@ -1260,11 +1133,6 @@ class CustomMiningStorage } void deinit() { - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _taskStorage[i].deinit(); - _solutionStorage[i].deinit(); - } _taskV2Storage.deinit(); _solutionV2Storage.deinit(); for (unsigned int i = 0; i < MAX_NUMBER_OF_PROCESSORS; i++) @@ -1276,18 +1144,10 @@ class CustomMiningStorage void reset() { ACQUIRE(gCustomMiningTaskStorageLock); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _taskStorage[i].reset(); - } _taskV2Storage.reset(); RELEASE(gCustomMiningTaskStorageLock); ACQUIRE(gCustomMiningSolutionStorageLock); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _solutionStorage[i].reset(); - } _solutionV2Storage.reset(); RELEASE(gCustomMiningSolutionStorageLock); @@ -1302,32 +1162,30 @@ class CustomMiningStorage unsigned char* packedData = _dataBuffer[processorNumber]; CustomMiningRespondDataHeader* packedHeader = (CustomMiningRespondDataHeader*)packedData; packedHeader->respondType = RespondCustomMiningData::taskType; - packedHeader->itemSize = sizeof(CustomMiningTask); + packedHeader->itemSize = sizeof(CustomMiningTaskV2); packedHeader->fromTimeStamp = fromTimeStamp; packedHeader->toTimeStamp = toTimeStamp; packedHeader->itemCount = 0; unsigned char* traverseData = packedData + sizeof(CustomMiningRespondDataHeader); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) + unsigned char* data = _taskV2Storage.getSerializedData(fromTimeStamp, toTimeStamp, processorNumber); + if (data != NULL) { - unsigned char* data = _taskStorage[i].getSerializedData(fromTimeStamp, toTimeStamp, processorNumber); - if (data != NULL) + CustomMiningRespondDataHeader* customMiningInternalHeader = (CustomMiningRespondDataHeader*)data; + ASSERT(packedHeader->itemSize == customMiningInternalHeader->itemSize); + unsigned long long dataSize = customMiningInternalHeader->itemCount * sizeof(CustomMiningTaskV2); + if (customMiningInternalHeader->itemCount > 0 && remainedSize >= dataSize) { - CustomMiningRespondDataHeader* customMiningInternalHeader = (CustomMiningRespondDataHeader*)data; - ASSERT(packedHeader->itemSize == customMiningInternalHeader->itemSize); - unsigned long long dataSize = customMiningInternalHeader->itemCount * sizeof(CustomMiningTask); - if (customMiningInternalHeader->itemCount > 0 && remainedSize >= dataSize) - { - packedHeader->itemCount += customMiningInternalHeader->itemCount; - // Copy data - copyMem(traverseData, data + sizeof(CustomMiningRespondDataHeader), dataSize); + packedHeader->itemCount += customMiningInternalHeader->itemCount; + // Copy data + copyMem(traverseData, data + sizeof(CustomMiningRespondDataHeader), dataSize); - // Update pointer and size - traverseData += dataSize; - remainedSize -= dataSize; - } + // Update pointer and size + traverseData += dataSize; + remainedSize -= dataSize; } } + return packedData; } @@ -1354,115 +1212,30 @@ class CustomMiningStorage } unsigned long long _last3TaskV2Indexes[3]; // [0] is max, [2] is min - CustomMiningTaskStorage _taskStorage[NUMBER_OF_TASK_PARTITIONS]; CustomMiningTaskV2Storage _taskV2Storage; - CustomMiningSolutionStorage _solutionStorage[NUMBER_OF_TASK_PARTITIONS]; CustomMiningSolutionStorage _solutionV2Storage; - - // Buffer can accessed from multiple threads unsigned char* _dataBuffer[MAX_NUMBER_OF_PROCESSORS]; }; -// Describe how a task is divided into groups -// The task is for computorID in range [firstComputorIdx, lastComputorIdx] -// domainSize determine the range of nonce that computor should work on -// Currenly, it is designed as -// domainSize = (2 ^ 32)/ (lastComputorIndex - firstComputorIndex + 1); -// myNonceOffset = (myComputorIndex - firstComputorIndex) * domainSize; -// myNonce = myNonceOffset + x; x = [0, domainSize - 1] -// For computorID calculation, -// computorID = myNonce / domainSize + firstComputorIndex -struct CustomMiningTaskPartition -{ - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int domainSize; -}; - -#define SOLUTION_CACHE_DYNAMIC_MEM 0 - -static CustomMiningTaskPartition gTaskPartition[NUMBER_OF_TASK_PARTITIONS]; - -#if SOLUTION_CACHE_DYNAMIC_MEM -static CustomMininingCache* gSystemCustomMiningSolutionCache = NULL; -#else -static CustomMininingCache gSystemCustomMiningSolutionCache[NUMBER_OF_TASK_PARTITIONS]; -#endif - static CustomMininingCache gSystemCustomMiningSolutionV2Cache; static CustomMiningStorage gCustomMiningStorage; static CustomMiningStats gCustomMiningStats; -// Get the part ID -int customMiningGetPartitionID(unsigned short firstComputorIndex, unsigned short lastComputorIndex) -{ - int partitionID = -1; - for (int k = 0; k < NUMBER_OF_TASK_PARTITIONS; k++) - { - if (firstComputorIndex == gTaskPartition[k].firstComputorIdx - && lastComputorIndex == gTaskPartition[k].lastComputorIdx) - { - partitionID = k; - break; - } - } - return partitionID; -} - - -// Generate computor task partition -int customMiningInitTaskPartitions() -{ - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - // Currently the task is partitioned evenly - gTaskPartition[i].firstComputorIdx = i * NUMBER_OF_COMPUTORS / NUMBER_OF_TASK_PARTITIONS; - gTaskPartition[i].lastComputorIdx = gTaskPartition[i].firstComputorIdx + NUMBER_OF_COMPUTORS / NUMBER_OF_TASK_PARTITIONS - 1; - ASSERT(gTaskPartition[i].lastComputorIdx > gTaskPartition[i].firstComputorIdx + 1); - gTaskPartition[i].domainSize = (unsigned int)((1ULL << 32) / ((unsigned long long)gTaskPartition[i].lastComputorIdx - gTaskPartition[i].firstComputorIdx + 1)); - } - return 0; -} - -// Get computor ids -int customMiningGetComputorID(unsigned int nonce, int partId) -{ - return nonce / gTaskPartition[partId].domainSize + gTaskPartition[partId].firstComputorIdx; -} -int customMiningInitialize() +static int customMiningInitialize() { gCustomMiningStorage.init(); -#if SOLUTION_CACHE_DYNAMIC_MEM - allocPoolWithErrorLog(L"gSystemCustomMiningSolutionCache", - NUMBER_OF_TASK_PARTITIONS * sizeof(CustomMininingCache), - (void**)&gSystemCustomMiningSolutionCache, - __LINE__); -#endif - setMem((unsigned char*)gSystemCustomMiningSolutionCache, NUMBER_OF_TASK_PARTITIONS * sizeof(CustomMininingCache), 0); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - gSystemCustomMiningSolutionCache[i].init(); - } gSystemCustomMiningSolutionV2Cache.init(); - customMiningInitTaskPartitions(); return 0; } -int customMiningDeinitialize() +static int customMiningDeinitialize() { -#if SOLUTION_CACHE_DYNAMIC_MEM - if (gSystemCustomMiningSolutionCache) - { - freePool(gSystemCustomMiningSolutionCache); - gSystemCustomMiningSolutionCache = NULL; - } -#endif gCustomMiningStorage.deinit(); return 0; } @@ -1470,37 +1243,24 @@ int customMiningDeinitialize() #ifdef NO_UEFI #else // Save score cache to SCORE_CACHE_FILE_NAME -void saveCustomMiningCache(int epoch, CHAR16* directory = NULL) +static void saveCustomMiningCache(int epoch, CHAR16* directory = NULL) { logToConsole(L"Saving custom mining cache file..."); CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 4] = epoch / 100 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 3] = (epoch % 100) / 10 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 2] = epoch % 10 + L'0'; - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 8] = i / 100 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 7] = (i % 100) / 10 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 6] = i % 10 + L'0'; - gSystemCustomMiningSolutionCache[i].save(CUSTOM_MINING_CACHE_FILE_NAME, directory); - } + gSystemCustomMiningSolutionV2Cache.save(CUSTOM_MINING_CACHE_FILE_NAME, directory); } // Update score cache filename with epoch and try to load file -bool loadCustomMiningCache(int epoch) +static bool loadCustomMiningCache(int epoch) { logToConsole(L"Loading custom mining cache..."); bool success = true; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 4] = epoch / 100 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 3] = (epoch % 100) / 10 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 2] = epoch % 10 + L'0'; - // TODO: Support later - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 8] = i / 100 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 7] = (i % 100) / 10 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 6] = i % 10 + L'0'; - success &= gSystemCustomMiningSolutionCache[i].load(CUSTOM_MINING_CACHE_FILE_NAME); - } + success &= gSystemCustomMiningSolutionV2Cache.load(CUSTOM_MINING_CACHE_FILE_NAME); return success; } #endif diff --git a/src/network_core/peers.h b/src/network_core/peers.h index 0150fb7b8..a66b74b31 100644 --- a/src/network_core/peers.h +++ b/src/network_core/peers.h @@ -182,6 +182,23 @@ static bool isWhiteListPeer(unsigned char address[4]) } */ +static bool isPrivateIp(const unsigned char address[4]) +{ + int total = min(int(sizeof(knownPublicPeers)/sizeof(knownPublicPeers[0])), NUMBER_OF_PRIVATE_IP); + for (int i = 0; i < total; i++) + { + const auto& privateIp = knownPublicPeers[i]; + if (address[0] == privateIp[0] + && address[1] == privateIp[1] + && address[2] == privateIp[2] + && address[3] == privateIp[3]) + { + return true; + } + } + return false; +} + static void closePeer(Peer* peer) { PROFILE_SCOPE(); @@ -403,6 +420,7 @@ static void enqueueResponse(Peer* peer, unsigned int dataSize, unsigned char typ */ static bool isBogonAddress(const IPv4Address& address) { + return false; return (!address.u8[0]) || (address.u8[0] == 127) || (address.u8[0] == 10) @@ -735,7 +753,7 @@ static void processReceivedData(unsigned int i, unsigned int salt) { _InterlockedIncrement64(&numberOfDiscardedRequests); - enqueueResponse(&peers[i], 0, TryAgain::type, requestResponseHeader->dejavu(), NULL); + enqueueResponse(&peers[i], 0, TryAgain::type(), requestResponseHeader->dejavu(), NULL); } } else diff --git a/src/network_messages/assets.h b/src/network_messages/assets.h index a54f13b35..1148bd655 100644 --- a/src/network_messages/assets.h +++ b/src/network_messages/assets.h @@ -62,9 +62,10 @@ struct RequestIssuedAssets { m256i publicKey; - enum { - type = 36, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_ISSUED_ASSETS; + } }; static_assert(sizeof(RequestIssuedAssets) == 32, "Something is wrong with the struct size."); @@ -77,9 +78,10 @@ struct RespondIssuedAssets unsigned int universeIndex; m256i siblings[ASSETS_DEPTH]; - enum { - type = 37, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_ISSUED_ASSETS; + } }; @@ -87,9 +89,10 @@ struct RequestOwnedAssets { m256i publicKey; - enum { - type = 38, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_OWNED_ASSETS; + } }; static_assert(sizeof(RequestOwnedAssets) == 32, "Something is wrong with the struct size."); @@ -103,9 +106,10 @@ struct RespondOwnedAssets unsigned int universeIndex; m256i siblings[ASSETS_DEPTH]; - enum { - type = 39, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_OWNED_ASSETS; + } }; @@ -113,9 +117,10 @@ struct RequestPossessedAssets { m256i publicKey; - enum { - type = 40, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_POSSESSED_ASSETS; + } }; static_assert(sizeof(RequestPossessedAssets) == 32, "Something is wrong with the struct size."); @@ -130,9 +135,10 @@ struct RespondPossessedAssets unsigned int universeIndex; m256i siblings[ASSETS_DEPTH]; - enum { - type = 41, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_POSSESSED_ASSETS; + } }; // Options to request assets: @@ -142,9 +148,10 @@ struct RespondPossessedAssets // - by universeIdx (set issuer and asset name to 0) union RequestAssets { - enum { - type = 52, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_ASSETS; + } // type of asset request static constexpr unsigned short requestIssuanceRecords = 0; @@ -200,9 +207,10 @@ struct RespondAssets unsigned int tick; unsigned int universeIndex; - enum { - type = 53, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_ASSETS; + } }; static_assert(sizeof(RespondAssets) == 56, "Something is wrong with the struct size."); diff --git a/src/network_messages/broadcast_message.h b/src/network_messages/broadcast_message.h index c58e6b82a..fe4c6c5d2 100644 --- a/src/network_messages/broadcast_message.h +++ b/src/network_messages/broadcast_message.h @@ -23,9 +23,10 @@ struct BroadcastMessage m256i destinationPublicKey; m256i gammingNonce; - enum { - type = 1, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::BROADCAST_MESSAGE; + } }; static_assert(sizeof(BroadcastMessage) == 32 + 32 + 32, "Something is wrong with the struct size."); diff --git a/src/network_messages/common_def.h b/src/network_messages/common_def.h index 4627d922a..926eb1686 100644 --- a/src/network_messages/common_def.h +++ b/src/network_messages/common_def.h @@ -2,7 +2,6 @@ #define SIGNATURE_SIZE 64 #define NUMBER_OF_TRANSACTIONS_PER_TICK 1024 // Must be 2^N -#define MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR 128 #define MAX_NUMBER_OF_CONTRACTS 1024 // Must be 1024 #define NUMBER_OF_COMPUTORS 676 #define QUORUM (NUMBER_OF_COMPUTORS * 2 / 3 + 1) @@ -19,6 +18,8 @@ #define MAX_AMOUNT (ISSUANCE_RATE * 1000LL) #define MAX_SUPPLY (ISSUANCE_RATE * 200ULL) +#include "network_message_type.h" + // If you want to use the network_meassges directory in your project without dependencies to other code, // you may define NETWORK_MESSAGES_WITHOUT_CORE_DEPENDENCIES before including any header or change the diff --git a/src/network_messages/common_response.h b/src/network_messages/common_response.h index 07d1f079d..23c97a3ff 100644 --- a/src/network_messages/common_response.h +++ b/src/network_messages/common_response.h @@ -2,14 +2,18 @@ struct EndResponse { - enum { - type = 35, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::END_RESPONSE; + } }; -struct TryAgain // Must be returned if _dejavu is not 0, and the incoming packet cannot be processed (usually when incoming packets queue is full) +// Must be returned if _dejavu is not 0, and the incoming packet +// cannot be processed (usually when incoming packets queue is full) +struct TryAgain { - enum { - type = 54, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::TRY_AGAIN; + } }; diff --git a/src/network_messages/computors.h b/src/network_messages/computors.h index c3783067c..fec79969e 100644 --- a/src/network_messages/computors.h +++ b/src/network_messages/computors.h @@ -19,16 +19,18 @@ struct BroadcastComputors { Computors computors; - enum { - type = 2, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::BROADCAST_COMPUTORS; + } }; struct RequestComputors { - enum { - type = 11, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_COMPUTORS; + } }; diff --git a/src/network_messages/contract.h b/src/network_messages/contract.h index 0d96a9117..9c0ea2c5b 100644 --- a/src/network_messages/contract.h +++ b/src/network_messages/contract.h @@ -3,14 +3,34 @@ #include "common_def.h" +struct RequestActiveIPOs +{ + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_ACTIVE_IPOS; + } +}; + + +struct RespondActiveIPO +{ + unsigned int contractIndex; + char assetName[8]; + + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_ACTIVE_IPO; + } +}; struct RequestContractIPO { unsigned int contractIndex; - enum { - type = 33, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_CONTRACT_IPO; + } }; @@ -21,9 +41,10 @@ struct RespondContractIPO m256i publicKeys[NUMBER_OF_COMPUTORS]; long long prices[NUMBER_OF_COMPUTORS]; - enum { - type = 34, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_CONTRACT_IPO; + } }; static_assert(sizeof(RespondContractIPO) == 4 + 4 + 32 * NUMBER_OF_COMPUTORS + 8 * NUMBER_OF_COMPUTORS, "Something is wrong with the struct size."); @@ -36,9 +57,10 @@ struct RequestContractFunction // Invokes contract function unsigned short inputSize; // Variable-size input - enum { - type = 42, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_CONTRACT_FUNCTION; + } }; @@ -46,7 +68,8 @@ struct RespondContractFunction // Returns result of contract function invocation { // Variable-size output; the size must be 0 if the invocation has failed for whatever reason (e.g. no a function registered for [inputType], or the function has timed out) - enum { - type = 43, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_CONTRACT_FUNCTION; + } }; diff --git a/src/network_messages/custom_mining.h b/src/network_messages/custom_mining.h index bd7aed79c..7df0fe14b 100644 --- a/src/network_messages/custom_mining.h +++ b/src/network_messages/custom_mining.h @@ -1,12 +1,16 @@ #pragma once +#include "network_message_type.h" + + // Message struture for request custom mining data -struct RequestedCustomMiningData +struct RequestCustomMiningData { - enum + static constexpr unsigned char type() { - type = 60, - }; + return NetworkMessageType::REQUEST_CUSTOM_MINING_DATA; + } + enum { taskType = 0, @@ -18,11 +22,6 @@ struct RequestedCustomMiningData unsigned long long fromTaskIndex; unsigned long long toTaskIndex; - // Determine which task partition - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int padding; - // Type of the request: either task (taskType) or solution (solutionType). long long dataType; }; @@ -30,10 +29,11 @@ struct RequestedCustomMiningData // Message struture for respond custom mining data struct RespondCustomMiningData { - enum + static constexpr unsigned char type() { - type = 61, - }; + return NetworkMessageType::RESPOND_CUSTOM_MINING_DATA; + } + enum { taskType = 0, @@ -43,24 +43,28 @@ struct RespondCustomMiningData // Ussualy: [CustomMiningRespondDataHeader ... NumberOfItems * ItemSize]; }; -struct RequestedCustomMiningSolutionVerification +struct RequestCustomMiningSolutionVerification { - enum + static constexpr unsigned char type() { - type = 62, - }; + return NetworkMessageType::REQUEST_CUSTOM_MINING_SOLUTION_VERIFICATION; + } + unsigned long long taskIndex; - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int nonce; + unsigned long long nonce; + unsigned long long encryptionLevel; + unsigned long long computorRandom; + unsigned long long reserve2; unsigned long long isValid; // validity of the solution. 0: invalid, >0: valid + }; struct RespondCustomMiningSolutionVerification { - enum + static constexpr unsigned char type() { - type = 63, - }; + return NetworkMessageType::RESPOND_CUSTOM_MINING_SOLUTION_VERIFICATION; + } + enum { notExisted = 0, // solution not existed in cache @@ -69,9 +73,10 @@ struct RespondCustomMiningSolutionVerification customMiningStateEnded = 3, // not in custom mining state }; unsigned long long taskIndex; - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int nonce; + unsigned long long nonce; + unsigned long long encryptionLevel; + unsigned long long computorRandom; + unsigned long long reserve2; long long status; // Flag indicate the status of solution }; diff --git a/src/network_messages/entity.h b/src/network_messages/entity.h index ccc58b236..36d9b020b 100644 --- a/src/network_messages/entity.h +++ b/src/network_messages/entity.h @@ -16,24 +16,30 @@ struct EntityRecord static_assert(sizeof(EntityRecord) == 32 + 2 * 8 + 2 * 4 + 2 * 4, "Something is wrong with the struct size."); -#define REQUEST_ENTITY 31 - -struct RequestedEntity +struct RequestEntity { m256i publicKey; -}; -static_assert(sizeof(RequestedEntity) == 32, "Something is wrong with the struct size."); + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_ENTITY; + } +}; +static_assert(sizeof(RequestEntity) == 32, "Something is wrong with the struct size."); -#define RESPOND_ENTITY 32 -struct RespondedEntity +struct RespondEntity { EntityRecord entity; unsigned int tick; int spectrumIndex; m256i siblings[SPECTRUM_DEPTH]; + + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_ENTITY; + } }; -static_assert(sizeof(RespondedEntity) == sizeof(EntityRecord) + 4 + 4 + 32 * SPECTRUM_DEPTH, "Something is wrong with the struct size."); +static_assert(sizeof(RespondEntity) == sizeof(EntityRecord) + 4 + 4 + 32 * SPECTRUM_DEPTH, "Something is wrong with the struct size."); diff --git a/src/network_messages/execution_fees.h b/src/network_messages/execution_fees.h new file mode 100644 index 000000000..5fe3c6023 --- /dev/null +++ b/src/network_messages/execution_fees.h @@ -0,0 +1,163 @@ +#pragma once + +#include "network_messages/transactions.h" +#include "contract_core/contract_def.h" + +// Transaction input type for execution fee reporting +constexpr int EXECUTION_FEE_REPORT_INPUT_TYPE = 9; + +// Variable-length transaction for reporting execution fees +// Layout: ExecutionFeeReportTransactionPrefix + contractIndices[numEntries] + [alignment padding] + executionFees[numEntries] + ExecutionFeeReportTransactionPostfix +struct ExecutionFeeReportTransactionPrefix : public Transaction +{ + static constexpr unsigned char transactionType() + { + return EXECUTION_FEE_REPORT_INPUT_TYPE; + } + + static constexpr long long minAmount() + { + return 0; // System transaction + } + + static constexpr unsigned short minInputSize() + { + return sizeof(phaseNumber) + sizeof(numEntries); + } + + static constexpr unsigned short maxInputSize() + { + // phaseNumber + numEntries + contractIndices[contractCount] + alignment + executionFees[contractCount] + dataLock + unsigned int indicesSize = contractCount * sizeof(unsigned int); + unsigned int alignmentPadding = (contractCount % 2 == 1) ? sizeof(unsigned int) : 0; + unsigned int feesSize = contractCount * sizeof(unsigned long long); + return static_cast(sizeof(phaseNumber) + sizeof(numEntries) + indicesSize + alignmentPadding + feesSize + sizeof(m256i)); + } + + static bool isValidExecutionFeeReport(const Transaction* transaction) + { + return transaction->amount == minAmount() + && transaction->inputSize >= minInputSize() + && transaction->inputSize <= maxInputSize(); + } + + static unsigned int getNumEntries(const Transaction* transaction) + { + const auto* prefix = (const ExecutionFeeReportTransactionPrefix*)transaction; + return prefix->numEntries; + } + + static bool isValidEntryAlignment(const Transaction* transaction) + { + const auto* prefix = (const ExecutionFeeReportTransactionPrefix*)transaction; + unsigned int numEntries = prefix->numEntries; + + // Calculate expected payload size + unsigned int indicesSize = numEntries * sizeof(unsigned int); + unsigned int alignmentPadding = (numEntries % 2 == 1) ? sizeof(unsigned int) : 0; + unsigned int feesSize = numEntries * sizeof(unsigned long long); + unsigned int expectedPayloadSize = indicesSize + alignmentPadding + feesSize; + + // Actual payload size (inputSize - phaseNumber - numEntries - dataLock) + unsigned int actualPayloadSize = transaction->inputSize - sizeof(prefix->phaseNumber) - sizeof(prefix->numEntries) - sizeof(m256i); + + return actualPayloadSize == expectedPayloadSize; + } + + static const unsigned int* getContractIndices(const Transaction* transaction) + { + const auto* prefix = (const ExecutionFeeReportTransactionPrefix*)transaction; + return (const unsigned int*)(transaction->inputPtr() + sizeof(prefix->phaseNumber) + sizeof(prefix->numEntries)); + } + + static const unsigned long long* getExecutionFees(const Transaction* transaction) + { + const auto* prefix = (const ExecutionFeeReportTransactionPrefix*)transaction; + unsigned int numEntries = prefix->numEntries; + unsigned int indicesSize = numEntries * sizeof(unsigned int); + unsigned int alignmentPadding = (numEntries % 2 == 1) ? sizeof(unsigned int) : 0; + + const unsigned char* afterPrefix = transaction->inputPtr() + sizeof(prefix->phaseNumber) + sizeof(prefix->numEntries); + return (const unsigned long long*)(afterPrefix + indicesSize + alignmentPadding); + } + + unsigned int phaseNumber; // Phase this report is for (tick / NUMBER_OF_COMPUTORS) + unsigned int numEntries; // Number of contract entries in this report + // Followed by: + // - unsigned int contractIndices[numEntries] + // - [0 or 4 bytes alignment padding for executionFees array] + // - long long executionFees[numEntries] +}; + +struct ExecutionFeeReportTransactionPostfix +{ + m256i dataLock; + unsigned char signature[SIGNATURE_SIZE]; +}; + +// Payload structure for execution fee transaction +// Note: postfix is written at variable position based on actual entry count, not at fixed position +struct ExecutionFeeReportPayload +{ + ExecutionFeeReportTransactionPrefix transaction; + unsigned int contractIndices[contractCount]; + unsigned long long executionFees[contractCount]; // Compiler auto-aligns to 8 bytes + ExecutionFeeReportTransactionPostfix postfix; +}; + +// Calculate expected size: Transaction(84) + phaseNumber(4) + numEntries(4) + contractIndices + alignment + executionFees + dataLock(32) + signature(64) +static_assert( sizeof(ExecutionFeeReportPayload) == sizeof(Transaction) + sizeof(unsigned int) + sizeof(unsigned int) + (contractCount * sizeof(unsigned int)) + ((contractCount % 2 == 1) ? sizeof(unsigned int) : 0) + (contractCount * sizeof(unsigned long long)) + sizeof(m256i) + SIGNATURE_SIZE, "ExecutionFeeReportPayload has wrong struct size"); +static_assert( sizeof(ExecutionFeeReportPayload) <= sizeof(Transaction) + MAX_INPUT_SIZE + SIGNATURE_SIZE, "ExecutionFeeReportPayload is bigger than max transaction size. Currently max 82 SC are supported by the report"); + +// Builds the execution fee report payload from contract execution times +// Returns the number of entries added (0 if no contracts were executed) +static inline unsigned int buildExecutionFeeReportPayload( + ExecutionFeeReportPayload& payload, + const unsigned long long* contractExecutionTimes, + const unsigned int phaseNumber, + const unsigned long long multiplierNumerator, + const unsigned long long multiplierDenominator +) +{ + if (multiplierDenominator == 0 || multiplierNumerator == 0) + { + return 0; + } + + payload.transaction.phaseNumber = phaseNumber; + + // Build arrays with contract indices and execution fees + unsigned int entryCount = 0; + for (unsigned int contractIndex = 1; contractIndex < contractCount; contractIndex++) + { + unsigned long long executionTime = contractExecutionTimes[contractIndex]; + if (executionTime > 0) + { + unsigned long long executionFee = (executionTime * multiplierNumerator) / multiplierDenominator; + if (executionFee > 0) + { + payload.contractIndices[entryCount] = contractIndex; + payload.executionFees[entryCount] = executionFee; + entryCount++; + } + } + } + payload.transaction.numEntries = entryCount; + + // Return if no contract was executed + if (entryCount == 0) + { + return 0; + } + + // Compact the executionFees to the correct position (right after contractIndices[entryCount] + alignment) + unsigned int alignmentPadding = (entryCount % 2 == 1) ? sizeof(unsigned int) : 0; + unsigned char* afterPrefix = ((unsigned char*)&payload) + sizeof(ExecutionFeeReportTransactionPrefix); + unsigned char* compactFeesPosition = afterPrefix + (entryCount * sizeof(unsigned int)) + alignmentPadding; + copyMem(compactFeesPosition, payload.executionFees, entryCount * sizeof(unsigned long long)); + + // Calculate and set input size based on actual number of entries + payload.transaction.inputSize = (unsigned short)(sizeof(payload.transaction.phaseNumber) + sizeof(payload.transaction.numEntries) + (entryCount * sizeof(unsigned int)) + alignmentPadding + (entryCount * sizeof(unsigned long long)) + sizeof(ExecutionFeeReportTransactionPostfix::dataLock)); + + return entryCount; +} diff --git a/src/network_messages/logging.h b/src/network_messages/logging.h index 3463e2f34..43b7584b2 100644 --- a/src/network_messages/logging.h +++ b/src/network_messages/logging.h @@ -12,9 +12,10 @@ struct RequestLog unsigned long long fromID; unsigned long long toID; // inclusive - enum { - type = 44, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_LOG; + } }; @@ -22,9 +23,10 @@ struct RespondLog { // Variable-size log; - enum { - type = 45, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_LOG; + } }; @@ -35,21 +37,23 @@ struct RequestLogIdRangeFromTx unsigned int tick; unsigned int txId; - enum { - type = 48, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_LOG_ID_RANGE_FROM_TX; + } }; // Response logid ranges from tx hash -struct ResponseLogIdRangeFromTx +struct RespondLogIdRangeFromTx { long long fromLogId; long long length; - enum { - type = 49, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_LOG_ID_RANGE_FROM_TX; + } }; // Request logId ranges (fromLogId, length) of all txs from a tick @@ -58,21 +62,23 @@ struct RequestAllLogIdRangesFromTick unsigned long long passcode[4]; unsigned int tick; - enum { - type = 50, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_ALL_LOG_ID_RANGES_FROM_TX; + } }; // Response logId ranges (fromLogId, length) of all txs from a tick -struct ResponseAllLogIdRangesFromTick +struct RespondAllLogIdRangesFromTick { long long fromLogId[LOG_TX_PER_TICK]; long long length[LOG_TX_PER_TICK]; - enum { - type = 51, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_ALL_LOG_ID_RANGES_FROM_TX; + } }; // Request the node to prune logs (to save disk) @@ -82,18 +88,21 @@ struct RequestPruningLog unsigned long long fromLogId; unsigned long long toLogId; - enum { - type = 56, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_PRUNING_LOG; + } }; // Response to above request, 0 if success, otherwise error code will be returned -struct ResponsePruningLog +struct RespondPruningLog { long long success; - enum { - type = 57, - }; + + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_PRUNING_LOG; + } }; // Request the digest of log event state, given requestedTick @@ -102,16 +111,19 @@ struct RequestLogStateDigest unsigned long long passcode[4]; unsigned int requestedTick; - enum { - type = 58, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_LOG_STATE_DIGEST; + } }; // Response above request, a 32 bytes digest -struct ResponseLogStateDigest +struct RespondLogStateDigest { m256i digest; - enum { - type = 59, - }; + + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_LOG_STATE_DIGEST; + } }; \ No newline at end of file diff --git a/src/network_messages/network_message_type.h b/src/network_messages/network_message_type.h new file mode 100644 index 000000000..5bfe6eb4c --- /dev/null +++ b/src/network_messages/network_message_type.h @@ -0,0 +1,55 @@ +#pragma once + +enum NetworkMessageType : unsigned char +{ + EXCHANGE_PUBLIC_PEERS = 0, + BROADCAST_MESSAGE = 1, + BROADCAST_COMPUTORS = 2, + BROADCAST_TICK = 3, + BROADCAST_FUTURE_TICK_DATA = 8, + REQUEST_COMPUTORS = 11, + REQUEST_QUORUM_TICK = 14, + REQUEST_TICK_DATA = 16, + BROADCAST_TRANSACTION = 24, + REQUEST_TRANSACTION_INFO = 26, + REQUEST_CURRENT_TICK_INFO = 27, + RESPOND_CURRENT_TICK_INFO = 28, + REQUEST_TICK_TRANSACTIONS = 29, + REQUEST_ENTITY = 31, + RESPOND_ENTITY = 32, + REQUEST_CONTRACT_IPO = 33, + RESPOND_CONTRACT_IPO = 34, + END_RESPONSE = 35, + REQUEST_ISSUED_ASSETS = 36, + RESPOND_ISSUED_ASSETS = 37, + REQUEST_OWNED_ASSETS = 38, + RESPOND_OWNED_ASSETS = 39, + REQUEST_POSSESSED_ASSETS = 40, + RESPOND_POSSESSED_ASSETS = 41, + REQUEST_CONTRACT_FUNCTION = 42, + RESPOND_CONTRACT_FUNCTION = 43, + REQUEST_LOG = 44, + RESPOND_LOG = 45, + REQUEST_SYSTEM_INFO = 46, + RESPOND_SYSTEM_INFO = 47, + REQUEST_LOG_ID_RANGE_FROM_TX = 48, + RESPOND_LOG_ID_RANGE_FROM_TX = 49, + REQUEST_ALL_LOG_ID_RANGES_FROM_TX = 50, + RESPOND_ALL_LOG_ID_RANGES_FROM_TX = 51, + REQUEST_ASSETS = 52, + RESPOND_ASSETS = 53, + TRY_AGAIN = 54, + REQUEST_PRUNING_LOG = 56, + RESPOND_PRUNING_LOG = 57, + REQUEST_LOG_STATE_DIGEST = 58, + RESPOND_LOG_STATE_DIGEST = 59, + REQUEST_CUSTOM_MINING_DATA = 60, + RESPOND_CUSTOM_MINING_DATA = 61, + REQUEST_CUSTOM_MINING_SOLUTION_VERIFICATION = 62, + RESPOND_CUSTOM_MINING_SOLUTION_VERIFICATION = 63, + REQUEST_ACTIVE_IPOS = 64, + RESPOND_ACTIVE_IPO = 65, + REQUEST_TX_STATUS = 201, // tx addon only + RESPOND_TX_STATUS = 202, // tx addon only + SPECIAL_COMMAND = 255, +}; diff --git a/src/network_messages/public_peers.h b/src/network_messages/public_peers.h index f3825d9a2..506b33367 100644 --- a/src/network_messages/public_peers.h +++ b/src/network_messages/public_peers.h @@ -6,9 +6,10 @@ struct ExchangePublicPeers { IPv4Address peers[NUMBER_OF_EXCHANGED_PEERS]; - enum { - type = 0, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::EXCHANGE_PUBLIC_PEERS; + } }; static_assert(sizeof(ExchangePublicPeers) == 4 * NUMBER_OF_EXCHANGED_PEERS, "Unexpected size!"); diff --git a/src/network_messages/special_command.h b/src/network_messages/special_command.h index 87b92b313..517754aeb 100644 --- a/src/network_messages/special_command.h +++ b/src/network_messages/special_command.h @@ -7,9 +7,10 @@ struct SpecialCommand { unsigned long long everIncreasingNonceAndCommandType; - enum { - type = 255, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::SPECIAL_COMMAND; + } }; #define SPECIAL_COMMAND_SHUT_DOWN 0ULL @@ -85,4 +86,31 @@ struct SpecialCommandSetConsoleLoggingModeRequestAndResponse unsigned char padding[7]; }; +#define SPECIAL_COMMAND_SAVE_SNAPSHOT 18ULL // F8 key +struct SpecialCommandSaveSnapshotRequestAndResponse +{ + enum + { + SAVING_TRIGGERED = 0, + SAVING_IN_PROGRESS, + REMOTE_SAVE_MODE_DISABLED, + UNKNOWN_FAILURE, + }; + + unsigned long long everIncreasingNonceAndCommandType; + unsigned int currentTick; + unsigned char status; + unsigned char padding[3]; +}; + #pragma pack(pop) + +#define SPECIAL_COMMAND_SET_EXECUTION_FEE_MULTIPLIER 19ULL +#define SPECIAL_COMMAND_GET_EXECUTION_FEE_MULTIPLIER 20ULL +// This struct is used as response for the get command and as request and response for the set command. +struct SpecialCommandExecutionFeeMultiplierRequestAndResponse +{ + unsigned long long everIncreasingNonceAndCommandType; + unsigned long long multiplierNumerator; + unsigned long long multiplierDenominator; +}; \ No newline at end of file diff --git a/src/network_messages/system_info.h b/src/network_messages/system_info.h index 30a1b043b..48c6933df 100644 --- a/src/network_messages/system_info.h +++ b/src/network_messages/system_info.h @@ -2,14 +2,22 @@ #include "common_def.h" -#define REQUEST_SYSTEM_INFO 46 - - -#define RESPOND_SYSTEM_INFO 47 +struct RequestSystemInfo +{ + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_SYSTEM_INFO; + } +}; #pragma pack(push, 1) struct RespondSystemInfo { + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_SYSTEM_INFO; + } + short version; unsigned short epoch; unsigned int tick; @@ -37,7 +45,7 @@ struct RespondSystemInfo unsigned long long currentEntityBalanceDustThreshold; unsigned int targetTickVoteSignature; - unsigned long long _reserve0; + unsigned long long computorPacketSignature; unsigned long long _reserve1; unsigned long long _reserve2; unsigned long long _reserve3; diff --git a/src/network_messages/tick.h b/src/network_messages/tick.h index 09c4f8223..73d0579b2 100644 --- a/src/network_messages/tick.h +++ b/src/network_messages/tick.h @@ -43,9 +43,10 @@ struct BroadcastTick { Tick tick; - enum { - type = 3, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::BROADCAST_TICK; + } }; @@ -78,9 +79,10 @@ struct BroadcastFutureTickData { TickData tickData; - enum { - type = 8, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::BROADCAST_FUTURE_TICK_DATA; + } }; @@ -95,9 +97,10 @@ struct RequestQuorumTick { RequestedQuorumTick quorumTick; - enum { - type = 14, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_QUORUM_TICK; + } }; @@ -111,17 +114,21 @@ struct RequestTickData { RequestedTickData requestedTickData; - enum { - type = 16, - }; + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_TICK_DATA; + } }; +struct RequestCurrentTickInfo +{ + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_CURRENT_TICK_INFO; + } +}; -#define REQUEST_CURRENT_TICK_INFO 27 - -#define RESPOND_CURRENT_TICK_INFO 28 - -struct CurrentTickInfo +struct RespondCurrentTickInfo { unsigned short tickDuration; unsigned short epoch; @@ -129,5 +136,10 @@ struct CurrentTickInfo unsigned short numberOfAlignedVotes; unsigned short numberOfMisalignedVotes; unsigned int initialTick; + + static constexpr unsigned char type() + { + return NetworkMessageType::RESPOND_CURRENT_TICK_INFO; + } }; diff --git a/src/network_messages/transactions.h b/src/network_messages/transactions.h index fb316bf91..ad2832530 100644 --- a/src/network_messages/transactions.h +++ b/src/network_messages/transactions.h @@ -9,8 +9,6 @@ struct ContractIPOBid unsigned short quantity; }; -#define BROADCAST_TRANSACTION 24 - // A transaction is made of this struct, followed by inputSize Bytes payload data and SIGNATURE_SIZE Bytes signature struct Transaction @@ -55,17 +53,25 @@ struct Transaction static_assert(sizeof(Transaction) == 32 + 32 + 8 + 4 + 2 + 2, "Something is wrong with the struct size."); -#define REQUEST_TICK_TRANSACTIONS 29 -struct RequestedTickTransactions +struct RequestTickTransactions { unsigned int tick; unsigned char transactionFlags[NUMBER_OF_TRANSACTIONS_PER_TICK / 8]; + + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_TICK_TRANSACTIONS; + } }; -#define REQUEST_TRANSACTION_INFO 26 -struct RequestedTransactionInfo +struct RequestTransactionInfo { m256i txDigest; + + static constexpr unsigned char type() + { + return NetworkMessageType::REQUEST_TRANSACTION_INFO; + } }; diff --git a/src/platform/concurrency.h b/src/platform/concurrency.h index 3182467ab..4cdf5d3df 100644 --- a/src/platform/concurrency.h +++ b/src/platform/concurrency.h @@ -73,6 +73,9 @@ class BusyWaitingTracker END_WAIT_WHILE() #define ATOMIC_STORE8(target, val) _InterlockedExchange8(&target, val) +// long in windows is 32bits +static_assert(sizeof(long) == 4, "Size of long for _InterlockedExchange is 4 bytes"); +#define ATOMIC_STORE32(target, val) _InterlockedExchange((volatile long*)&target, val) #define ATOMIC_INC64(target) _InterlockedIncrement64(&target) #define ATOMIC_AND64(target, val) _InterlockedAnd64(&target, val) #define ATOMIC_STORE64(target, val) _InterlockedExchange64(&target, val) diff --git a/src/platform/debugging.h b/src/platform/debugging.h index e9a0eefbe..0e15dee8c 100644 --- a/src/platform/debugging.h +++ b/src/platform/debugging.h @@ -2,6 +2,7 @@ #include "assert.h" #include "concurrency.h" +#include "console_logging.h" #include "file_io.h" diff --git a/src/platform/file_io.h b/src/platform/file_io.h index a0e5f198a..1616bdec3 100644 --- a/src/platform/file_io.h +++ b/src/platform/file_io.h @@ -8,10 +8,10 @@ #include #include #include + #include "console_logging.h" #include "concurrency.h" #include "memory.h" -#include "debugging.h" // If you get an error reading and writing files, set the chunk sizes below to // the cluster size set for formatting you disk. If you have no idea about the @@ -33,7 +33,9 @@ static constexpr int ASYNC_FILE_IO_MAX_QUEUE_ITEMS = (1ULL << ASYNC_FILE_IO_MAX_ static EFI_FILE_PROTOCOL* root = NULL; class AsyncFileIO; static AsyncFileIO* gAsyncFileIO = NULL; + static void addDebugMessage(const CHAR16* msg); + static long long getFileSize(CHAR16* fileName, CHAR16* directory = NULL) { #ifdef NO_UEFI diff --git a/src/platform/quorum_value.h b/src/platform/quorum_value.h new file mode 100644 index 000000000..49568da35 --- /dev/null +++ b/src/platform/quorum_value.h @@ -0,0 +1,31 @@ +#pragma once + +#include "lib/platform_common/sorting.h" + +// Calculates percentile value from array (in-place sort) +// Returns value at position: (count * Numerator) / Denominator +template +T calculatePercentileValue(T* values, unsigned int count) +{ + static_assert(Denominator > 0, "Denominator must be greater than 0"); + static_assert(Numerator < Denominator, "Numerator must be < Denominator"); + + if (count == 0) + { + return T(0); + } + + quickSort(values, 0, count - 1, Order); + + unsigned int percentileIndex = (count * Numerator) / Denominator; + + return values[percentileIndex]; +} + +// Calculates 2/3 quorum value with ascending sort order +template +T calculateAscendingQuorumValue(T* values, unsigned int count) +{ + return calculatePercentileValue(values, count); +} diff --git a/src/platform/virtual_memory.h b/src/platform/virtual_memory.h index db88e149b..4133e3a1e 100644 --- a/src/platform/virtual_memory.h +++ b/src/platform/virtual_memory.h @@ -5,6 +5,7 @@ #include "platform/time.h" #include "platform/memory_util.h" #include "platform/debugging.h" +#include "platform/file_io.h" #include "four_q.h" #include "kangaroo_twelve.h" diff --git a/src/private_settings.h b/src/private_settings.h index dc5e68c4a..94a7e4ee8 100644 --- a/src/private_settings.h +++ b/src/private_settings.h @@ -4,12 +4,15 @@ // Do NOT share the data of "Private Settings" section with anybody!!! -#define OPERATOR "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +#define OPERATOR "MEFKYFCDXDUILCAJKOIKWQAPENJDUHSSYPBRWFOTLALILAYWQFDSITJELLHG" static unsigned char computorSeeds[][55 + 1] = { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }; +// number of private ips for computor's internal services +// these are the first N ip in knownPublicPeers, these IPs will never be shared or deleted +#define NUMBER_OF_PRIVATE_IP 2 // Enter static IPs of peers (ideally at least 4 including your own IP) to disseminate them to other peers. // You can find current peer IPs at https://app.qubic.li/network/live static const unsigned char knownPublicPeers[][4] = { @@ -24,45 +27,45 @@ static const unsigned char whiteListPeers[][4] = { }; */ -#define ENABLE_STANDARD_LOGGING 0 // logging universe + spectrum -#define ENABLE_SMART_CONTRACT_LOGGING 0// logging smart contract +#define ENABLE_QUBIC_LOGGING_EVENT 0 // turn on logging events -#if !ENABLE_STANDARD_LOGGING && ENABLE_SMART_CONTRACT_LOGGING -#error ENABLE_SMART_CONTRACT_LOGGING 1 also requires ENABLE_STANDARD_LOGGING 1 -#endif +// Virtual memory settings for logging +#define LOG_BUFFER_PAGE_SIZE 300000000ULL +#define PMAP_LOG_PAGE_SIZE 30000000ULL +#define IMAP_LOG_PAGE_SIZE 10000ULL +#define VM_NUM_CACHE_PAGE 8 -#if ENABLE_STANDARD_LOGGING -#define LOG_UNIVERSE 1 // all universe activities/events (incl: issue, ownership/possession changes) -#define LOG_SPECTRUM 1 // all spectrum activities/events (incl: transfers, burn, dust cleaning) -#else -#define LOG_UNIVERSE 0 -#define LOG_SPECTRUM 0 -#endif -#if ENABLE_SMART_CONTRACT_LOGGING +#if ENABLE_QUBIC_LOGGING_EVENT +// DO NOT MODIFY THIS AREA UNLESS YOU ARE DEVELOPING LOGGING FEATURES +#define LOG_UNIVERSE 1 +#define LOG_SPECTRUM 1 #define LOG_CONTRACT_ERROR_MESSAGES 1 #define LOG_CONTRACT_WARNING_MESSAGES 1 #define LOG_CONTRACT_INFO_MESSAGES 1 #define LOG_CONTRACT_DEBUG_MESSAGES 1 #define LOG_CUSTOM_MESSAGES 1 #else +#define LOG_UNIVERSE 0 +#define LOG_SPECTRUM 0 #define LOG_CONTRACT_ERROR_MESSAGES 0 #define LOG_CONTRACT_WARNING_MESSAGES 0 #define LOG_CONTRACT_INFO_MESSAGES 0 #define LOG_CONTRACT_DEBUG_MESSAGES 0 #define LOG_CUSTOM_MESSAGES 0 #endif + static unsigned long long logReaderPasscodes[4] = { - 0, 0, 0, 0 // REMOVE THIS ENTRY AND REPLACE IT WITH YOUR OWN RANDOM NUMBERS IN [0..18446744073709551615] RANGE IF LOGGING IS ENABLED + 1,2,3,4// REMOVE THIS ENTRY AND REPLACE IT WITH YOUR OWN RANDOM NUMBERS IN [0..18446744073709551615] RANGE IF LOGGING IS ENABLED }; // Mode for auto save ticks: // 0: disable // 1: save tick storage every TICK_STORAGE_AUTOSAVE_TICK_PERIOD ticks, only AUX mode // 2: save tick storage only when pressing the `F8` key or it is requested remotely -#define TICK_STORAGE_AUTOSAVE_MODE 0 +#define TICK_STORAGE_AUTOSAVE_MODE 0 // NOTE: Strategy to pick TICK_STORAGE_AUTOSAVE_TICK_PERIOD: // Although the default value is 1000, there is a chance that your node can be misaligned at tick XXXX2000,XXXX3000,XXXX4000,... // Perform state persisting when your node is misaligned will also make your node misaligned after resuming. // Thus, picking various TICK_STORAGE_AUTOSAVE_TICK_PERIOD numbers across AUX nodes is recommended. // some suggested prime numbers you can try: 971 977 983 991 997 -#define TICK_STORAGE_AUTOSAVE_TICK_PERIOD 1000 \ No newline at end of file +#define TICK_STORAGE_AUTOSAVE_TICK_PERIOD 1337 \ No newline at end of file diff --git a/src/public_settings.h b/src/public_settings.h index 4d058020c..d544281d1 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -1,5 +1,7 @@ #pragma once +#include "platform/global_var.h" + ////////// Public Settings \\\\\\\\\\ ////////////////////////////////////////////////////////////////////////// @@ -7,9 +9,9 @@ // no need to define AVX512 here anymore, just change the project settings to use the AVX512 version // random seed is now obtained from spectrumDigests - +#define TESTNET #define MAX_NUMBER_OF_PROCESSORS 32 -#define NUMBER_OF_SOLUTION_PROCESSORS 12 +#define NUMBER_OF_SOLUTION_PROCESSORS 2 // Number of buffers available for executing contract functions in parallel; having more means reserving a bit more RAM (+1 = +32 MB) // and less waiting in request processors if there are more parallel contract function requests. The maximum value that may make sense @@ -23,11 +25,19 @@ // Number of ticks from prior epoch that are kept after seamless epoch transition. These can be requested after transition. #define TICKS_TO_KEEP_FROM_PRIOR_EPOCH 100 -#define TARGET_TICK_DURATION 1500 -#define TRANSACTION_SPARSENESS 1 +// The tick duration used for timing and scheduling logic. +#define TARGET_TICK_DURATION 7000 + +// The tick duration used to calculate the size of memory buffers. +// This determines the memory footprint of the application. +#define TICK_DURATION_FOR_ALLOCATION_MS 7000 +#define TRANSACTION_SPARSENESS 4 + +// Number of ticks that are stored in the pending txs pool. This also defines how many ticks in advance a tx can be registered. +#define PENDING_TXS_POOL_NUM_TICKS (1000 * 60 * 10ULL / TICK_DURATION_FOR_ALLOCATION_MS) // 10 minutes // Below are 2 variables that are used for auto-F5 feature: -#define AUTO_FORCE_NEXT_TICK_THRESHOLD 0ULL // Multiplier of TARGET_TICK_DURATION for the system to detect "F5 case" | set to 0 to disable +#define AUTO_FORCE_NEXT_TICK_THRESHOLD 20ULL // Multiplier of TARGET_TICK_DURATION for the system to detect "F5 case" | set to 0 to disable // to prevent bad actor causing misalignment. // depends on actual tick time of the network, operators should set this number randomly in this range [12, 26] // eg: If AUTO_FORCE_NEXT_TICK_THRESHOLD is 8 and TARGET_TICK_DURATION is 2, then the system will start "auto F5 procedure" after 16 seconds after receveing 451+ votes @@ -37,7 +47,7 @@ #define NEXT_TICK_TIMEOUT_THRESHOLD 5ULL // Multiplier of TARGET_TICK_DURATION for the system to discard next tick in tickData. // This will lead to zero `expectedNextTickTransactionDigest` in consensus - + #define PEER_REFRESHING_PERIOD 120000ULL #if AUTO_FORCE_NEXT_TICK_THRESHOLD != 0 static_assert(NEXT_TICK_TIMEOUT_THRESHOLD < AUTO_FORCE_NEXT_TICK_THRESHOLD, "Timeout threshold must be smaller than auto F5 threshold"); @@ -56,15 +66,16 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Config options that should NOT be changed by operators #define VERSION_A 1 -#define VERSION_B 252 +#define VERSION_B 274 #define VERSION_C 0 // Epoch and initial tick for node startup -#define EPOCH 171 -#define TICK 30220000 +#define EPOCH 199 +#define TICK 42232000 +#define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK -#define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" -#define DISPATCHER "XPXYKFLGSWRHRGAUKWFWVXCDVEYAPCPCNUTMUDWFGDYQCWZNJMWFZEEGCFFO" +#define ARBITRATOR "MEFKYFCDXDUILCAJKOIKWQAPENJDUHSSYPBRWFOTLALILAYWQFDSITJELLHG" +#define DISPATCHER "DISPAPLNOYSWXCJMZEMFUNCCMMJANGQPYJDSEXZTTBFSUEPYPEKCICADBUCJ" static unsigned short SYSTEM_FILE_NAME[] = L"system"; static unsigned short SYSTEM_END_OF_EPOCH_FILE_NAME[] = L"system.eoe"; @@ -73,11 +84,13 @@ static unsigned short UNIVERSE_FILE_NAME[] = L"universe.???"; static unsigned short SCORE_CACHE_FILE_NAME[] = L"score.???"; static unsigned short CONTRACT_FILE_NAME[] = L"contract????.???"; static unsigned short CUSTOM_MINING_REVENUE_END_OF_EPOCH_FILE_NAME[] = L"custom_revenue.eoe"; -static unsigned short CUSTOM_MINING_CACHE_FILE_NAME[] = L"custom_mining_cache???.???"; +static unsigned short CUSTOM_MINING_CACHE_FILE_NAME[] = L"custom_mining_cache.???"; +static unsigned short CONTRACT_EXEC_FEES_ACC_FILE_NAME[] = L"contract_exec_fees_acc.???"; +static unsigned short CONTRACT_EXEC_FEES_REC_FILE_NAME[] = L"contract_exec_fees_rec.???"; static constexpr unsigned long long NUMBER_OF_INPUT_NEURONS = 512; // K static constexpr unsigned long long NUMBER_OF_OUTPUT_NEURONS = 512; // L -static constexpr unsigned long long NUMBER_OF_TICKS = 600; // N +static constexpr unsigned long long NUMBER_OF_TICKS = 1000; // N static constexpr unsigned long long NUMBER_OF_NEIGHBORS = 728; // 2M. Must be divided by 2 static constexpr unsigned long long NUMBER_OF_MUTATIONS = 150; static constexpr unsigned long long POPULATION_THRESHOLD = NUMBER_OF_INPUT_NEURONS + NUMBER_OF_OUTPUT_NEURONS + NUMBER_OF_MUTATIONS; // P @@ -87,12 +100,15 @@ static constexpr unsigned int SOLUTION_THRESHOLD_DEFAULT = 321; #define SOLUTION_SECURITY_DEPOSIT 1000000 // Signing difficulty -#define TARGET_TICK_VOTE_SIGNATURE 0x00095CBEU // around 7000 signing operations per ID +#define TARGET_TICK_VOTE_SIGNATURE 0x07FFFFFFU // around 7000 signing operations per ID // include commonly needed definitions #include "network_messages/common_def.h" -#define MAX_NUMBER_OF_TICKS_PER_EPOCH (((((60 * 60 * 24 * 7) / (TARGET_TICK_DURATION / 1000)) + NUMBER_OF_COMPUTORS - 1) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS) +#define TESTNET_EPOCH_DURATION 86 // ~10 minutes with 7 second ticks (86 * 7 = 602 seconds) +#define MAX_NUMBER_OF_TICKS_PER_EPOCH (TESTNET_EPOCH_DURATION + 5) + + #define FIRST_TICK_TRANSACTION_OFFSET sizeof(unsigned long long) #define MAX_TRANSACTION_SIZE (MAX_INPUT_SIZE + sizeof(Transaction) + SIGNATURE_SIZE) @@ -100,10 +116,21 @@ static constexpr unsigned int SOLUTION_THRESHOLD_DEFAULT = 321; #define EXTERNAL_COMPUTATIONS_INTERVAL (676 + 1) static_assert(INTERNAL_COMPUTATIONS_INTERVAL >= NUMBER_OF_COMPUTORS, "Internal computation phase needs to be at least equal NUMBER_OF_COMPUTORS"); -// Format is DoW-hh-mm-ss in hex format, total 4bytes, each use 1 bytes +// List of start-end for full external computation times. The event must not be overlap. +// Format is DoW-hh-mm-ss in hex format, total 4 bytes, each use 1 bytes // DoW: Day of the week 0: Sunday, 1 = Monday ... -#define FULL_EXTERNAL_COMPUTATIONS_TIME_START_TIME 0x060C0000 // Sat 12:00:00 -#define FULL_EXTERNAL_COMPUTATIONS_TIME_STOP_TIME 0x000C0000 // Sun 12:00:00 +static unsigned int gFullExternalComputationTimes[][2] = +{ + {0x040C0000U, 0x040C1E00U}, // Thu 12:00:00 - Fri 12:00:00 + {0x060C0000U, 0x060C1E00U}, // Sat 12:00:00 - Sun 12:00:00 + {0x010C0000U, 0x010C1E00U}, // Mon 12:00:00 - Tue 12:00:00 +}; #define STACK_SIZE 4194304 #define TRACK_MAX_STACK_BUFFER_SIZE + +// Multipliers to convert from raw contract execution time to contract execution fee. +// Use values like (numerator 1, denominator 10) for division by 10. +GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierNumerator GLOBAL_VAR_INIT(1ULL); +GLOBAL_VAR_DECL unsigned long long executionTimeMultiplierDenominator GLOBAL_VAR_INIT(1ULL); + diff --git a/src/qubic.cpp b/src/qubic.cpp index 9f4babb9a..c6d034680 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,6 +1,6 @@ #define SINGLE_COMPILE_UNIT -// #define NO_NOST +// #define NO_QRWA // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" @@ -60,6 +60,9 @@ #include "ticking/ticking.h" #include "contract_core/qpi_ticking_impl.h" #include "vote_counter.h" +#include "ticking/execution_fee_report_collector.h" +#include "ticking/stable_computor_index.h" +#include "network_messages/execution_fees.h" #include "contract_core/ipo.h" #include "contract_core/qpi_ipo_impl.h" @@ -70,6 +73,7 @@ #include "mining/mining.h" #include "oracles/oracle_machines.h" +#include "contract_core/qpi_mining_impl.h" #include "revenue.h" ////////// Qubic \\\\\\\\\\ @@ -82,7 +86,7 @@ #define MAX_MESSAGE_PAYLOAD_SIZE MAX_TRANSACTION_SIZE #define MAX_UNIVERSE_SIZE 1073741824 #define MESSAGE_DISSEMINATION_THRESHOLD 1000000000 -#define PORT 21841 +#define PORT 31841 #define SYSTEM_DATA_SAVING_PERIOD 300000ULL #define TICK_TRANSACTIONS_PUBLICATION_OFFSET 2 // Must be only 2 #define MIN_MINING_SOLUTIONS_PUBLICATION_OFFSET 3 // Must be 3+ @@ -131,7 +135,9 @@ static unsigned short ownComputorIndicesMapping[sizeof(computorSeeds) / sizeof(c static TickStorage ts; static VoteCounter voteCounter; +static ExecutionFeeReportCollector executionFeeReportCollector; static TickData nextTickData; +static PendingTxsPool pendingTxsPool; static m256i uniqueNextTickTransactionDigests[NUMBER_OF_COMPUTORS]; static unsigned int uniqueNextTickTransactionDigestCounters[NUMBER_OF_COMPUTORS]; @@ -139,13 +145,7 @@ static unsigned int uniqueNextTickTransactionDigestCounters[NUMBER_OF_COMPUTORS] static unsigned int resourceTestingDigest = 0; static unsigned int numberOfTransactions = 0; -static volatile char entityPendingTransactionsLock = 0; -static unsigned char* entityPendingTransactions = NULL; -static unsigned char* entityPendingTransactionDigests = NULL; -static unsigned int entityPendingTransactionIndices[SPECTRUM_CAPACITY]; // [SPECTRUM_CAPACITY] must be >= than [NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR] -static volatile char computorPendingTransactionsLock = 0; -static unsigned char* computorPendingTransactions = NULL; -static unsigned char* computorPendingTransactionDigests = NULL; + static unsigned long long spectrumChangeFlags[SPECTRUM_CAPACITY / (sizeof(unsigned long long) * 8)]; static unsigned long long mainLoopNumerator = 0, mainLoopDenominator = 0; @@ -154,6 +154,7 @@ static unsigned int contractProcessorPhase; static const Transaction* contractProcessorTransaction = 0; // does not have signature in some cases, see notifyContractOfIncomingTransfer() static int contractProcessorTransactionMoneyflew = 0; static unsigned char contractProcessorPostIncomingTransferType = 0; +static const UserProcedureNotification* contractProcessorUserProcedureNotification = 0; static EFI_EVENT contractProcessorEvent; static m256i contractStateDigests[MAX_NUMBER_OF_CONTRACTS * 2 - 1]; const unsigned long long contractStateDigestsSizeInBytes = sizeof(contractStateDigests); @@ -239,9 +240,11 @@ struct unsigned char customMiningSharesCounterData[CustomMiningSharesCounter::_customMiningSolutionCounterDataSize]; } nodeStateBuffer; #endif -static bool saveComputer(CHAR16* directory = NULL); +static bool saveContractStateFiles(CHAR16* directory = NULL); +static bool saveContractExecFeeFiles(CHAR16* directory = NULL, bool saveAccumulatedTime = false); static bool saveSystem(CHAR16* directory = NULL); -static bool loadComputer(CHAR16* directory = NULL, bool forceLoadFromFile = false); +static bool loadContractStateFiles(CHAR16* directory = NULL, bool forceLoadFromFile = false); +static bool loadContractExecFeeFiles(CHAR16* directory = NULL, bool loadAccumulatedTime = false); static bool saveRevenueComponents(CHAR16* directory = NULL); #if ENABLED_LOGGING @@ -260,6 +263,7 @@ static struct unsigned char signature[SIGNATURE_SIZE]; } voteCounterPayload; +static ExecutionFeeReportPayload executionFeeReportPayload; static struct { @@ -281,7 +285,7 @@ static struct static struct { RequestResponseHeader header; - RequestedTickTransactions requestedTickTransactions; + RequestTickTransactions requestedTickTransactions; } requestedTickTransactions; static struct { @@ -415,19 +419,26 @@ static void getComputerDigest(m256i& digest) // This is currently avoided by calling getComputerDigest() from tick processor only (and in non-concurrent init) contractStateLock[digestIndex].acquireRead(); - const unsigned long long startTick = __rdtsc(); + const unsigned long long startTime = __rdtsc(); KangarooTwelve(contractStates[digestIndex], (unsigned int)size, &contractStateDigests[digestIndex], 32); - const unsigned long long executionTicks = __rdtsc() - startTick; + const unsigned long long executionTime = __rdtsc() - startTime; contractStateLock[digestIndex].releaseRead(); // K12 of state is included in contract execution time - _interlockedadd64(&contractTotalExecutionTicks[digestIndex], executionTicks); + _interlockedadd64(&contractTotalExecutionTime[digestIndex], executionTime); + // do not charge contract 0 state digest computation, + // only charge execution time if contract is already constructed/not in IPO + // TODO: enable this after adding proper tracking of contract state writes + //if (digestIndex > 0 && system.epoch >= contractDescriptions[digestIndex].constructionEpoch) + //{ + // executionTimeAccumulator.addTime(digestIndex, executionTime); + //} // Gather data for comparing different versions of K12 if (K12MeasurementsCount < 500) { - K12MeasurementsSum += executionTicks; + K12MeasurementsSum += executionTime; K12MeasurementsCount++; } } @@ -525,115 +536,7 @@ static void processBroadcastMessage(const unsigned long long processorNumber, Re recordCustomMining = gIsInCustomMiningState; RELEASE(gIsInCustomMiningStateLock); - if (messagePayloadSize == sizeof(CustomMiningTask) && request->sourcePublicKey == dispatcherPublicKey) - { - // See CustomMiningTaskMessage structure - // MESSAGE_TYPE_CUSTOM_MINING_TASK - - // Compute the gamming key to get the sub-type of message - unsigned char sharedKeyAndGammingNonce[64]; - setMem(sharedKeyAndGammingNonce, 32, 0); - copyMem(&sharedKeyAndGammingNonce[32], &request->gammingNonce, 32); - unsigned char gammingKey[32]; - KangarooTwelve64To32(sharedKeyAndGammingNonce, gammingKey); - - // Record the task emitted by dispatcher - if (recordCustomMining && gammingKey[0] == MESSAGE_TYPE_CUSTOM_MINING_TASK) - { - const CustomMiningTask* task = ((CustomMiningTask*)((unsigned char*)request + sizeof(BroadcastMessage))); - - // Determine the task part id - int partId = customMiningGetPartitionID(task->firstComputorIndex, task->lastComputorIndex); - if (partId >= 0) - { - // Record the task message - ACQUIRE(gCustomMiningTaskStorageLock); - int taskAddSts = gCustomMiningStorage._taskStorage[partId].addData(task); - RELEASE(gCustomMiningTaskStorageLock); - - if (CustomMiningTaskStorage::OK == taskAddSts) - { - ATOMIC_INC64(gCustomMiningStats.phase[partId].tasks); - } - } - } - } - else if (messagePayloadSize == sizeof(CustomMiningSolution)) - { - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) - { - if (request->sourcePublicKey == broadcastedComputors.computors.publicKeys[i]) - { - // Compute the gamming key to get the sub-type of message - unsigned char sharedKeyAndGammingNonce[64]; - setMem(sharedKeyAndGammingNonce, 32, 0); - copyMem(&sharedKeyAndGammingNonce[32], &request->gammingNonce, 32); - unsigned char gammingKey[32]; - KangarooTwelve64To32(sharedKeyAndGammingNonce, gammingKey); - - if (recordCustomMining && gammingKey[0] == MESSAGE_TYPE_CUSTOM_MINING_SOLUTION) - { - // Record the solution - bool isSolutionGood = false; - const CustomMiningSolution* solution = ((CustomMiningSolution*)((unsigned char*)request + sizeof(BroadcastMessage))); - - int partId = customMiningGetPartitionID(solution->firstComputorIndex, solution->lastComputorIndex); - - // TODO: taskIndex can use for detect for-sure stale shares - if (partId >= 0 && solution->taskIndex > 0) - { - CustomMiningSolutionCacheEntry cacheEntry; - cacheEntry.set(solution); - - unsigned int cacheIndex = 0; - int sts = gSystemCustomMiningSolutionCache[partId].tryFetching(cacheEntry, cacheIndex); - - // Check for duplicated solution - if (sts == CUSTOM_MINING_CACHE_MISS) - { - gSystemCustomMiningSolutionCache[partId].addEntry(cacheEntry, cacheIndex); - isSolutionGood = true; - } - - if (isSolutionGood) - { - // Check the computor idx of this solution. - unsigned short computorID = customMiningGetComputorID(solution->nonce, partId); - if (computorID <= gTaskPartition[partId].lastComputorIdx) - { - - ACQUIRE(gCustomMiningSharesCountLock); - gCustomMiningSharesCount[computorID]++; - RELEASE(gCustomMiningSharesCountLock); - - CustomMiningSolutionStorageEntry solutionStorageEntry; - solutionStorageEntry.taskIndex = solution->taskIndex; - solutionStorageEntry.nonce = solution->nonce; - solutionStorageEntry.cacheEntryIndex = cacheIndex; - - ACQUIRE(gCustomMiningSolutionStorageLock); - gCustomMiningStorage._solutionStorage[partId].addData(&solutionStorageEntry); - RELEASE(gCustomMiningSolutionStorageLock); - - } - } - - // Record stats - const unsigned int hitCount = gSystemCustomMiningSolutionCache[partId].hitCount(); - const unsigned int missCount = gSystemCustomMiningSolutionCache[partId].missCount(); - const unsigned int collision = gSystemCustomMiningSolutionCache[partId].collisionCount(); - - ATOMIC_STORE64(gCustomMiningStats.phase[partId].shares, missCount); - ATOMIC_STORE64(gCustomMiningStats.phase[partId].duplicated, hitCount); - ATOMIC_MAX64(gCustomMiningStats.maxCollisionShareCount, collision); - - } - } - break; - } - } - } - else if (messagePayloadSize == sizeof(CustomMiningTaskV2) && request->sourcePublicKey == dispatcherPublicKey) + if (messagePayloadSize == sizeof(CustomMiningTaskV2) && request->sourcePublicKey == dispatcherPublicKey) { unsigned char sharedKeyAndGammingNonce[64]; setMem(sharedKeyAndGammingNonce, 32, 0); @@ -649,7 +552,7 @@ static void processBroadcastMessage(const unsigned long long processorNumber, Re // Record the task message ACQUIRE(gCustomMiningTaskStorageLock); int taskAddSts = gCustomMiningStorage._taskV2Storage.addData(task); - if (CustomMiningTaskStorage::OK == taskAddSts) + if (CustomMiningTaskV2Storage::OK == taskAddSts) { ATOMIC_INC64(gCustomMiningStats.phaseV2.tasks); gCustomMiningStorage.updateTaskIndex(task->taskIndex); @@ -696,7 +599,7 @@ static void processBroadcastMessage(const unsigned long long processorNumber, Re if (isSolutionGood) { // Check the computor idx of this solution. - unsigned short computorID = (solution->nonce >> 32ULL) % 676ULL; + unsigned short computorID = customMiningGetComputorID(solution); ACQUIRE(gCustomMiningSharesCountLock); gCustomMiningSharesCount[computorID]++; @@ -917,9 +820,9 @@ static void processBroadcastTick(Peer* peer, RequestResponseHeader* header) && request->tick.millisecond <= 999) { unsigned char digest[32]; - request->tick.computorIndex ^= BroadcastTick::type; + request->tick.computorIndex ^= BroadcastTick::type(); KangarooTwelve(&request->tick, sizeof(Tick) - SIGNATURE_SIZE, digest, sizeof(digest)); - request->tick.computorIndex ^= BroadcastTick::type; + request->tick.computorIndex ^= BroadcastTick::type(); const bool verifyFourQCurve = true; if (verifyTickVoteSignature(broadcastedComputors.computors.publicKeys[request->tick.computorIndex].m256i_u8, digest, request->tick.signature, verifyFourQCurve)) { @@ -996,9 +899,9 @@ static void processBroadcastFutureTickData(Peer* peer, RequestResponseHeader* he if (ok) { unsigned char digest[32]; - request->tickData.computorIndex ^= BroadcastFutureTickData::type; + request->tickData.computorIndex ^= BroadcastFutureTickData::type(); KangarooTwelve(&request->tickData, sizeof(TickData) - SIGNATURE_SIZE, digest, sizeof(digest)); - request->tickData.computorIndex ^= BroadcastFutureTickData::type; + request->tickData.computorIndex ^= BroadcastFutureTickData::type(); if (verify(broadcastedComputors.computors.publicKeys[request->tickData.computorIndex].m256i_u8, digest, request->tickData.signature)) { if (header->isDejavuZero()) @@ -1073,42 +976,7 @@ static void processBroadcastTransaction(Peer* peer, RequestResponseHeader* heade enqueueResponse(NULL, header); } - const int computorIndex = ::computorIndex(request->sourcePublicKey); - if (computorIndex >= 0) - { - ACQUIRE(computorPendingTransactionsLock); - - const unsigned int offset = random(MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR); - if (((Transaction*)&computorPendingTransactions[computorIndex * offset * MAX_TRANSACTION_SIZE])->tick < request->tick - && request->tick < system.initialTick + MAX_NUMBER_OF_TICKS_PER_EPOCH) - { - copyMem(&computorPendingTransactions[computorIndex * offset * MAX_TRANSACTION_SIZE], request, transactionSize); - KangarooTwelve(request, transactionSize, &computorPendingTransactionDigests[computorIndex * offset * 32ULL], 32); - } - - RELEASE(computorPendingTransactionsLock); - } - else - { - const int spectrumIndex = ::spectrumIndex(request->sourcePublicKey); - if (spectrumIndex >= 0) - { - ACQUIRE(entityPendingTransactionsLock); - - // Pending transactions pool follows the rule: A transaction with a higher tick overwrites previous transaction from the same address. - // The second filter is to avoid accident made by users/devs (setting scheduled tick too high) and get locked until end of epoch. - // It also makes sense that a node doesn't need to store a transaction that is scheduled on a tick that node will never reach. - // Notice: MAX_NUMBER_OF_TICKS_PER_EPOCH is not set globally since every node may have different TARGET_TICK_DURATION time due to memory limitation. - if (((Transaction*)&entityPendingTransactions[spectrumIndex * MAX_TRANSACTION_SIZE])->tick < request->tick - && request->tick < system.initialTick + MAX_NUMBER_OF_TICKS_PER_EPOCH) - { - copyMem(&entityPendingTransactions[spectrumIndex * MAX_TRANSACTION_SIZE], request, transactionSize); - KangarooTwelve(request, transactionSize, &entityPendingTransactionDigests[spectrumIndex * 32ULL], 32); - } - - RELEASE(entityPendingTransactionsLock); - } - } + pendingTxsPool.add(request); unsigned int tickIndex = ts.tickToIndexCurrentEpoch(request->tick); ts.tickData.acquireLock(); @@ -1145,11 +1013,11 @@ static void processRequestComputors(Peer* peer, RequestResponseHeader* header) { if (broadcastedComputors.computors.epoch) { - enqueueResponse(peer, sizeof(broadcastedComputors), BroadcastComputors::type, header->dejavu(), &broadcastedComputors); + enqueueResponse(peer, sizeof(broadcastedComputors), BroadcastComputors::type(), header->dejavu(), &broadcastedComputors); } else { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } } @@ -1196,7 +1064,7 @@ static void processRequestQuorumTick(Peer* peer, RequestResponseHeader* header) if (tsTick->epoch == tickEpoch) { ts.ticks.acquireLock(computorIndices[index]); - enqueueResponse(peer, sizeof(Tick), BroadcastTick::type, header->dejavu(), tsTick); + enqueueResponse(peer, sizeof(Tick), BroadcastTick::type(), header->dejavu(), tsTick); ts.ticks.releaseLock(computorIndices[index]); } } @@ -1204,7 +1072,7 @@ static void processRequestQuorumTick(Peer* peer, RequestResponseHeader* header) computorIndices[index] = computorIndices[--numberOfComputorIndices]; } } - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } static void processRequestTickData(Peer* peer, RequestResponseHeader* header) @@ -1213,17 +1081,17 @@ static void processRequestTickData(Peer* peer, RequestResponseHeader* header) TickData* td = ts.tickData.getByTickIfNotEmpty(request->requestedTickData.tick); if (td) { - enqueueResponse(peer, sizeof(TickData), BroadcastFutureTickData::type, header->dejavu(), td); + enqueueResponse(peer, sizeof(TickData), BroadcastFutureTickData::type(), header->dejavu(), td); } else { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } } static void processRequestTickTransactions(Peer* peer, RequestResponseHeader* header) { - RequestedTickTransactions* request = header->getPayload(); + RequestTickTransactions* request = header->getPayload(); unsigned short tickEpoch = 0; const unsigned long long* tsReqTickTransactionOffsets; @@ -1240,15 +1108,15 @@ static void processRequestTickTransactions(Peer* peer, RequestResponseHeader* he if (tickEpoch != 0) { - unsigned short tickTransactionIndices[NUMBER_OF_TRANSACTIONS_PER_TICK]; - unsigned short numberOfTickTransactions; + unsigned int tickTransactionIndices[NUMBER_OF_TRANSACTIONS_PER_TICK]; + unsigned int numberOfTickTransactions; for (numberOfTickTransactions = 0; numberOfTickTransactions < NUMBER_OF_TRANSACTIONS_PER_TICK; numberOfTickTransactions++) { tickTransactionIndices[numberOfTickTransactions] = numberOfTickTransactions; } while (numberOfTickTransactions) { - const unsigned short index = random(numberOfTickTransactions); + const unsigned int index = random(numberOfTickTransactions); if (!(request->transactionFlags[tickTransactionIndices[index] >> 3] & (1 << (tickTransactionIndices[index] & 7)))) { @@ -1277,12 +1145,12 @@ static void processRequestTickTransactions(Peer* peer, RequestResponseHeader* he tickTransactionIndices[index] = tickTransactionIndices[--numberOfTickTransactions]; } } - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } static void processRequestTransactionInfo(Peer* peer, RequestResponseHeader* header) { - RequestedTransactionInfo* request = header->getPayload(); + RequestTransactionInfo* request = header->getPayload(); const Transaction* transaction = ts.transactionsDigestAccess.findTransaction(request->txDigest); if (transaction) { @@ -1290,13 +1158,13 @@ static void processRequestTransactionInfo(Peer* peer, RequestResponseHeader* hea } else { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } } static void processRequestCurrentTickInfo(Peer* peer, RequestResponseHeader* header) { - CurrentTickInfo currentTickInfo; + RespondCurrentTickInfo currentTickInfo; if (broadcastedComputors.computors.epoch) { @@ -1315,17 +1183,17 @@ static void processRequestCurrentTickInfo(Peer* peer, RequestResponseHeader* hea } else { - setMem(¤tTickInfo, sizeof(CurrentTickInfo), 0); + setMem(¤tTickInfo, sizeof(RespondCurrentTickInfo), 0); } - enqueueResponse(peer, sizeof(currentTickInfo), RESPOND_CURRENT_TICK_INFO, header->dejavu(), ¤tTickInfo); + enqueueResponse(peer, sizeof(currentTickInfo), RespondCurrentTickInfo::type(), header->dejavu(), ¤tTickInfo); } static void processResponseCurrentTickInfo(Peer* peer, RequestResponseHeader* header) { - if (header->size() == sizeof(RequestResponseHeader) + sizeof(CurrentTickInfo)) + if (header->size() == sizeof(RequestResponseHeader) + sizeof(RespondCurrentTickInfo)) { - CurrentTickInfo currentTickInfo = *(header->getPayload< CurrentTickInfo>()); + RespondCurrentTickInfo currentTickInfo = *(header->getPayload()); // avoid malformed data if (currentTickInfo.initialTick == system.initialTick && currentTickInfo.epoch == system.epoch @@ -1339,9 +1207,9 @@ static void processResponseCurrentTickInfo(Peer* peer, RequestResponseHeader* he static void processRequestEntity(Peer* peer, RequestResponseHeader* header) { - RespondedEntity respondedEntity; + RespondEntity respondedEntity; - RequestedEntity* request = header->getPayload(); + RequestEntity* request = header->getPayload(); respondedEntity.entity.publicKey = request->publicKey; // Inside spectrumIndex already have acquire/release lock respondedEntity.spectrumIndex = spectrumIndex(respondedEntity.entity.publicKey); @@ -1366,7 +1234,22 @@ static void processRequestEntity(Peer* peer, RequestResponseHeader* header) } - enqueueResponse(peer, sizeof(respondedEntity), RESPOND_ENTITY, header->dejavu(), &respondedEntity); + enqueueResponse(peer, sizeof(respondedEntity), RespondEntity::type(), header->dejavu(), &respondedEntity); +} + +static void processRequestActiveIPOs(Peer* peer, RequestResponseHeader* header) +{ + RespondActiveIPO response; + for (unsigned int contractIndex = 1; contractIndex < contractCount; ++contractIndex) + { + if (system.epoch == contractDescriptions[contractIndex].constructionEpoch - 1) // IPO happens in the epoch before construction + { + response.contractIndex = contractIndex; + copyMem(response.assetName, contractDescriptions[contractIndex].assetName, 8); + enqueueResponse(peer, sizeof(RespondActiveIPO), RespondActiveIPO::type(), header->dejavu(), &response); + } + } + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } static void processRequestContractIPO(Peer* peer, RequestResponseHeader* header) @@ -1377,7 +1260,7 @@ static void processRequestContractIPO(Peer* peer, RequestResponseHeader* header) respondContractIPO.contractIndex = request->contractIndex; respondContractIPO.tick = system.tick; if (request->contractIndex >= contractCount - || system.epoch >= contractDescriptions[request->contractIndex].constructionEpoch) + || system.epoch != (contractDescriptions[request->contractIndex].constructionEpoch - 1)) { setMem(respondContractIPO.publicKeys, sizeof(respondContractIPO.publicKeys), 0); setMem(respondContractIPO.prices, sizeof(respondContractIPO.prices), 0); @@ -1391,7 +1274,7 @@ static void processRequestContractIPO(Peer* peer, RequestResponseHeader* header) contractStateLock[request->contractIndex].releaseRead(); } - enqueueResponse(peer, sizeof(respondContractIPO), RespondContractIPO::type, header->dejavu(), &respondContractIPO); + enqueueResponse(peer, sizeof(respondContractIPO), RespondContractIPO::type(), header->dejavu(), &respondContractIPO); } static void processRequestContractFunction(Peer* peer, const unsigned long long processorNumber, RequestResponseHeader* header) @@ -1404,7 +1287,7 @@ static void processRequestContractFunction(Peer* peer, const unsigned long long || system.epoch < contractDescriptions[request->contractIndex].constructionEpoch || !contractUserFunctions[request->contractIndex][request->inputType]) { - enqueueResponse(peer, 0, RespondContractFunction::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, RespondContractFunction::type(), header->dejavu(), NULL); } else { @@ -1413,15 +1296,15 @@ static void processRequestContractFunction(Peer* peer, const unsigned long long if (errorCode == NoContractError) { // success: respond with function output - enqueueResponse(peer, qpiContext.outputSize, RespondContractFunction::type, header->dejavu(), qpiContext.outputBuffer); + enqueueResponse(peer, qpiContext.outputSize, RespondContractFunction::type(), header->dejavu(), qpiContext.outputBuffer); } else { // error: respond with empty output, send TryAgain if the function was stopped to resolve a potential // deadlock - unsigned char type = RespondContractFunction::type; + unsigned char type = RespondContractFunction::type(); if (errorCode == ContractErrorStoppedToResolveDeadlock) - type = TryAgain::type; + type = TryAgain::type(); enqueueResponse(peer, 0, type, header->dejavu(), NULL); } } @@ -1455,7 +1338,17 @@ static void processRequestSystemInfo(Peer* peer, RequestResponseHeader* header) respondedSystemInfo.currentEntityBalanceDustThreshold = (dustThresholdBurnAll > dustThresholdBurnHalf) ? dustThresholdBurnAll : dustThresholdBurnHalf; respondedSystemInfo.targetTickVoteSignature = TARGET_TICK_VOTE_SIGNATURE; - enqueueResponse(peer, sizeof(respondedSystemInfo), RESPOND_SYSTEM_INFO, header->dejavu(), &respondedSystemInfo); + + if (broadcastedComputors.computors.epoch != 0) + { + copyMem(&respondedSystemInfo.computorPacketSignature, broadcastedComputors.computors.signature, 8); + } + else + { + respondedSystemInfo.computorPacketSignature = 0; + } + + enqueueResponse(peer, sizeof(respondedSystemInfo), RespondSystemInfo::type(), header->dejavu(), &respondedSystemInfo); } @@ -1465,15 +1358,13 @@ static void processRequestSystemInfo(Peer* peer, RequestResponseHeader* header) // to prevent it from being re-sent for verification. static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, RequestResponseHeader* header) { - RequestedCustomMiningSolutionVerification* request = header->getPayload(); - if (header->size() >= sizeof(RequestResponseHeader) + sizeof(RequestedCustomMiningSolutionVerification) + SIGNATURE_SIZE) + RequestCustomMiningSolutionVerification* request = header->getPayload(); + if (header->size() >= sizeof(RequestResponseHeader) + sizeof(RequestCustomMiningSolutionVerification) + SIGNATURE_SIZE) { unsigned char digest[32]; KangarooTwelve(request, header->size() - sizeof(RequestResponseHeader) - SIGNATURE_SIZE, digest, sizeof(digest)); if (verify(operatorPublicKey.m256i_u8, digest, ((const unsigned char*)header + (header->size() - SIGNATURE_SIZE)))) { - RespondCustomMiningSolutionVerification respond; - // Update the share counting // Only record shares in idle phase char recordSolutions = 0; @@ -1481,25 +1372,20 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, recordSolutions = gIsInCustomMiningState; RELEASE(gIsInCustomMiningStateLock); + RespondCustomMiningSolutionVerification respond = customMiningVerificationRequestToRespond(request); if (recordSolutions) { - CustomMiningSolutionCacheEntry fullEntry; - fullEntry.set(request->taskIndex, request->nonce, request->firstComputorIdx, request->lastComputorIdx); + CustomMiningSolutionV2 solution = customMiningVerificationRequestToSolution(request); + + CustomMiningSolutionV2CacheEntry fullEntry; + fullEntry.set(&solution); fullEntry.setVerified(true); fullEntry.setValid(request->isValid > 0); - // Make sure the solution still existed - int partId = customMiningGetPartitionID(request->firstComputorIdx, request->lastComputorIdx); // Check the computor idx of this solution - int computorID = NUMBER_OF_COMPUTORS; - if (partId >= 0) - { - computorID = customMiningGetComputorID(request->nonce, partId); - } - - if (partId >=0 - && computorID <= gTaskPartition[partId].lastComputorIdx - && CUSTOM_MINING_CACHE_HIT == gSystemCustomMiningSolutionCache[partId].tryFetchingAndUpdate(fullEntry, CUSTOM_MINING_CACHE_HIT)) + unsigned short computorID = customMiningGetComputorID(&solution); + // Also re-update the cache data with verified = true and validity + if ( gSystemCustomMiningSolutionV2Cache.tryFetchingAndUpdateHitData(fullEntry)) { // Reduce the share of this nonce if it is invalid if (0 == request->isValid) @@ -1509,13 +1395,13 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, RELEASE(gCustomMiningSharesCountLock); // Save the number of invalid share count - ATOMIC_INC64(gCustomMiningStats.phase[partId].invalid); + ATOMIC_INC64(gCustomMiningStats.phaseV2.invalid); respond.status = RespondCustomMiningSolutionVerification::invalid; } else { - ATOMIC_INC64(gCustomMiningStats.phase[partId].valid); + ATOMIC_INC64(gCustomMiningStats.phaseV2.valid); respond.status = RespondCustomMiningSolutionVerification::valid; } } @@ -1528,12 +1414,7 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, { respond.status = RespondCustomMiningSolutionVerification::customMiningStateEnded; } - - respond.taskIndex = request->taskIndex; - respond.firstComputorIdx = request->firstComputorIdx; - respond.lastComputorIdx = request->lastComputorIdx; - respond.nonce = request->nonce; - enqueueResponse(peer, sizeof(respond), RespondCustomMiningSolutionVerification::type, header->dejavu(), &respond); + enqueueResponse(peer, sizeof(respond), RespondCustomMiningSolutionVerification::type(), header->dejavu(), &respond); } } } @@ -1546,8 +1427,8 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, // For the solution respond, only respond solution that has not been verified yet static void processCustomMiningDataRequest(Peer* peer, const unsigned long long processorNumber, RequestResponseHeader* header) { - RequestedCustomMiningData* request = header->getPayload(); - if (header->size() >= sizeof(RequestResponseHeader) + sizeof(RequestedCustomMiningData) + SIGNATURE_SIZE) + RequestCustomMiningData* request = header->getPayload(); + if (header->size() >= sizeof(RequestResponseHeader) + sizeof(RequestCustomMiningData) + SIGNATURE_SIZE) { unsigned char digest[32]; @@ -1556,7 +1437,7 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long { unsigned char* respond = NULL; // Request tasks - if (request->dataType == RequestedCustomMiningData::taskType) + if (request->dataType == RequestCustomMiningData::taskType) { // For task type, return all data from the current phase ACQUIRE(gCustomMiningTaskStorageLock); @@ -1573,30 +1454,24 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long enqueueResponse( peer, (unsigned int)respondDataSize, - RespondCustomMiningData::type, header->dejavu(), respond); + RespondCustomMiningData::type(), header->dejavu(), respond); } else { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } } // Request solutions - else if (request->dataType == RequestedCustomMiningData::solutionType) + else if (request->dataType == RequestCustomMiningData::solutionType) { // For solution type, return all solution from the current phase - int partId = customMiningGetPartitionID(request->firstComputorIdx, request->lastComputorIdx); - if (partId >= 0) { ACQUIRE(gCustomMiningSolutionStorageLock); // Look for all solution data - respond = gCustomMiningStorage._solutionStorage[partId].getSerializedData(request->fromTaskIndex, processorNumber); + respond = gCustomMiningStorage._solutionV2Storage.getSerializedData(request->fromTaskIndex, processorNumber); RELEASE(gCustomMiningSolutionStorageLock); } - else - { - respond = NULL; - } // Has the solutions if (NULL != respond) @@ -1610,12 +1485,12 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long unsigned char* respondSolutionPayload = respondSolution + sizeof(CustomMiningRespondDataHeader); long long remainedDataToSend = CUSTOM_MINING_RESPOND_MESSAGE_MAX_SIZE; int sendItem = 0; - for (int k = 0; k < customMiningInternalHeader->itemCount && remainedDataToSend > sizeof(CustomMiningSolution); k++) + for (int k = 0; k < customMiningInternalHeader->itemCount && remainedDataToSend > sizeof(CustomMiningSolutionV2); k++) { CustomMiningSolutionStorageEntry entry = solutionEntries[k]; - CustomMiningSolutionCacheEntry fullEntry; + CustomMiningSolutionV2CacheEntry fullEntry; - gSystemCustomMiningSolutionCache[partId].getEntry(fullEntry, (unsigned int)entry.cacheEntryIndex); + gSystemCustomMiningSolutionV2Cache.getEntry(fullEntry, (unsigned int)entry.cacheEntryIndex); // Check data is matched and not verifed yet if (!fullEntry.isEmpty() @@ -1624,16 +1499,16 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long && fullEntry.getNonce() == entry.nonce) { // Append data to send - CustomMiningSolution solution; + CustomMiningSolutionV2 solution; fullEntry.get(solution); - copyMem(respondSolutionPayload + k * sizeof(CustomMiningSolution), &solution, sizeof(CustomMiningSolution)); - remainedDataToSend -= sizeof(CustomMiningSolution); + copyMem(respondSolutionPayload + k * sizeof(CustomMiningSolutionV2), &solution, sizeof(CustomMiningSolutionV2)); + remainedDataToSend -= sizeof(CustomMiningSolutionV2); sendItem++; } } - customMiningInternalHeader->itemSize = sizeof(CustomMiningSolution); + customMiningInternalHeader->itemSize = sizeof(CustomMiningSolutionV2); customMiningInternalHeader->itemCount = sendItem; customMiningInternalHeader->respondType = RespondCustomMiningData::solutionType; const unsigned long long respondDataSize = sizeof(CustomMiningRespondDataHeader) + customMiningInternalHeader->itemCount * customMiningInternalHeader->itemSize; @@ -1641,16 +1516,16 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long enqueueResponse( peer, (unsigned int)respondDataSize, - RespondCustomMiningData::type, header->dejavu(), respondSolution); + RespondCustomMiningData::type(), header->dejavu(), respondSolution); } else { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } } else // Unknonwn type { - enqueueResponse(peer, 0, EndResponse::type, header->dejavu(), NULL); + enqueueResponse(peer, 0, EndResponse::type(), header->dejavu(), NULL); } } } @@ -1688,7 +1563,7 @@ static void processSpecialCommand(Peer* peer, RequestResponseHeader* header) response.everIncreasingNonceAndCommandType = _request->everIncreasingNonceAndCommandType; response.epoch = _request->epoch; response.threshold = (_request->epoch < MAX_NUMBER_EPOCH) ? solutionThreshold[_request->epoch] : SOLUTION_THRESHOLD_DEFAULT; - enqueueResponse(peer, sizeof(SpecialCommandSetSolutionThresholdRequestAndResponse), SpecialCommand::type, header->dejavu(), &response); + enqueueResponse(peer, sizeof(SpecialCommandSetSolutionThresholdRequestAndResponse), SpecialCommand::type(), header->dejavu(), &response); } break; case SPECIAL_COMMAND_TOGGLE_MAIN_MODE_REQUEST: @@ -1702,25 +1577,25 @@ static void processSpecialCommand(Peer* peer, RequestResponseHeader* header) { mainAuxStatus = _request->mainModeFlag; } - enqueueResponse(peer, sizeof(SpecialCommandToggleMainModeRequestAndResponse), SpecialCommand::type, header->dejavu(), _request); + enqueueResponse(peer, sizeof(SpecialCommandToggleMainModeRequestAndResponse), SpecialCommand::type(), header->dejavu(), _request); } break; case SPECIAL_COMMAND_REFRESH_PEER_LIST: { forceRefreshPeerList = true; - enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type, header->dejavu(), request); // echo back to indicate success + enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type(), header->dejavu(), request); // echo back to indicate success } break; case SPECIAL_COMMAND_FORCE_NEXT_TICK: { forceNextTick = true; - enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type, header->dejavu(), request); // echo back to indicate success + enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type(), header->dejavu(), request); // echo back to indicate success } break; case SPECIAL_COMMAND_REISSUE_VOTE: { system.latestCreatedTick--; - enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type, header->dejavu(), request); // echo back to indicate success + enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type(), header->dejavu(), request); // echo back to indicate success } break; case SPECIAL_COMMAND_SEND_TIME: @@ -1750,7 +1625,7 @@ static void processSpecialCommand(Peer* peer, RequestResponseHeader* header) SpecialCommandSendTime response; response.everIncreasingNonceAndCommandType = (request->everIncreasingNonceAndCommandType & 0xFFFFFFFFFFFFFF) | (SPECIAL_COMMAND_SEND_TIME << 56); copyMem(&response.utcTime, &utcTime, sizeof(response.utcTime)); // caution: response.utcTime is subset of global utcTime (smaller size) - enqueueResponse(peer, sizeof(SpecialCommandSendTime), SpecialCommand::type, header->dejavu(), &response); + enqueueResponse(peer, sizeof(SpecialCommandSendTime), SpecialCommand::type(), header->dejavu(), &response); } break; case SPECIAL_COMMAND_GET_MINING_SCORE_RANKING: @@ -1770,7 +1645,7 @@ static void processSpecialCommand(Peer* peer, RequestResponseHeader* header) sizeof(requestMiningScoreRanking.everIncreasingNonceAndCommandType) + sizeof(requestMiningScoreRanking.numberOfRankings) + sizeof(requestMiningScoreRanking.rankings[0]) * requestMiningScoreRanking.numberOfRankings, - SpecialCommand::type, + SpecialCommand::type(), header->dejavu(), &requestMiningScoreRanking); } @@ -1779,14 +1654,14 @@ static void processSpecialCommand(Peer* peer, RequestResponseHeader* header) case SPECIAL_COMMAND_FORCE_SWITCH_EPOCH: { forceSwitchEpoch = true; - enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type, header->dejavu(), request); // echo back to indicate success + enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type(), header->dejavu(), request); // echo back to indicate success } break; case SPECIAL_COMMAND_CONTINUE_SWITCH_EPOCH: { epochTransitionCleanMemoryFlag = 1; - enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type, header->dejavu(), request); // echo back to indicate success + enqueueResponse(peer, sizeof(SpecialCommand), SpecialCommand::type(), header->dejavu(), request); // echo back to indicate success } break; @@ -1794,9 +1669,54 @@ static void processSpecialCommand(Peer* peer, RequestResponseHeader* header) { const auto* _request = header->getPayload(); consoleLoggingLevel = _request->loggingMode; - enqueueResponse(peer, sizeof(SpecialCommandSetConsoleLoggingModeRequestAndResponse), SpecialCommand::type, header->dejavu(), _request); + enqueueResponse(peer, sizeof(SpecialCommandSetConsoleLoggingModeRequestAndResponse), SpecialCommand::type(), header->dejavu(), _request); + } + break; + + case SPECIAL_COMMAND_SAVE_SNAPSHOT: + { + SpecialCommandSaveSnapshotRequestAndResponse response; + response.everIncreasingNonceAndCommandType = request->everIncreasingNonceAndCommandType; + response.status = SpecialCommandSaveSnapshotRequestAndResponse::UNKNOWN_FAILURE; + response.currentTick = 0; + +#if TICK_STORAGE_AUTOSAVE_MODE + if (requestPersistingNodeState) + { + response.status = SpecialCommandSaveSnapshotRequestAndResponse::SAVING_IN_PROGRESS; + } + else + { + ATOMIC_STORE32(requestPersistingNodeState, 1); + response.currentTick = system.tick; + response.status = SpecialCommandSaveSnapshotRequestAndResponse::SAVING_TRIGGERED; + } +#else + response.status = SpecialCommandSaveSnapshotRequestAndResponse::REMOTE_SAVE_MODE_DISABLED; +#endif + enqueueResponse(peer, sizeof(SpecialCommandSaveSnapshotRequestAndResponse), SpecialCommand::type(), header->dejavu(), &response); + } + break; + + case SPECIAL_COMMAND_SET_EXECUTION_FEE_MULTIPLIER: + { + const auto* _request = header->getPayload(); + executionTimeMultiplierNumerator = _request->multiplierNumerator; + executionTimeMultiplierDenominator = _request->multiplierDenominator; + enqueueResponse(peer, sizeof(SpecialCommandExecutionFeeMultiplierRequestAndResponse), SpecialCommand::type(), header->dejavu(), _request); } break; + + case SPECIAL_COMMAND_GET_EXECUTION_FEE_MULTIPLIER: + { + SpecialCommandExecutionFeeMultiplierRequestAndResponse response; + response.everIncreasingNonceAndCommandType = request->everIncreasingNonceAndCommandType; + response.multiplierNumerator = executionTimeMultiplierNumerator; + response.multiplierDenominator = executionTimeMultiplierDenominator; + enqueueResponse(peer, sizeof(SpecialCommandExecutionFeeMultiplierRequestAndResponse), SpecialCommand::type(), header->dejavu(), &response); + } + break; + } } } @@ -1816,13 +1736,27 @@ static void setNewMiningSeed() score->initMiningData(spectrumDigests[(SPECTRUM_CAPACITY * 2 - 1) - 1]); } -WeekDay gFullExternalStartTime; -WeekDay gFullExternalEndTime; +// Total number of external mining event. +// Can set to zero to disable event +static constexpr int gNumberOfFullExternalMiningEvents = sizeof(gFullExternalComputationTimes) > 0 ? sizeof(gFullExternalComputationTimes) / sizeof(gFullExternalComputationTimes[0]) : 0; +struct FullExternallEvent +{ + WeekDay startTime; + WeekDay endTime; +}; +FullExternallEvent* gFullExternalEventTime = NULL; static bool gSpecialEventFullExternalComputationPeriod = false; // a flag indicates a special event (period) that the network running 100% external computation +static WeekDay currentEventEndTime; static bool isFullExternalComputationTime(TimeDate tickDate) { + // No event + if (gNumberOfFullExternalMiningEvents <= 0) + { + return false; + } + // Get current day of the week WeekDay tickWeekDay; tickWeekDay.hour = tickDate.hour; @@ -1831,12 +1765,16 @@ static bool isFullExternalComputationTime(TimeDate tickDate) tickWeekDay.millisecond = tickDate.millisecond; tickWeekDay.dayOfWeek = getDayOfWeek(tickDate.day, tickDate.month, 2000 + tickDate.year); - - // Check if the day is in range. - if (isWeekDayInRange(tickWeekDay, gFullExternalStartTime, gFullExternalEndTime)) + // Check if the day is in range. Expect the time is not overlap. + for (int i = 0; i < gNumberOfFullExternalMiningEvents; ++i) { - gSpecialEventFullExternalComputationPeriod = true; - return true; + if (isWeekDayInRange(tickWeekDay, gFullExternalEventTime[i].startTime, gFullExternalEventTime[i].endTime)) + { + gSpecialEventFullExternalComputationPeriod = true; + + currentEventEndTime = gFullExternalEventTime[i].endTime; + return true; + } } // When not in range, and the time pass the gFullExternalEndTime. We need to make sure the ending happen @@ -1845,9 +1783,9 @@ static bool isFullExternalComputationTime(TimeDate tickDate) { // Check time pass the end time TimeDate endTimeDate = tickDate; - endTimeDate.hour = gFullExternalEndTime.hour; - endTimeDate.minute = gFullExternalEndTime.minute; - endTimeDate.second = gFullExternalEndTime.second; + endTimeDate.hour = currentEventEndTime.hour; + endTimeDate.minute = currentEventEndTime.minute; + endTimeDate.second = currentEventEndTime.second; if (compareTimeDate(tickDate, endTimeDate) == 1) { @@ -1860,117 +1798,101 @@ static bool isFullExternalComputationTime(TimeDate tickDate) } } - // The event only happen once + // Event is marked as end gSpecialEventFullExternalComputationPeriod = false; return false; } -static void checkAndSwitchMiningPhase(short tickEpoch, TimeDate tickDate) -{ - // Check if current time is for full custom mining period - static bool fullExternalTimeBegin = false; - bool restartTheMiningPhase = false; - - // Make sure the tick is valid - if (tickEpoch == system.epoch) - { - if (isFullExternalComputationTime(tickDate)) - { - // Trigger time - if (!fullExternalTimeBegin) - { - fullExternalTimeBegin = true; - - // Turn off the qubic mining phase - score->initMiningData(m256i::zero()); - } - } - else // Not in the full external time, just behavior like normal. - { - // Switch back to qubic mining phase if neccessary - if (fullExternalTimeBegin) - { - if (getTickInMiningPhaseCycle() <= INTERNAL_COMPUTATIONS_INTERVAL) - { - setNewMiningSeed(); - } - } - fullExternalTimeBegin = false; - } - } - - // Incase of the full custom mining is just end. The setNewMiningSeed() will wait for next period of qubic mining phase - if (!fullExternalTimeBegin) - { - const unsigned int r = getTickInMiningPhaseCycle(); - if (!r) - { - setNewMiningSeed(); - } - else - { - if (r == INTERNAL_COMPUTATIONS_INTERVAL + 3) // 3 is added because of 3-tick shift for transaction confirmation - { - score->initMiningData(m256i::zero()); - } - } - } -} - // Clean up before custom mining phase. Thread-safe function static void beginCustomMiningPhase() { - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - gSystemCustomMiningSolutionCache[i].reset(); - } - gSystemCustomMiningSolutionV2Cache.reset(); gCustomMiningStorage.reset(); gCustomMiningStats.phaseResetAndEpochAccumulate(); } -static void checkAndSwitchCustomMiningPhase(short tickEpoch, TimeDate tickDate) +// resetPhase: If true, allows reinitializing mining seed and the custom mining phase flag +// even when already inside the current phase. These values are normally set only once +// at the beginning of a phase. +static void checkAndSwitchMiningPhase(short tickEpoch, TimeDate tickDate, bool resetPhase) { bool isBeginOfCustomMiningPhase = false; char isInCustomMiningPhase = 0; - // Check if current time is for full custom mining period - static bool fullExternalTimeBegin = false; - - // Make sure the tick is valid - if (tickEpoch == system.epoch) + // When resetting the phase: + // - If in the internal mining phase => reset the mining seed for the new epoch + // - If in the external (custom) mining phase => reset mining data (counters, etc.) + if (resetPhase) { - if (isFullExternalComputationTime(tickDate)) + const unsigned int r = getTickInMiningPhaseCycle(); + if (r < INTERNAL_COMPUTATIONS_INTERVAL) { - // Trigger time - if (!fullExternalTimeBegin) - { - fullExternalTimeBegin = true; - isBeginOfCustomMiningPhase = true; - } - isInCustomMiningPhase = 1; + setNewMiningSeed(); } - else // Not in the full external time, just behavior like normal. + else { - fullExternalTimeBegin = false; + score->initMiningData(m256i::zero()); + isBeginOfCustomMiningPhase = true; + isInCustomMiningPhase = 1; } } - - if (!fullExternalTimeBegin) + else { - const unsigned int r = getTickInMiningPhaseCycle(); - if (r >= INTERNAL_COMPUTATIONS_INTERVAL) + // Track whether we’re currently in a full external computation window + static bool isInFullExternalTime = false; + + // Make sure the tick is valid and not in the reset phase state + if (tickEpoch == system.epoch) { - isInCustomMiningPhase = 1; - if (r == INTERNAL_COMPUTATIONS_INTERVAL) + if (isFullExternalComputationTime(tickDate)) { - isBeginOfCustomMiningPhase = true; + // Trigger time + if (!isInFullExternalTime) + { + isInFullExternalTime = true; + + // Turn off the qubic mining phase + score->initMiningData(m256i::zero()); + + // Start the custom mining phase + isBeginOfCustomMiningPhase = true; + } + isInCustomMiningPhase = 1; + } + else + { + // Not in the full external phase anymore + isInFullExternalTime = false; } } - else + + // Incase of the full custom mining is just end. The setNewMiningSeed() will wait for next period of qubic mining phase + if (!isInFullExternalTime) { - isInCustomMiningPhase = 0; + const unsigned int r = getTickInMiningPhaseCycle(); + if (!r) + { + setNewMiningSeed(); + } + else + { + if (r == INTERNAL_COMPUTATIONS_INTERVAL + 3) // 3 is added because of 3-tick shift for transaction confirmation + { + score->initMiningData(m256i::zero()); + } + + // Setting for custom mining phase + isInCustomMiningPhase = 0; + if (r >= INTERNAL_COMPUTATIONS_INTERVAL) + { + isInCustomMiningPhase = 1; + // Begin of custom mining phase. Turn the flag on so we can reset some state variables + if (r == INTERNAL_COMPUTATIONS_INTERVAL) + { + isBeginOfCustomMiningPhase = true; + } + } + } } } @@ -1984,29 +1906,6 @@ static void checkAndSwitchCustomMiningPhase(short tickEpoch, TimeDate tickDate) ACQUIRE(gIsInCustomMiningStateLock); gIsInCustomMiningState = isInCustomMiningPhase; RELEASE(gIsInCustomMiningStateLock); - -} - -// a function to check and switch mining phase especially for begin/end epoch event -// if we are in internal mining phase (no matter beginning or in the middle) => reset mining seed to new spectrum of the new epoch -// same for external mining phase => reset all counters are needed -// this function should be called after beginEpoch procedure -// TODO: merge checkMiningPhaseBeginAndEndEpoch + checkAndSwitchCustomMiningPhase + checkAndSwitchMiningPhase -static void checkMiningPhaseBeginAndEndEpoch() -{ - const unsigned int r = getTickInMiningPhaseCycle(); - if (r < INTERNAL_COMPUTATIONS_INTERVAL) - { - setNewMiningSeed(); - } - else - { - score->initMiningData(m256i::zero()); - beginCustomMiningPhase(); - ACQUIRE(gIsInCustomMiningStateLock); - gIsInCustomMiningState = 1; - RELEASE(gIsInCustomMiningStateLock); - } } // Updates the global numberTickTransactions based on the tick data in the tick storage. @@ -2127,31 +2026,31 @@ static void requestProcessor(void* ProcedureArgument) RELEASE(requestQueueTailLock); switch (header->type()) { - case ExchangePublicPeers::type: + case ExchangePublicPeers::type(): { processExchangePublicPeers(peer, header); } break; - case BroadcastMessage::type: + case BroadcastMessage::type(): { processBroadcastMessage(processorNumber, header); } break; - case BroadcastComputors::type: + case BroadcastComputors::type(): { processBroadcastComputors(peer, header); } break; - case BroadcastTick::type: + case BroadcastTick::type(): { processBroadcastTick(peer, header); } break; - case BroadcastFutureTickData::type: + case BroadcastFutureTickData::type(): { processBroadcastFutureTickData(peer, header); } @@ -2163,137 +2062,143 @@ static void requestProcessor(void* ProcedureArgument) } break; - case RequestComputors::type: + case RequestComputors::type(): { processRequestComputors(peer, header); } break; - case RequestQuorumTick::type: + case RequestQuorumTick::type(): { processRequestQuorumTick(peer, header); } break; - case RequestTickData::type: + case RequestTickData::type(): { processRequestTickData(peer, header); } break; - case REQUEST_TICK_TRANSACTIONS: + case RequestTickTransactions::type(): { processRequestTickTransactions(peer, header); } break; - case REQUEST_TRANSACTION_INFO: + case RequestTransactionInfo::type(): { processRequestTransactionInfo(peer, header); } break; - case REQUEST_CURRENT_TICK_INFO: + case RequestCurrentTickInfo::type(): { processRequestCurrentTickInfo(peer, header); } break; - case RESPOND_CURRENT_TICK_INFO: + case RespondCurrentTickInfo::type(): { processResponseCurrentTickInfo(peer, header); } break; - case REQUEST_ENTITY: + case RequestEntity::type(): { processRequestEntity(peer, header); } break; - case RequestContractIPO::type: + case RequestActiveIPOs::type(): + { + processRequestActiveIPOs(peer, header); + } + break; + + case RequestContractIPO::type(): { processRequestContractIPO(peer, header); } break; - case RequestIssuedAssets::type: + case RequestIssuedAssets::type(): { processRequestIssuedAssets(peer, header); } break; - case RequestOwnedAssets::type: + case RequestOwnedAssets::type(): { processRequestOwnedAssets(peer, header); } break; - case RequestPossessedAssets::type: + case RequestPossessedAssets::type(): { processRequestPossessedAssets(peer, header); } break; - case RequestContractFunction::type: + case RequestContractFunction::type(): { processRequestContractFunction(peer, processorNumber, header); } break; - case RequestLog::type: + case RequestLog::type(): { logger.processRequestLog(processorNumber, peer, header); } break; - case RequestLogIdRangeFromTx::type: + case RequestLogIdRangeFromTx::type(): { logger.processRequestTxLogInfo(processorNumber, peer, header); } break; - case RequestAllLogIdRangesFromTick::type: + case RequestAllLogIdRangesFromTick::type(): { logger.processRequestTickTxLogInfo(processorNumber, peer, header); } break; - case RequestPruningLog::type: + case RequestPruningLog::type(): { logger.processRequestPrunePageFile(peer, header); } break; - case RequestLogStateDigest::type: + case RequestLogStateDigest::type(): { logger.processRequestGetLogDigest(peer, header); } break; - case REQUEST_SYSTEM_INFO: + case RequestSystemInfo::type(): { processRequestSystemInfo(peer, header); } break; - case RequestAssets::type: + case RequestAssets::type(): { processRequestAssets(peer, header); } break; - case RequestedCustomMiningSolutionVerification::type: + case RequestCustomMiningSolutionVerification::type(): { processRequestedCustomMiningSolutionVerificationRequest(peer, header); } break; - case RequestedCustomMiningData::type: + case RequestCustomMiningData::type(): { processCustomMiningDataRequest(peer, processorNumber, header); } break; - case SpecialCommand::type: + case SpecialCommand::type(): { processSpecialCommand(peer, header); } @@ -2301,7 +2206,7 @@ static void requestProcessor(void* ProcedureArgument) #if ADDON_TX_STATUS_REQUEST /* qli: process RequestTxStatus message */ - case REQUEST_TX_STATUS: + case RequestTxStatus::type(): { processRequestConfirmedTx(processorNumber, peer, header); } @@ -2338,6 +2243,16 @@ static void contractProcessor(void*) if (system.epoch == contractDescriptions[executedContractIndex].constructionEpoch && system.epoch < contractDescriptions[executedContractIndex].destructionEpoch) { + // INITIALIZE is called right after IPO, hence no check for executionFeeReserve is needed. + // A failed IPO is indicated by a contractError and INITIALIZE is not executed. + + // Check if contract is in an error state + if (contractError[executedContractIndex] != NoContractError) + { + // Skip execution - contract is in error state + continue; + } + setMem(contractStates[executedContractIndex], contractDescriptions[executedContractIndex].stateSize, 0); QpiContextSystemProcedureCall qpiContext(executedContractIndex, INITIALIZE); qpiContext.call(); @@ -2353,6 +2268,16 @@ static void contractProcessor(void*) if (system.epoch >= contractDescriptions[executedContractIndex].constructionEpoch && system.epoch < contractDescriptions[executedContractIndex].destructionEpoch) { + // BEGIN_EPOCH runs even with a non-positive executionFeeReserve + // to keep SC in a valid state. + + // Check if contract is in an error state + if (contractError[executedContractIndex] != NoContractError) + { + // Skip execution - contract is in error state + continue; + } + QpiContextSystemProcedureCall qpiContext(executedContractIndex, BEGIN_EPOCH); qpiContext.call(); } @@ -2367,6 +2292,19 @@ static void contractProcessor(void*) if (system.epoch >= contractDescriptions[executedContractIndex].constructionEpoch && system.epoch < contractDescriptions[executedContractIndex].destructionEpoch) { + // Check if contract has sufficient execution fee reserve before executing + if (getContractFeeReserve(executedContractIndex) <= 0) + { + // Skip execution - contract has insufficient fees + continue; + } + // Check if contract is in an error state + if (contractError[executedContractIndex] != NoContractError) + { + // Skip execution - contract is in error state + continue; + } + QpiContextSystemProcedureCall qpiContext(executedContractIndex, BEGIN_TICK); qpiContext.call(); } @@ -2381,6 +2319,19 @@ static void contractProcessor(void*) if (system.epoch >= contractDescriptions[executedContractIndex].constructionEpoch && system.epoch < contractDescriptions[executedContractIndex].destructionEpoch) { + // Check if contract has sufficient execution fee reserve before executing + if (getContractFeeReserve(executedContractIndex) <= 0) + { + // Skip execution - contract has insufficient fees + continue; + } + // Check if contract is in an error state + if (contractError[executedContractIndex] != NoContractError) + { + // Skip execution - contract is in error state + continue; + } + QpiContextSystemProcedureCall qpiContext(executedContractIndex, END_TICK); qpiContext.call(); } @@ -2395,6 +2346,16 @@ static void contractProcessor(void*) if (system.epoch >= contractDescriptions[executedContractIndex].constructionEpoch && system.epoch < contractDescriptions[executedContractIndex].destructionEpoch) { + // END_EPOCH runs even with a non-positive executionFeeReserve + // to keep SC in a valid state. + + // Check if contract is in an error state + if (contractError[executedContractIndex] != NoContractError) + { + // Skip execution - contract is in error state + continue; + } + QpiContextSystemProcedureCall qpiContext(executedContractIndex, END_EPOCH); qpiContext.call(); } @@ -2457,6 +2418,20 @@ static void contractProcessor(void*) contractProcessorTransaction = 0; } break; + + case USER_PROCEDURE_NOTIFICATION_CALL: + { + const auto* notification = contractProcessorUserProcedureNotification; + ASSERT(notification && notification->procedure && notification->inputPtr); + ASSERT(notification->inputSize <= MAX_INPUT_SIZE); + ASSERT(notification->localsSize <= MAX_SIZE_OF_CONTRACT_LOCALS); + + QpiContextUserProcedureNotificationCall qpiContext(*notification); + qpiContext.call(); + + contractProcessorUserProcedureNotification = 0; + } + break; } if (!isVirtualMachine) @@ -2511,7 +2486,7 @@ static void processTickTransactionContractIPO(const Transaction* transaction, co ASSERT(!transaction->amount && transaction->inputSize == sizeof(ContractIPOBid)); ASSERT(spectrumIndex >= 0); ASSERT(contractIndex < contractCount); - ASSERT(system.epoch < contractDescriptions[contractIndex].constructionEpoch); + ASSERT(system.epoch == (contractDescriptions[contractIndex].constructionEpoch - 1)); ContractIPOBid* contractIPOBid = (ContractIPOBid*)transaction->inputPtr(); bidInContractIPO(contractIPOBid->price, contractIPOBid->quantity, transaction->sourcePublicKey, spectrumIndex, contractIndex); @@ -2903,6 +2878,12 @@ static void processTickTransaction(const Transaction* transaction, const m256i& } break; + case EXECUTION_FEE_REPORT_INPUT_TYPE: + { + executionFeeReportCollector.processTransactionData(transaction, dataLock); + } + break; + } } else @@ -2918,7 +2899,7 @@ static void processTickTransaction(const Transaction* transaction, const m256i& && contractIndex < contractCount) { // Contract transactions - if (system.epoch < contractDescriptions[contractIndex].constructionEpoch) + if (system.epoch == (contractDescriptions[contractIndex].constructionEpoch - 1)) { // IPO if (!transaction->amount @@ -2927,10 +2908,32 @@ static void processTickTransaction(const Transaction* transaction, const m256i& processTickTransactionContractIPO(transaction, spectrumIndex, contractIndex); } } - else if (system.epoch < contractDescriptions[contractIndex].destructionEpoch) + else if (system.epoch >= contractDescriptions[contractIndex].constructionEpoch + && system.epoch < contractDescriptions[contractIndex].destructionEpoch) { - // Regular contract procedure invocation - moneyFlew = processTickTransactionContractProcedure(transaction, spectrumIndex, contractIndex); + // Check if contract has sufficient execution fee reserve and is not in an error state + if (getContractFeeReserve(contractIndex) <= 0 || contractError[contractIndex] != NoContractError) + { + // Contract has insufficient execution fees or is in error state - refund transaction amount + if (transaction->amount > 0) + { + int destIndex = ::spectrumIndex(transaction->destinationPublicKey); + if (destIndex >= 0) + { + decreaseEnergy(destIndex, transaction->amount); + increaseEnergy(transaction->sourcePublicKey, transaction->amount); + + const QuTransfer quTransfer = { transaction->destinationPublicKey, transaction->sourcePublicKey, transaction->amount }; + logger.logQuTransfer(quTransfer); + } + } + moneyFlew = false; + } + else + { + // Regular contract procedure invocation + moneyFlew = processTickTransactionContractProcedure(transaction, spectrumIndex, contractIndex); + } } } } @@ -3019,10 +3022,78 @@ static bool makeAndBroadcastCustomMiningTransaction(int i, BroadcastFutureTickDa } ts.tickTransactions.releaseLock(); } - return true; + return true; + } + } + return false; +} + +static bool makeAndBroadcastExecutionFeeTransaction(int i, BroadcastFutureTickData& td, int txSlot) +{ + PROFILE_NAMED_SCOPE("processTick(): broadcast execution fee tx"); + ASSERT(txSlot < NUMBER_OF_TRANSACTIONS_PER_TICK); + + auto& payload = executionFeeReportPayload; + payload.transaction.sourcePublicKey = computorPublicKeys[ownComputorIndicesMapping[i]]; + payload.transaction.destinationPublicKey = m256i::zero(); + payload.transaction.amount = 0; + payload.transaction.tick = system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET; + payload.transaction.inputType = ExecutionFeeReportTransactionPrefix::transactionType(); + + // Build the payload with contract execution times + executionTimeAccumulator.acquireLock(); + unsigned int entryCount = buildExecutionFeeReportPayload( + payload, + executionTimeAccumulator.getPrevPhaseAccumulatedTimes(), + (system.tick / NUMBER_OF_COMPUTORS) - 1, + executionTimeMultiplierNumerator, + executionTimeMultiplierDenominator + ); + executionTimeAccumulator.releaseLock(); + + // Return if no contract was executed during last phase + if (entryCount == 0) + { + return false; + } + + // Set datalock at the end of the compacted payload + m256i* datalockPtr = (m256i*)(payload.transaction.inputPtr() + payload.transaction.inputSize - sizeof(m256i)); + *datalockPtr = td.tickData.timelock; + + // Calculate the correct position of the signature as this is a variable length package + unsigned char* signaturePtr = ((unsigned char*)datalockPtr) + sizeof(ExecutionFeeReportTransactionPostfix::dataLock); + + unsigned char digest[32]; + unsigned int sizeToHash = sizeof(Transaction) + payload.transaction.inputSize; + KangarooTwelve(&payload, sizeToHash, digest, sizeof(digest)); + sign(computorSubseeds[ownComputorIndicesMapping[i]].m256i_u8, computorPublicKeys[ownComputorIndicesMapping[i]].m256i_u8, digest, signaturePtr); + + // Broadcast ExecutionFeeReport + unsigned int transactionSize = sizeToHash + sizeof(ExecutionFeeReportTransactionPostfix::signature); + enqueueResponse(NULL, transactionSize, BROADCAST_TRANSACTION, 0, &payload); + + // Copy the content of this exectuion fee report to local memory + unsigned int tickIndex = ts.tickToIndexCurrentEpoch(td.tickData.tick); + KangarooTwelve(&payload, transactionSize, digest, sizeof(digest)); + auto* tsReqTickTransactionOffsets = ts.tickTransactionOffsets.getByTickIndex(tickIndex); + if (txSlot < NUMBER_OF_TRANSACTIONS_PER_TICK) // valid slot + { + // TODO: refactor function add transaction to txStorage + ts.tickTransactions.acquireLock(); + if (!tsReqTickTransactionOffsets[txSlot]) // not yet have value + { + if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) //have enough space + { + td.tickData.transactionDigests[txSlot] = m256i(digest); + tsReqTickTransactionOffsets[txSlot] = ts.nextTickTransactionOffset; + copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), &payload, transactionSize); + ts.nextTickTransactionOffset += transactionSize; + } } + ts.tickTransactions.releaseLock(); } - return false; + return true; } OPTIMIZE_OFF() @@ -3064,10 +3135,21 @@ static void processTick(unsigned long long processorNumber) // it should never go here } - if (system.tick == system.initialTick) + // Ensure to only call INITIALIZE and BEGIN_EPOCH once per epoch: + // system.initialTick usually is the first tick of the epoch, except when the network is restarted + // from scratch with a new TICK (which shall be indicated by TICK_IS_FIRST_TICK_OF_EPOCH == 0). + // However, after seamless epoch transition (system.epoch > EPOCH), system.initialTick is the first + // tick of the epoch in any case. + if (system.tick == system.initialTick && (TICK_IS_FIRST_TICK_OF_EPOCH || system.epoch > EPOCH)) { - PROFILE_NAMED_SCOPE_BEGIN("processTick(): INITIALIZE"); - logger.registerNewTx(system.tick, logger.SC_INITIALIZE_TX); + { + // this is the very first logging event of the epoch + // hint message for 3rd party services the start of the epoch + logger.registerNewTx(system.tick, logger.SC_INITIALIZE_TX); + DummyCustomMessage dcm{ CUSTOM_MESSAGE_OP_START_EPOCH }; + logger.logCustomMessage(dcm); + } + PROFILE_NAMED_SCOPE_BEGIN("processTick(): INITIALIZE"); contractProcessorPhase = INITIALIZE; contractProcessorState = 1; WAIT_WHILE(contractProcessorState); @@ -3177,6 +3259,14 @@ static void processTick(unsigned long long processorNumber) PROFILE_SCOPE_END(); } + // The last executionFeeReport for the previous phase is published by comp (0-indexed) in the last tick t1 of the + // previous phase (t1 % NUMBER_OF_COMPUTORS == NUMBER_OF_COMPUTORS - 1) for inclusion in tick t2 = t1 + TICK_TRANSACTIONS_PUBLICATION_OFFSET. + // Tick t2 corresponds to tick of the current phase. + if (system.tick % NUMBER_OF_COMPUTORS == TICK_TRANSACTIONS_PUBLICATION_OFFSET - 1) + { + executionFeeReportCollector.processReports(); + } + PROFILE_NAMED_SCOPE_BEGIN("processTick(): END_TICK"); logger.registerNewTx(system.tick, logger.SC_END_TICK_TX); contractProcessorPhase = END_TICK; @@ -3288,7 +3378,7 @@ static void processTick(unsigned long long processorNumber) // This is the tick leader in MAIN mode -> construct future tick data (selecting transactions to // include into tick) - broadcastedFutureTickData.tickData.computorIndex = ownComputorIndices[i] ^ BroadcastFutureTickData::type; // We XOR almost all packets with their type value to make sure an entity cannot be tricked into signing one thing while actually signing something else + broadcastedFutureTickData.tickData.computorIndex = ownComputorIndices[i] ^ BroadcastFutureTickData::type(); // We XOR almost all packets with their type value to make sure an entity cannot be tricked into signing one thing while actually signing something else broadcastedFutureTickData.tickData.epoch = system.epoch; broadcastedFutureTickData.tickData.tick = system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET; @@ -3307,119 +3397,73 @@ static void processTick(unsigned long long processorNumber) timelockPreimage[2] = etalonTick.saltedComputerDigest; KangarooTwelve(timelockPreimage, sizeof(timelockPreimage), &broadcastedFutureTickData.tickData.timelock, sizeof(broadcastedFutureTickData.tickData.timelock)); - unsigned int j = 0; - - ACQUIRE(computorPendingTransactionsLock); - - // Get indices of pending computor transactions that are scheduled to be included in tickData - unsigned int numberOfEntityPendingTransactionIndices = 0; - for (unsigned int k = 0; k < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; k++) - { - const Transaction* tx = ((Transaction*)&computorPendingTransactions[k * MAX_TRANSACTION_SIZE]); - if (tx->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET) - { - entityPendingTransactionIndices[numberOfEntityPendingTransactionIndices++] = k; - } - } - - // Randomly select computor tx scheduled for the tick until tick is full or all pending tx are included - while (j < NUMBER_OF_TRANSACTIONS_PER_TICK && numberOfEntityPendingTransactionIndices) + unsigned int nextTxIndex = 0; + unsigned int numPendingTickTxs = pendingTxsPool.getNumberOfPendingTickTxs(system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET); + pendingTxsPool.acquireLock(); + for (unsigned int tx = 0; tx < numPendingTickTxs; ++tx) { - const unsigned int index = random(numberOfEntityPendingTransactionIndices); - - const Transaction* pendingTransaction = ((Transaction*)&computorPendingTransactions[entityPendingTransactionIndices[index] * MAX_TRANSACTION_SIZE]); - ASSERT(pendingTransaction->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET); +// #if !defined(NDEBUG) && !defined(NO_UEFI) +// addDebugMessage(L"pendingTxsPool.get() call in processTick()"); +// #endif + const Transaction* pendingTransaction = pendingTxsPool.getTx(system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET, tx); + if (pendingTransaction) { ASSERT(pendingTransaction->checkValidity()); const unsigned int transactionSize = pendingTransaction->totalSize(); + ts.tickTransactions.acquireLock(); if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) { - ts.tickTransactions.acquireLock(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - ts.tickTransactionOffsets(pendingTransaction->tick, j) = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), (void*)pendingTransaction, transactionSize); - broadcastedFutureTickData.tickData.transactionDigests[j] = &computorPendingTransactionDigests[entityPendingTransactionIndices[index] * 32ULL]; - j++; - ts.nextTickTransactionOffset += transactionSize; - } - ts.tickTransactions.releaseLock(); + ts.tickTransactionOffsets(pendingTransaction->tick, nextTxIndex) = ts.nextTickTransactionOffset; + copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), (void*)pendingTransaction, transactionSize); + const m256i* digest = pendingTxsPool.getDigest(system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET, tx); + // digest should always be != nullptr because pendingTransaction != nullptr + ASSERT(digest); + broadcastedFutureTickData.tickData.transactionDigests[nextTxIndex] = digest ? *digest : m256i::zero(); + ts.nextTickTransactionOffset += transactionSize; + nextTxIndex++; } + ts.tickTransactions.releaseLock(); } - - entityPendingTransactionIndices[index] = entityPendingTransactionIndices[--numberOfEntityPendingTransactionIndices]; - } - - RELEASE(computorPendingTransactionsLock); - - ACQUIRE(entityPendingTransactionsLock); - - // Get indices of pending non-computor transactions that are scheduled to be included in tickData - numberOfEntityPendingTransactionIndices = 0; - for (unsigned int k = 0; k < SPECTRUM_CAPACITY; k++) - { - const Transaction* tx = ((Transaction*)&entityPendingTransactions[k * MAX_TRANSACTION_SIZE]); - if (tx->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET) + else { - entityPendingTransactionIndices[numberOfEntityPendingTransactionIndices++] = k; + break; } } + pendingTxsPool.releaseLock(); - // Randomly select non-computor tx scheduled for the tick until tick is full or all pending tx are included - while (j < NUMBER_OF_TRANSACTIONS_PER_TICK && numberOfEntityPendingTransactionIndices) { - const unsigned int index = random(numberOfEntityPendingTransactionIndices); - - const Transaction* pendingTransaction = ((Transaction*)&entityPendingTransactions[entityPendingTransactionIndices[index] * MAX_TRANSACTION_SIZE]); - ASSERT(pendingTransaction->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET); - { - ASSERT(pendingTransaction->checkValidity()); - const unsigned int transactionSize = pendingTransaction->totalSize(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - ts.tickTransactions.acquireLock(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - ts.tickTransactionOffsets(pendingTransaction->tick, j) = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), (void*)pendingTransaction, transactionSize); - broadcastedFutureTickData.tickData.transactionDigests[j] = &entityPendingTransactionDigests[entityPendingTransactionIndices[index] * 32ULL]; - j++; - ts.nextTickTransactionOffset += transactionSize; - } - ts.tickTransactions.releaseLock(); - } - } - - entityPendingTransactionIndices[index] = entityPendingTransactionIndices[--numberOfEntityPendingTransactionIndices]; + // insert & broadcast vote counter tx + makeAndBroadcastTickVotesTransaction(i, broadcastedFutureTickData, nextTxIndex++); } - - RELEASE(entityPendingTransactionsLock); - { - // insert & broadcast vote counter tx - makeAndBroadcastTickVotesTransaction(i, broadcastedFutureTickData, j++); + // insert & broadcast external mining score packet (containing the score for each computor on the last external mining phase) + // this type of tx is only broadcasted in internal mining phases + if (makeAndBroadcastCustomMiningTransaction(i, broadcastedFutureTickData, nextTxIndex)) + { + nextTxIndex++; + } } { - // insert & broadcast custom mining share - if (makeAndBroadcastCustomMiningTransaction(i, broadcastedFutureTickData, j)) // this type of tx is only broadcasted in mining phases + // include execution fees tx for phase n - 1 + if (makeAndBroadcastExecutionFeeTransaction(i, broadcastedFutureTickData, nextTxIndex)) { - j++; + nextTxIndex++; } } - for (; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) + for (; nextTxIndex < NUMBER_OF_TRANSACTIONS_PER_TICK; ++nextTxIndex) { - broadcastedFutureTickData.tickData.transactionDigests[j] = m256i::zero(); + broadcastedFutureTickData.tickData.transactionDigests[nextTxIndex] = m256i::zero(); } setMem(broadcastedFutureTickData.tickData.contractFees, sizeof(broadcastedFutureTickData.tickData.contractFees), 0); unsigned char digest[32]; KangarooTwelve(&broadcastedFutureTickData.tickData, sizeof(TickData) - SIGNATURE_SIZE, digest, sizeof(digest)); - broadcastedFutureTickData.tickData.computorIndex ^= BroadcastFutureTickData::type; + broadcastedFutureTickData.tickData.computorIndex ^= BroadcastFutureTickData::type(); sign(computorSubseeds[ownComputorIndicesMapping[i]].m256i_u8, computorPublicKeys[ownComputorIndicesMapping[i]].m256i_u8, digest, broadcastedFutureTickData.tickData.signature); - enqueueResponse(NULL, sizeof(broadcastedFutureTickData), BroadcastFutureTickData::type, 0, &broadcastedFutureTickData); + enqueueResponse(NULL, sizeof(broadcastedFutureTickData), BroadcastFutureTickData::type(), 0, &broadcastedFutureTickData); } system.latestLedTick = system.tick; @@ -3542,11 +3586,6 @@ static void resetCustomMining() gCustomMiningSharesCounter.init(); setMem(gCustomMiningSharesCount, sizeof(gCustomMiningSharesCount), 0); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - gSystemCustomMiningSolutionCache[i].reset(); - } - gSystemCustomMiningSolutionV2Cache.reset(); for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) { @@ -3574,25 +3613,19 @@ static void beginEpoch() #ifndef NDEBUG ts.checkStateConsistencyWithAssert(); + pendingTxsPool.checkStateConsistencyWithAssert(); #endif ts.beginEpoch(system.initialTick); + pendingTxsPool.beginEpoch(system.initialTick); voteCounter.init(); #ifndef NDEBUG ts.checkStateConsistencyWithAssert(); + pendingTxsPool.checkStateConsistencyWithAssert(); #endif #if ADDON_TX_STATUS_REQUEST beginEpochTxStatusRequestAddOn(system.initialTick); #endif - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; i++) - { - ((Transaction*)&computorPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick = 0; - } - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) - { - ((Transaction*)&entityPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick = 0; - } - setMem(solutionPublicationTicks, sizeof(solutionPublicationTicks), 0); setMem(faultyComputorFlags, sizeof(faultyComputorFlags), 0); @@ -3608,6 +3641,14 @@ static void beginEpoch() CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 3] = (system.epoch % 100) / 10 + L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 2] = system.epoch % 10 + L'0'; + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 4] = system.epoch / 100 + L'0'; + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 3] = (system.epoch % 100) / 10 + L'0'; + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 2] = system.epoch % 10 + L'0'; + + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 4] = system.epoch / 100 + L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 3] = (system.epoch % 100) / 10 + L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 2] = system.epoch % 10 + L'0'; + score->initMemory(); score->resetTaskQueue(); setMem(minerSolutionFlags, NUMBER_OF_MINER_SOLUTION_FLAGS / 8, 0); @@ -3641,6 +3682,7 @@ static void beginEpoch() #endif logger.reset(system.initialTick); + } @@ -3777,7 +3819,12 @@ static void endEpoch() } assetsEndEpoch(); - + { + // this is the last logging event of the epoch + // a hint message for 3rd party services the end of the epoch + DummyCustomMessage dcm{ CUSTOM_MESSAGE_OP_END_EPOCH }; + logger.logCustomMessage(dcm); + } logger.updateTick(system.tick); #if PAUSE_BEFORE_CLEAR_MEMORY // re-open request processors for other services to query @@ -3962,13 +4009,28 @@ static bool saveAllNodeStates() CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 4] = L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 3] = L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 2] = L'0'; + + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 4] = L'0'; + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 3] = L'0'; + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 2] = L'0'; + + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 4] = L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 3] = L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 2] = L'0'; + setText(message, L"Saving computer files"); logToConsole(message); - if (!saveComputer(directory)) + if (!saveContractStateFiles(directory)) + { + logToConsole(L"Failed to save contract state files"); + return false; + } + if (!saveContractExecFeeFiles(directory, /*saveAccumulatedTime=*/true)) { - logToConsole(L"Failed to save computer"); + logToConsole(L"Failed to save contract execution fee files"); return false; } + setText(message, L"Saving system to system.snp"); logToConsole(message); @@ -4107,10 +4169,24 @@ static bool loadAllNodeStates() CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 4] = L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 3] = L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 2] = L'0'; - const bool forceLoadContractFile = true; - if (!loadComputer(directory, forceLoadContractFile)) + + if (!loadContractStateFiles(directory, /*forceLoadFromFile=*/true)) + { + logToConsole(L"Failed to load contract state files"); + return false; + } + + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 4] = L'0'; + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 3] = L'0'; + CONTRACT_EXEC_FEES_ACC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_ACC_FILE_NAME[0]) - 2] = L'0'; + + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 4] = L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 3] = L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 2] = L'0'; + + if (!loadContractExecFeeFiles(directory, /*loadAccumulatedTime=*/true)) { - logToConsole(L"Failed to load computer"); + logToConsole(L"Failed to load contract execution fee files"); return false; } @@ -4441,90 +4517,56 @@ static void prepareNextTickTransactions() if (numberOfKnownNextTickTransactions != numberOfNextTickTransactions) { - // Checks if any of the missing transactions is available in the computorPendingTransaction and remove unknownTransaction flag if found - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; i++) - { - Transaction* pendingTransaction = (Transaction*)&computorPendingTransactions[i * MAX_TRANSACTION_SIZE]; - if (pendingTransaction->tick == nextTick) - { - ACQUIRE(computorPendingTransactionsLock); - - ASSERT(pendingTransaction->checkValidity()); - auto* tsPendingTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(pendingTransaction->tick); - for (unsigned int j = 0; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) - { - if (unknownTransactions[j >> 6] & (1ULL << (j & 63))) - { - if (&computorPendingTransactionDigests[i * 32ULL] == nextTickData.transactionDigests[j]) - { - ts.tickTransactions.acquireLock(); - // write tx to tick tx storage, no matter if tsNextTickTransactionOffsets[i] is 0 (new tx) - // or not (tx with digest that doesn't match tickData needs to be overwritten) - { - const unsigned int transactionSize = pendingTransaction->totalSize(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - tsPendingTransactionOffsets[j] = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), pendingTransaction, transactionSize); - ts.nextTickTransactionOffset += transactionSize; - - numberOfKnownNextTickTransactions++; - } - } - ts.tickTransactions.releaseLock(); + // Checks if any of the missing transactions is available in the pending transaction pool and remove unknownTransaction flag if found - unknownTransactions[j >> 6] &= ~(1ULL << (j & 63)); - - break; - } - } - } - - RELEASE(computorPendingTransactionsLock); - } - } - // Checks if any of the missing transactions is available in the entityPendingTransaction and remove unknownTransaction flag if found - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) + unsigned int numPendingTickTxs = pendingTxsPool.getNumberOfPendingTickTxs(nextTick); + pendingTxsPool.acquireLock(); + for (unsigned int i = 0; i < numPendingTickTxs; ++i) { - Transaction* pendingTransaction = (Transaction*)&entityPendingTransactions[i * MAX_TRANSACTION_SIZE]; - if (pendingTransaction->tick == nextTick) +// #if !defined(NDEBUG) && !defined(NO_UEFI) +// addDebugMessage(L"pendingTxsPool.get() call in prepareNextTickTransactions()"); +// #endif + Transaction* pendingTransaction = pendingTxsPool.getTx(nextTick, i); + if (pendingTransaction) { - ACQUIRE(entityPendingTransactionsLock); - ASSERT(pendingTransaction->checkValidity()); auto* tsPendingTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(pendingTransaction->tick); - for (unsigned int j = 0; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) + + const m256i* digest = pendingTxsPool.getDigest(nextTick, i); + if (digest) { - if (unknownTransactions[j >> 6] & (1ULL << (j & 63))) + for (unsigned int j = 0; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) { - if (&entityPendingTransactionDigests[i * 32ULL] == nextTickData.transactionDigests[j]) + if (unknownTransactions[j >> 6] & (1ULL << (j & 63))) { - ts.tickTransactions.acquireLock(); - // write tx to tick tx storage, no matter if tsNextTickTransactionOffsets[i] is 0 (new tx) - // or not (tx with digest that doesn't match tickData needs to be overwritten) + if (*digest == nextTickData.transactionDigests[j]) { - const unsigned int transactionSize = pendingTransaction->totalSize(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) + ts.tickTransactions.acquireLock(); + // write tx to tick tx storage, no matter if tsNextTickTransactionOffsets[i] is 0 (new tx) + // or not (tx with digest that doesn't match tickData needs to be overwritten) { - tsPendingTransactionOffsets[j] = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), pendingTransaction, transactionSize); - ts.nextTickTransactionOffset += transactionSize; + const unsigned int transactionSize = pendingTransaction->totalSize(); + if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) + { + tsPendingTransactionOffsets[j] = ts.nextTickTransactionOffset; + copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), pendingTransaction, transactionSize); + ts.nextTickTransactionOffset += transactionSize; - numberOfKnownNextTickTransactions++; + numberOfKnownNextTickTransactions++; + } } - } - ts.tickTransactions.releaseLock(); + ts.tickTransactions.releaseLock(); - unknownTransactions[j >> 6] &= ~(1ULL << (j & 63)); + unknownTransactions[j >> 6] &= ~(1ULL << (j & 63)); - break; + break; + } } } } - - RELEASE(entityPendingTransactionsLock); } } + pendingTxsPool.releaseLock(); // At this point unknownTransactions is set to 1 for all transactions that are unknown // Update requestedTickTransactions the list of txs that not exist in memory so the MAIN loop can try to fetch them from peers @@ -4532,7 +4574,8 @@ static void prepareNextTickTransactions() // As processNextTickTransactions returns tx for which the flag ist set to 0 (tx with flag set to 1 are not returned) // We check if the last tickTransactionRequest it already sent - if(requestedTickTransactions.requestedTickTransactions.tick == 0){ + if (requestedTickTransactions.requestedTickTransactions.tick == 0) + { // Initialize transactionFlags to one so that by default we do not request any transaction setMem(requestedTickTransactions.requestedTickTransactions.transactionFlags, sizeof(requestedTickTransactions.requestedTickTransactions.transactionFlags), 0xff); for (unsigned int i = 0; i < NUMBER_OF_TRANSACTIONS_PER_TICK; i++) @@ -4644,7 +4687,7 @@ static void broadcastTickVotes() copyMem(&broadcastTick.tick, &etalonTick, sizeof(Tick)); for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) { - broadcastTick.tick.computorIndex = ownComputorIndices[i] ^ BroadcastTick::type; + broadcastTick.tick.computorIndex = ownComputorIndices[i] ^ BroadcastTick::type(); broadcastTick.tick.epoch = system.epoch; m256i saltedData[2]; saltedData[0] = computorPublicKeys[ownComputorIndicesMapping[i]]; @@ -4666,11 +4709,11 @@ static void broadcastTickVotes() unsigned char digest[32]; KangarooTwelve(&broadcastTick.tick, sizeof(Tick) - SIGNATURE_SIZE, digest, sizeof(digest)); - broadcastTick.tick.computorIndex ^= BroadcastTick::type; + broadcastTick.tick.computorIndex ^= BroadcastTick::type(); signTickVote(computorSubseeds[ownComputorIndicesMapping[i]].m256i_u8, computorPublicKeys[ownComputorIndicesMapping[i]].m256i_u8, digest, broadcastTick.tick.signature); - enqueueResponse(NULL, sizeof(broadcastTick), BroadcastTick::type, 0, &broadcastTick); + enqueueResponse(NULL, sizeof(broadcastTick), BroadcastTick::type(), 0, &broadcastTick); // NOTE: here we don't copy these votes to memory, instead we wait other nodes echoing these votes back because: // - if own votes don't get echoed back, that indicates this node has internet/topo issue, and need to reissue vote (F9) // - all votes need to be processed in a single place of code (for further handling) @@ -5136,8 +5179,7 @@ static void tickProcessor(void*) if (tickDataSuits) { const int dayIndex = ::dayIndex(etalonTick.year, etalonTick.month, etalonTick.day); - if ((dayIndex == 738570 + system.epoch * 7 && etalonTick.hour >= 12) - || dayIndex > 738570 + system.epoch * 7) + if (system.tick - system.initialTick >= TESTNET_EPOCH_DURATION) { // start seamless epoch transition epochTransitionState = 1; @@ -5206,25 +5248,14 @@ static void tickProcessor(void*) system.tick++; updateNumberOfTickTransactions(); + pendingTxsPool.incrementFirstStoredTick(); - short tickEpoch = 0; - TimeDate currentTickDate; - ts.tickData.acquireLock(); - const TickData& td = ts.tickData[currentTickIndex]; - currentTickDate.millisecond = td.millisecond; - currentTickDate.second = td.second; - currentTickDate.minute = td.minute; - currentTickDate.hour = td.hour; - currentTickDate.day = td.day; - currentTickDate.month = td.month; - currentTickDate.year = td.year; - tickEpoch = td.epoch == system.epoch ? system.epoch : 0; - ts.tickData.releaseLock(); - - checkAndSwitchMiningPhase(tickEpoch, currentTickDate); - - checkAndSwitchCustomMiningPhase(tickEpoch, currentTickDate); + if (system.tick % NUMBER_OF_COMPUTORS == 0) + { + executionTimeAccumulator.startNewAccumulation(); + } + bool isBeginEpoch = false; if (epochTransitionState == 1) { @@ -5237,13 +5268,18 @@ static void tickProcessor(void*) // Save the file of revenue. This blocking save can be called from any thread saveRevenueComponents(NULL); + // Reorder futureComputors so requalifying computors keep their index + // This is needed for correct execution fee reporting across epoch boundaries + static_assert(reorgBufferSize >= stableComputorIndexBufferSize(), "reorgBuffer too small for stable computor index"); + calculateStableComputorIndex(system.futureComputors, broadcastedComputors.computors.publicKeys, reorgBuffer); + // instruct main loop to save system and wait until it is done systemMustBeSaved = true; WAIT_WHILE(systemMustBeSaved); epochTransitionState = 2; beginEpoch(); - checkMiningPhaseBeginAndEndEpoch(); + isBeginEpoch = true; // Some debug checks that we are ready for the next epoch ASSERT(system.numberOfSolutions == 0); @@ -5275,6 +5311,22 @@ static void tickProcessor(void*) } ASSERT(epochTransitionWaitingRequestProcessors >= 0 && epochTransitionWaitingRequestProcessors <= nRequestProcessorIDs); + short tickEpoch = 0; + TimeDate currentTickDate; + ts.tickData.acquireLock(); + const TickData& td = ts.tickData[currentTickIndex]; + currentTickDate.millisecond = td.millisecond; + currentTickDate.second = td.second; + currentTickDate.minute = td.minute; + currentTickDate.hour = td.hour; + currentTickDate.day = td.day; + currentTickDate.month = td.month; + currentTickDate.year = td.year; + tickEpoch = td.epoch == system.epoch ? system.epoch : 0; + ts.tickData.releaseLock(); + + checkAndSwitchMiningPhase(tickEpoch, currentTickDate, isBeginEpoch); + gTickNumberOfComputors = 0; gTickTotalNumberOfComputors = 0; targetNextTickDataDigestIsKnown = false; @@ -5331,23 +5383,27 @@ static void contractProcessorShutdownCallback(EFI_EVENT Event, void* Context) // directory: source directory to load the file. Default: NULL - load from root dir / // forceLoadFromFile: when loading node states from file, we want to make sure it load from file and ignore constructionEpoch == system.epoch case -static bool loadComputer(CHAR16* directory, bool forceLoadFromFile) +static bool loadContractStateFiles(CHAR16* directory, bool forceLoadFromFile) { logToConsole(L"Loading contract files ..."); for (unsigned int contractIndex = 0; contractIndex < contractCount; contractIndex++) { + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 9] = contractIndex / 1000 + L'0'; + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 8] = (contractIndex % 1000) / 100 + L'0'; + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 7] = (contractIndex % 100) / 10 + L'0'; + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 6] = contractIndex % 10 + L'0'; if (contractDescriptions[contractIndex].constructionEpoch == system.epoch && !forceLoadFromFile) { + setText(message, L" -> "); + appendText(message, CONTRACT_FILE_NAME); setMem(contractStates[contractIndex], contractDescriptions[contractIndex].stateSize, 0); + appendText(message, L" not loaded but initialized with zeros for construction"); + logToConsole(message); } else { - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 9] = contractIndex / 1000 + L'0'; - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 8] = (contractIndex % 1000) / 100 + L'0'; - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 7] = (contractIndex % 100) / 10 + L'0'; - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 6] = contractIndex % 10 + L'0'; long long loadedSize = load(CONTRACT_FILE_NAME, contractDescriptions[contractIndex].stateSize, contractStates[contractIndex], directory); - setText(message, L" -> "); + setText(message, L" -> "); // set the message after loading otherwise `message` will contain potential messages from load() appendText(message, CONTRACT_FILE_NAME); if (loadedSize != contractDescriptions[contractIndex].stateSize) { @@ -5367,17 +5423,36 @@ static bool loadComputer(CHAR16* directory, bool forceLoadFromFile) logToConsole(message); } } + + logToConsole(L"All contract files successfully loaded or initialized."); + return true; } -static bool saveComputer(CHAR16* directory) +static bool loadContractExecFeeFiles(CHAR16* directory, bool loadAccumulatedTime) +{ + logToConsole(L"Loading contract execution fee files..."); + + if (!executionFeeReportCollector.loadFromFile(CONTRACT_EXEC_FEES_REC_FILE_NAME, directory)) + return false; + + if (loadAccumulatedTime && !executionTimeAccumulator.loadFromFile(CONTRACT_EXEC_FEES_ACC_FILE_NAME, directory)) + return false; + + logToConsole(loadAccumulatedTime ? L"Received fee reports and accumulated execution times successfully loaded." + : L"Received fee reports successfully loaded."); + + return true; +} + +static bool saveContractStateFiles(CHAR16* directory) { logToConsole(L"Saving contract files..."); - const unsigned long long beginningTick = __rdtsc(); + unsigned long long beginningTick = __rdtsc(); - bool ok = true; unsigned long long totalSize = 0; + long long savedSize = 0; for (unsigned int contractIndex = 0; contractIndex < contractCount; contractIndex++) { @@ -5386,27 +5461,43 @@ static bool saveComputer(CHAR16* directory) CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 7] = (contractIndex % 100) / 10 + L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 6] = contractIndex % 10 + L'0'; contractStateLock[contractIndex].acquireRead(); - long long savedSize = save(CONTRACT_FILE_NAME, contractDescriptions[contractIndex].stateSize, contractStates[contractIndex], directory); + savedSize = save(CONTRACT_FILE_NAME, contractDescriptions[contractIndex].stateSize, contractStates[contractIndex], directory); contractStateLock[contractIndex].releaseRead(); totalSize += savedSize; if (savedSize != contractDescriptions[contractIndex].stateSize) { - ok = false; - - break; + return false; } } - if (ok) - { - setNumber(message, totalSize, TRUE); - appendText(message, L" bytes of the computer data are saved ("); - appendNumber(message, (__rdtsc() - beginningTick) * 1000000 / frequency, TRUE); - appendText(message, L" microseconds)."); - logToConsole(message); - return true; - } - return false; + setNumber(message, totalSize, TRUE); + appendText(message, L" bytes of the contract state files are saved ("); + appendNumber(message, (__rdtsc() - beginningTick) * 1000000 / frequency, TRUE); + appendText(message, L" microseconds)."); + logToConsole(message); + + return true; +} + +static bool saveContractExecFeeFiles(CHAR16* directory, bool saveAccumulatedTime) +{ + logToConsole(L"Saving contract execution fee files..."); + + unsigned long long beginningTick = __rdtsc(); + + if (!executionFeeReportCollector.saveToFile(CONTRACT_EXEC_FEES_REC_FILE_NAME, directory)) + return false; + + if (saveAccumulatedTime && !executionTimeAccumulator.saveToFile(CONTRACT_EXEC_FEES_ACC_FILE_NAME, directory)) + return false; + + setText(message, saveAccumulatedTime ? L"Received fee reports and accumulated execution times are saved (" + : L"Received fee reports are saved ("); + appendNumber(message, (__rdtsc() - beginningTick) * 1000000 / frequency, TRUE); + appendText(message, L" microseconds)."); + logToConsole(message); + + return true; } static bool saveSystem(CHAR16* directory) @@ -5470,13 +5561,13 @@ static bool initialize() setMem(publicPeers, sizeof(publicPeers), 0); requestedComputors.header.setSize(); - requestedComputors.header.setType(RequestComputors::type); + requestedComputors.header.setType(RequestComputors::type()); requestedQuorumTick.header.setSize(); - requestedQuorumTick.header.setType(RequestQuorumTick::type); + requestedQuorumTick.header.setType(RequestQuorumTick::type()); requestedTickData.header.setSize(); - requestedTickData.header.setType(RequestTickData::type); + requestedTickData.header.setType(RequestTickData::type()); requestedTickTransactions.header.setSize(); - requestedTickTransactions.header.setType(REQUEST_TICK_TRANSACTIONS); + requestedTickTransactions.header.setType(RequestTickTransactions::type()); requestedTickTransactions.requestedTickTransactions.tick = 0; if (!initFilesystem()) @@ -5486,22 +5577,12 @@ static bool initialize() { if (!ts.init()) return false; - if (!allocPoolWithErrorLog(L"entityPendingTransaction buffer", SPECTRUM_CAPACITY * MAX_TRANSACTION_SIZE,(void**)&entityPendingTransactions, __LINE__) || - !allocPoolWithErrorLog(L"entityPendingTransaction buffer", SPECTRUM_CAPACITY * 32ULL,(void**)&entityPendingTransactionDigests , __LINE__)) - { - return false; - } - if (!allocPoolWithErrorLog(L"computorPendingTransactions buffer", NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR * MAX_TRANSACTION_SIZE, (void**)&computorPendingTransactions, __LINE__) || - !allocPoolWithErrorLog(L"computorPendingTransactions buffer", NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR * 32ULL, (void**)&computorPendingTransactionDigests, __LINE__)) - { - return false; - } - + if (!pendingTxsPool.init()) + return false; setMem(spectrumChangeFlags, sizeof(spectrumChangeFlags), 0); - if (!initSpectrum()) return false; @@ -5512,6 +5593,7 @@ static bool initialize() return false; initContractExec(); + executionFeeReportCollector.init(); for (unsigned int contractIndex = 0; contractIndex < contractCount; contractIndex++) { unsigned long long size = contractDescriptions[contractIndex].stateSize; @@ -5527,6 +5609,12 @@ static bool initialize() } setMem(score, sizeof(*score), 0); + if (!allocPoolWithErrorLog(L"score", sizeof(*score_qpi), (void**)&score_qpi, __LINE__)) + { + return false; + } + setMem(score_qpi, sizeof(*score_qpi), 0); + setMem(solutionThreshold, sizeof(int) * MAX_NUMBER_EPOCH, 0); if (!allocPoolWithErrorLog(L"minserSolutionFlag", NUMBER_OF_MINER_SOLUTION_FLAGS / 8, (void**)&minerSolutionFlags, __LINE__)) { @@ -5567,7 +5655,7 @@ static bool initialize() lastExpectedTickTransactionDigest = m256i::zero(); - //Init custom mining data. Reset function will be called in beginEpoch() + // Init custom mining data. Reset function will be called in beginEpoch() customMiningInitialize(); beginEpoch(); @@ -5577,6 +5665,9 @@ static bool initialize() #if TICK_STORAGE_AUTOSAVE_MODE bool canLoadFromFile = loadAllNodeStates(); + + // loading might have changed system.tick, so restart pendingTxsPool + pendingTxsPool.beginEpoch(system.tick); #else bool canLoadFromFile = false; #endif @@ -5647,8 +5738,12 @@ static bool initialize() appendText(message, L"."); logToConsole(message); } - if (!loadComputer()) + if (!loadContractStateFiles()) + return false; +#ifndef START_NETWORK_FROM_SCRATCH + if (!loadContractExecFeeFiles()) return false; +#endif m256i computerDigest; { setText(message, L"Computer digest = "); @@ -5673,6 +5768,7 @@ static bool initialize() } } + initializeContractErrors(); initializeContracts(); if (loadMiningSeedFromFile) @@ -5682,7 +5778,10 @@ static bool initialize() } else { - checkMiningPhaseBeginAndEndEpoch(); + short tickEpoch = -1; + TimeDate tickDate; + setMem((void*)&tickDate, sizeof(TimeDate), 0); + checkAndSwitchMiningPhase(tickEpoch, tickDate, true); } score->loadScoreCache(system.epoch); @@ -5784,8 +5883,18 @@ static bool initialize() emptyTickResolver.lastTryClock = 0; // Convert time parameters for full custom mining time - gFullExternalStartTime = convertWeekTimeFromPackedData(FULL_EXTERNAL_COMPUTATIONS_TIME_START_TIME); - gFullExternalEndTime = convertWeekTimeFromPackedData(FULL_EXTERNAL_COMPUTATIONS_TIME_STOP_TIME); + if (gNumberOfFullExternalMiningEvents > 0) + { + if ((!allocPoolWithErrorLog(L"gFullExternalEventTime", gNumberOfFullExternalMiningEvents * sizeof(FullExternallEvent), (void**)&gFullExternalEventTime, __LINE__))) + { + return false; + } + for (int i = 0; i < gNumberOfFullExternalMiningEvents; i++) + { + gFullExternalEventTime[i].startTime = convertWeekTimeFromPackedData(gFullExternalComputationTimes[i][0]); + gFullExternalEventTime[i].endTime = convertWeekTimeFromPackedData(gFullExternalComputationTimes[i][1]); + } + } return true; } @@ -5822,24 +5931,10 @@ static void deinitialize() } } - if (computorPendingTransactionDigests) - { - freePool(computorPendingTransactionDigests); - } - if (computorPendingTransactions) - { - freePool(computorPendingTransactions); - } - if (entityPendingTransactionDigests) - { - freePool(entityPendingTransactionDigests); - } - if (entityPendingTransactions) - { - freePool(entityPendingTransactions); - } ts.deinit(); + pendingTxsPool.deinit(); + if (score) { freePool(score); @@ -6012,38 +6107,12 @@ static void logInfo() } else { - const CHAR16 alphabet[26][2] = { L"A", L"B", L"C", L"D", L"E", L"F", L"G", L"H", L"I", L"J", L"K", L"L", L"M", L"N", L"O", L"P", L"Q", L"R", L"S", L"T", L"U", L"V", L"W", L"X", L"Y", L"Z" }; - for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) - { - appendText(message, alphabet[ownComputorIndices[i] / 26]); - appendText(message, alphabet[ownComputorIndices[i] % 26]); - if (i < (unsigned int)(numberOfOwnComputorIndices - 1)) - { - appendText(message, L"+"); - } - else - { - appendText(message, L"."); - } - } + appendText(message, L"[Owning "); + appendNumber(message, numberOfOwnComputorIndices, false); + appendText(message, L" indices]"); } logToConsole(message); - unsigned int numberOfPendingTransactions = 0; - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; i++) - { - if (((Transaction*)&computorPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick > system.tick) - { - numberOfPendingTransactions++; - } - } - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) - { - if (((Transaction*)&entityPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick > system.tick) - { - numberOfPendingTransactions++; - } - } if (nextTickTransactionsSemaphore) { setText(message, L"?"); @@ -6089,7 +6158,7 @@ static void logInfo() appendNumber(message, td.millisecond % 10, FALSE); appendText(message, L".) "); } - appendNumber(message, numberOfPendingTransactions, TRUE); + appendNumber(message, pendingTxsPool.getTotalNumberOfPendingTxs(system.tick), TRUE); appendText(message, L" pending transactions."); logToConsole(message); @@ -6114,7 +6183,7 @@ static void logInfo() appendText(message, L"?"); } appendText(message, L" mcs | Total Qx execution time = "); - appendNumber(message, contractTotalExecutionTicks[QX_CONTRACT_INDEX] * 1000 / frequency, TRUE); + appendNumber(message, contractTotalExecutionTime[QX_CONTRACT_INDEX] * 1000 / frequency, TRUE); appendText(message, L" ms | Solution process time = "); appendNumber(message, solutionTotalExecutionTicks * 1000 / frequency, TRUE); appendText(message, L" ms | Spectrum reorg time = "); @@ -6593,7 +6662,14 @@ static void processKeyPresses() CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 4] = L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 3] = L'0'; CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 2] = L'0'; - saveComputer(); + + saveContractStateFiles(); + + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 4] = L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 3] = L'0'; + CONTRACT_EXEC_FEES_REC_FILE_NAME[sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME) / sizeof(CONTRACT_EXEC_FEES_REC_FILE_NAME[0]) - 2] = L'0'; + + saveContractExecFeeFiles(); #ifdef ENABLE_PROFILING gProfilingDataCollector.writeToFile(); @@ -6621,7 +6697,11 @@ static void processKeyPresses() case 0x12: { logToConsole(L"Pressed F8 key"); - requestPersistingNodeState = 1; +#if TICK_STORAGE_AUTOSAVE_MODE + ATOMIC_STORE32(requestPersistingNodeState, 1); +#else + logToConsole(L"Manual trigger saving snapshot is disabled."); +#endif } break; @@ -6730,6 +6810,11 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) { logToConsole(L"Setting up multiprocessing ..."); + #if !defined(NDEBUG) + // Set flag to false BEFORE starting any processors to avoid race condition + debugLogOnlyMainProcessorRunning = false; + #endif + unsigned int computingProcessorNumber; EFI_GUID mpServiceProtocolGuid = EFI_MP_SERVICES_PROTOCOL_GUID; bs->LocateProtocol(&mpServiceProtocolGuid, NULL, (void**)&mpServicesProtocol); @@ -6798,10 +6883,6 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) createEvent(EVT_NOTIFY_SIGNAL, TPL_CALLBACK, shutdownCallback, NULL, &processors[numberOfProcessors].event); mpServicesProtocol->StartupThisAP(mpServicesProtocol, Processor::runFunction, i, processors[numberOfProcessors].event, 0, &processors[numberOfProcessors], NULL); - #if !defined(NDEBUG) - debugLogOnlyMainProcessorRunning = false; - #endif - if (!solutionProcessorFlags[i % NUMBER_OF_SOLUTION_PROCESSORS] && !solutionProcessorFlags[i]) { @@ -6925,7 +7006,8 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) // prepare and send ExchangePublicPeers message ExchangePublicPeers* request = (ExchangePublicPeers*)&peers[i].dataToTransmit[sizeof(RequestResponseHeader)]; bool noVerifiedPublicPeers = true; - for (unsigned int k = 0; k < numberOfPublicPeers; k++) + // Only check non-private peers for handshake status + for (unsigned int k = NUMBER_OF_PRIVATE_IP; k < numberOfPublicPeers; k++) { if (publicPeers[k].isHandshaked /*&& publicPeers[k].isFullnode*/) { @@ -6943,15 +7025,24 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) } else { - // randomly select verified public peers - const unsigned int publicPeerIndex = random(numberOfPublicPeers); - if (publicPeers[publicPeerIndex].isHandshaked /*&& publicPeers[publicPeerIndex].isFullnode*/) + if (NUMBER_OF_PRIVATE_IP < numberOfPublicPeers) { - request->peers[j] = publicPeers[publicPeerIndex].address; + // randomly select verified public peers and discard private IPs + // first NUMBER_OF_PRIVATE_IP ips are same on both array publicPeers and knownPublicPeers + const unsigned int publicPeerIndex = NUMBER_OF_PRIVATE_IP + random(numberOfPublicPeers - NUMBER_OF_PRIVATE_IP); + // share the peer if it's not our private IPs and is handshaked + if (publicPeers[publicPeerIndex].isHandshaked) + { + request->peers[j] = publicPeers[publicPeerIndex].address; + } + else + { + j--; + } } else { - j--; + request->peers[j].u32 = 0; } } } @@ -6959,7 +7050,7 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) RequestResponseHeader* requestHeader = (RequestResponseHeader*)peers[i].dataToTransmit; requestHeader->setSize(); requestHeader->randomizeDejavu(); - requestHeader->setType(ExchangePublicPeers::type); + requestHeader->setType(ExchangePublicPeers::type()); peers[i].dataToTransmitSize = requestHeader->size(); _InterlockedIncrement64(&numberOfDisseminatedRequests); @@ -7147,7 +7238,8 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) } if (computerMustBeSaved) { - saveComputer(); + saveContractStateFiles(); + saveContractExecFeeFiles(); computerMustBeSaved = false; } @@ -7182,7 +7274,7 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) // Start auto save if nextAutoSaveTick == system.tick (or if the main loop has missed nextAutoSaveTick) if (system.tick >= nextPersistingNodeStateTick) { - requestPersistingNodeState = 1; + ATOMIC_STORE32(requestPersistingNodeState, 1); while (system.tick >= nextPersistingNodeStateTick) { nextPersistingNodeStateTick += TICK_STORAGE_AUTOSAVE_TICK_PERIOD; @@ -7206,7 +7298,7 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) #ifdef ENABLE_PROFILING gProfilingDataCollector.writeToFile(); #endif - requestPersistingNodeState = 0; + ATOMIC_STORE32(requestPersistingNodeState, 0); logToConsole(L"Complete saving all node states"); } #if TICK_STORAGE_AUTOSAVE_MODE == 1 diff --git a/src/score.h b/src/score.h index 0b027cff9..0921cfbf2 100644 --- a/src/score.h +++ b/src/score.h @@ -24,9 +24,6 @@ static unsigned long long top_of_stack; ////////// Scoring algorithm \\\\\\\\\\ -#define NOT_CALCULATED -127 //not yet calculated -#define NULL_INDEX -2 - constexpr unsigned char INPUT_NEURON_TYPE = 0; constexpr unsigned char OUTPUT_NEURON_TYPE = 1; constexpr unsigned char EVOLUTION_NEURON_TYPE = 2; @@ -36,30 +33,29 @@ static_assert(false, "Either AVX2 or AVX512 is required."); #endif #if defined (__AVX512F__) - static constexpr int BATCH_SIZE = 64; - static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; - static inline int popcnt512(__m512i v) - { - __m512i pc = _mm512_popcnt_epi64(v); - return (int)_mm512_reduce_add_epi64(pc); - } +static constexpr int BATCH_SIZE = 64; +static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; +static inline int popcnt512(__m512i v) +{ + __m512i pc = _mm512_popcnt_epi64(v); + return (int)_mm512_reduce_add_epi64(pc); +} #elif defined(__AVX2__) - static constexpr int BATCH_SIZE = 32; - static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; - static inline unsigned popcnt256(__m256i v) - { - return popcnt64(_mm256_extract_epi64(v, 0)) + - popcnt64(_mm256_extract_epi64(v, 1)) + - popcnt64(_mm256_extract_epi64(v, 2)) + - popcnt64(_mm256_extract_epi64(v, 3)); - } +static constexpr int BATCH_SIZE = 32; +static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; +static inline unsigned popcnt256(__m256i v) +{ + return popcnt64(_mm256_extract_epi64(v, 0)) + + popcnt64(_mm256_extract_epi64(v, 1)) + + popcnt64(_mm256_extract_epi64(v, 2)) + + popcnt64(_mm256_extract_epi64(v, 3)); +} #endif constexpr unsigned long long POOL_VEC_SIZE = (((1ULL << 32) + 64)) >> 3; // 2^32+64 bits ~ 512MB constexpr unsigned long long POOL_VEC_PADDING_SIZE = (POOL_VEC_SIZE + 200 - 1) / 200 * 200; // padding for multiple of 200 constexpr unsigned long long STATE_SIZE = 200; -static const char gLUT3States[] = { 0, 1, -1 }; static void generateRandom2Pool(const unsigned char* miningSeed, unsigned char* state, unsigned char* pool) { @@ -143,50 +139,6 @@ static void extract64Bits(unsigned long long number, char* output) } } -static void packNegPos(const char* data, - unsigned long long dataSize, - unsigned char* negMask, - unsigned char* posMask) -{ - for (unsigned long long i = 0; i < dataSize; ++i) - { - negMask[i] = (data[i] == -1); - posMask[i] = (data[i] == 1); - } -} - -static void unpackNegPos( - const unsigned char* negMask, - const unsigned char* posMask, - const unsigned long long dataSize, - char* data) -{ - for (unsigned long long i = 0; i < dataSize; ++i) - { - data[i] = 0; - if (negMask[i]) - { - data[i] = -1; - continue; - } - if (posMask[i]) - { - data[i] = 1; - continue; - } - } -} - -static char convertMaskValue(unsigned char pos, unsigned char neg) -{ - /* - value = +1 if pos=1 , neg=0 - value = -1 if pos=0 , neg=1 - value = 0 otherwise - */ - return (char)(pos - neg); -} - static void setBitValue(unsigned char* data, unsigned long long bitIdx, unsigned char bitValue) { // (data[bitIdx >> 3] & ~(1u << (bitIdx & 7u))). Set the bit at data[bitIdx >> 3] byte become zeros @@ -203,14 +155,6 @@ static unsigned char getBitValue(const unsigned char* data, unsigned long long return ((data[bitIdx >> 3] >> (bitIdx & 7u)) & 1u); } -static char getValueFromBit(const unsigned char* negMask, const unsigned char* posMask, unsigned long long bitIdx) -{ - unsigned long long idx = bitIdx; /* bit index */ - unsigned char negBit = (negMask[idx >> 3] >> (idx & 7)) & 1u; - unsigned char posBit = (posMask[idx >> 3] >> (idx & 7)) & 1u; - return convertMaskValue(posBit, negBit); -} - template static void paddingDatabits( unsigned char* data, @@ -281,7 +225,7 @@ static void packNegPosWithPadding(const char* data, const __m512i vMinus1 = _mm512_set1_epi8(-1); const __m512i vPlus1 = _mm512_set1_epi8(+1); unsigned long long k = 0; - for (; k + BATCH_SIZE < dataSizeInBits; k += BATCH_SIZE) + for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) { __m512i v = _mm512_loadu_si512(reinterpret_cast(data + k)); __mmask64 mNeg = _mm512_cmpeq_epi8_mask(v, vMinus1); @@ -300,7 +244,7 @@ static void packNegPosWithPadding(const char* data, const __m256i vMinus1 = _mm256_set1_epi8(-1); const __m256i vPlus1 = _mm256_set1_epi8(+1); unsigned long long k = 0; - for (; k + BATCH_SIZE < dataSizeInBits; k += BATCH_SIZE) + for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) { __m256i v = _mm256_loadu_si256(reinterpret_cast(data + k)); @@ -340,22 +284,6 @@ static void packNegPosWithPadding(const char* data, } } -void unpackNegPosBits(const unsigned char* negMask, - const unsigned char* posMask, - unsigned long long dataSize, - unsigned long long paddedSize, - char* out) -{ - const unsigned long long startBit = paddedSize; /* first real */ - for (unsigned long long i = 0; i < dataSize; ++i) - { - unsigned long long idx = startBit + i; /* bit index */ - unsigned char negBit = (negMask[idx >> 3] >> (idx & 7)) & 1u; - unsigned char posBit = (posMask[idx >> 3] >> (idx & 7)) & 1u; - out[i] = convertMaskValue(posBit, negBit); - } -} - // Load 256/512 values start from a bit index into a m512 or m256 register #if defined (__AVX512F__) static inline __m512i load512Bits(const unsigned char* array, unsigned long long bitLocation) @@ -377,7 +305,7 @@ static inline __m512i load512Bits(const unsigned char* array, unsigned long long static inline __m256i load256Bits(const unsigned char* array, unsigned long long bitLocation) { const unsigned long long byteIndex = bitLocation >> 3; - const unsigned long long bitOffset = bitLocation & 7ULL; + const int bitOffset = (int)(bitLocation & 7ULL); // Load a 256-bit (32-byte) vector starting at the byte index. const __m256i v = _mm256_loadu_si256(reinterpret_cast(array + byteIndex)); @@ -465,7 +393,7 @@ struct ScoreFunction typedef char Neuron; typedef unsigned char NeuronType; - + // Data for roll back struct ANN { @@ -875,53 +803,158 @@ struct ScoreFunction { unsigned long long population = currentANN.population; - // Prepare the padding regions - currentANN.prepareData(); - // Test copy padding unsigned char* pPaddingNeuronMinus = currentANN.neuronMinus1s; unsigned char* pPaddingNeuronPlus = currentANN.neuronPlus1s; - unsigned char* pPaddingSynapseMinus = currentANN.synapseMinus1s; unsigned char* pPaddingSynapsePlus = currentANN.synapsePlus1s; paddingDatabits(pPaddingNeuronMinus, population); paddingDatabits(pPaddingNeuronPlus, population); + #if defined (__AVX512F__) - const unsigned long long chunks = incommingSynapsesPitch >> 9; /* bits/512 */ -#else - const unsigned long long chunks = incommingSynapsesPitch >> 8; /* bits/256 */ -#endif - for (unsigned long long n = 0; n < population; ++n, pPaddingSynapsePlus += incommingSynapseBatchSize, pPaddingSynapseMinus += incommingSynapseBatchSize) + constexpr unsigned long long chunks = incommingSynapsesPitch >> 9; + __m512i minusBlock[chunks]; + __m512i minusNext[chunks]; + __m512i plusBlock[chunks]; + __m512i plusNext[chunks]; + + constexpr unsigned long long blockSizeNeurons = 64ULL; + constexpr unsigned long long bytesPerWord = 8ULL; + + unsigned long long n = 0; + const unsigned long long lastBlock = (population / blockSizeNeurons) * blockSizeNeurons; + for (; n < lastBlock; n += blockSizeNeurons) + { + // byteIndex = start byte for word containing neuron n + unsigned long long byteIndex = ((n >> 6) << 3); // (n / 64) * 8 + unsigned long long curIdx = byteIndex; + unsigned long long nextIdx = byteIndex + bytesPerWord; // +8 bytes + + // Load the neuron windows once per block for all chunks + unsigned long long loadCur = curIdx; + unsigned long long loadNext = nextIdx; + for (unsigned blk = 0; blk < chunks; ++blk, loadCur += BATCH_SIZE, loadNext += BATCH_SIZE) + { + plusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadCur)); + plusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadNext)); + minusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadCur)); + minusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadNext)); + } + + __m512i sh = _mm512_setzero_si512(); + __m512i sh64 = _mm512_set1_epi64(64); + const __m512i ones512 = _mm512_set1_epi64(1); + + // For each neuron inside this 64-neuron block + for (unsigned int lane = 0; lane < 64; ++lane) + { + const unsigned long long current_n = n + lane; + // synapse pointers for this neuron + unsigned char* pSynapsePlus = pPaddingSynapsePlus + current_n * incommingSynapseBatchSize; + unsigned char* pSynapseMinus = pPaddingSynapseMinus + current_n * incommingSynapseBatchSize; + + __m512i plusPopulation = _mm512_setzero_si512(); + __m512i minusPopulation = _mm512_setzero_si512(); + + for (unsigned blk = 0; blk < chunks; ++blk) + { + const __m512i synP = _mm512_loadu_si512((const void*)(pSynapsePlus + blk * BATCH_SIZE)); + const __m512i synM = _mm512_loadu_si512((const void*)(pSynapseMinus + blk * BATCH_SIZE)); + + // stitch 64-bit lanes: cur >> s | next << (64 - s) + __m512i neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(plusBlock[blk], sh), _mm512_sllv_epi64(plusNext[blk], sh64)); + __m512i neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(minusBlock[blk], sh), _mm512_sllv_epi64(minusNext[blk], sh64)); + + __m512i tmpP = _mm512_and_si512(neuronMinus, synM); + const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synP, tmpP, 234); + + __m512i tmpM = _mm512_and_si512(neuronMinus, synP); + const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synM, tmpM, 234); + + plusPopulation = _mm512_add_epi64(plusPopulation, _mm512_popcnt_epi64(plus)); + minusPopulation = _mm512_add_epi64(minusPopulation, _mm512_popcnt_epi64(minus)); + } + sh = _mm512_add_epi64(sh, ones512); + sh64 = _mm512_sub_epi64(sh64, ones512); + + // Reduce to scalar and compute neuron value + int score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + char neuronValue = (score > 0) - (score < 0); + neuronValueBuffer[current_n] = neuronValue; + + // Update the neuron positive and negative bitmaps + unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; + unsigned char nNextPos = neuronValue > 0 ? 1 : 0; + setBitValue(currentANN.nextneuronMinus1s, current_n + radius, nNextNeg); + setBitValue(currentANN.nextNeuronPlus1s, current_n + radius, nNextPos); + } + } + + for (; n < population; ++n) { char neuronValue = 0; int score = 0; - int synapseBlkIdx = 0; // blk index of synapse - int neuronBlkIdx = 0; - for (unsigned blk = 0; blk < chunks; ++blk, synapseBlkIdx += BATCH_SIZE, neuronBlkIdx +=BATCH_SIZE_X8) + unsigned char* pSynapsePlus = pPaddingSynapsePlus + n * incommingSynapseBatchSize; + unsigned char* pSynapseMinus = pPaddingSynapseMinus + n * incommingSynapseBatchSize; + + const unsigned long long byteIndex = n >> 3; + const unsigned int bitOffset = (n & 7U); + const unsigned int bitOffset_8 = (8u - bitOffset); + __m512i sh = _mm512_set1_epi64((long long)bitOffset); + __m512i sh8 = _mm512_set1_epi64((long long)bitOffset_8); + + __m512i plusPopulation = _mm512_setzero_si512(); + __m512i minusPopulation = _mm512_setzero_si512(); + + for (unsigned blk = 0; blk < chunks; ++blk, pSynapsePlus += BATCH_SIZE, pSynapseMinus += BATCH_SIZE) { -#if defined (__AVX512F__) - const __m512i synapsePlus = _mm512_loadu_si512((const void*)(pPaddingSynapsePlus + synapseBlkIdx)); - const __m512i synapseMinus = _mm512_loadu_si512((const void*)(pPaddingSynapseMinus + synapseBlkIdx)); + const __m512i synapsePlus = _mm512_loadu_si512((const void*)(pSynapsePlus)); + const __m512i synapseMinus = _mm512_loadu_si512((const void*)(pSynapseMinus)); - __m512i neuronPlus = load512Bits(pPaddingNeuronPlus, n + neuronBlkIdx); - __m512i neuronMinus = load512Bits(pPaddingNeuronMinus, n + neuronBlkIdx); + __m512i neuronPlus = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE)); + __m512i neuronPlusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE + 1)); + __m512i neuronMinus = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE)); + __m512i neuronMinusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE + 1)); - //__m512i plus = _mm512_or_si512(_mm512_and_si512(neuronPlus, synapsePlus), - // _mm512_and_si512(neuronMinus, synapseMinus)); + neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(neuronPlus, sh), _mm512_sllv_epi64(neuronPlusNext, sh8)); + neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(neuronMinus, sh), _mm512_sllv_epi64(neuronMinusNext, sh8)); - //__m512i minus = _mm512_or_si512(_mm512_and_si512(neuronPlus, synapseMinus), - // _mm512_and_si512(neuronMinus, synapsePlus)); + __m512i tempP = _mm512_and_si512(neuronMinus, synapseMinus); + const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synapsePlus, tempP, 234); - const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synapsePlus, _mm512_and_si512(neuronMinus, synapseMinus), 234); - const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synapseMinus, _mm512_and_si512(neuronMinus, synapsePlus), 234); + __m512i tempM = _mm512_and_si512(neuronMinus, synapsePlus); + const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synapseMinus, tempM, 234); - const __m512i plusPopulation = _mm512_popcnt_epi64(plus); - const __m512i minusPopulation = _mm512_popcnt_epi64(minus); - score += (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + tempP = _mm512_popcnt_epi64(plus); + tempM = _mm512_popcnt_epi64(minus); + plusPopulation = _mm512_add_epi64(tempP, plusPopulation); + minusPopulation = _mm512_add_epi64(tempM, minusPopulation); + } + score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + neuronValue = (score > 0) - (score < 0); + neuronValueBuffer[n] = neuronValue; + + unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; + unsigned char nNextPos = neuronValue > 0 ? 1 : 0; + setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); + setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); + } #else + constexpr unsigned long long chunks = incommingSynapsesPitch >> 8; + for (unsigned long long n = 0; n < population; ++n, pPaddingSynapsePlus += incommingSynapseBatchSize, pPaddingSynapseMinus += incommingSynapseBatchSize) + { + char neuronValue = 0; + int score = 0; + unsigned char* pSynapsePlus = pPaddingSynapsePlus; + unsigned char* pSynapseMinus = pPaddingSynapseMinus; + + int synapseBlkIdx = 0; // blk index of synapse + int neuronBlkIdx = 0; + for (unsigned blk = 0; blk < chunks; ++blk, synapseBlkIdx += BATCH_SIZE, neuronBlkIdx += BATCH_SIZE_X8) + { // Process 256bits at once, neigbor shilf 64 bytes = 256 bits const __m256i synapsePlus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapsePlus + synapseBlkIdx)); const __m256i synapseMinus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapseMinus + synapseBlkIdx)); @@ -936,7 +969,6 @@ struct ScoreFunction _mm256_and_si256(neuronMinus, synapsePlus)); score += popcnt256(plus) - popcnt256(minus); -#endif } neuronValue = (score > 0) - (score < 0); @@ -947,8 +979,11 @@ struct ScoreFunction unsigned char nNextPos = neuronValue > 0 ? 1 : 0; setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); - } +#endif + + + copyMem(currentANN.neurons, neuronValueBuffer, population * sizeof(Neuron)); copyMem(currentANN.neuronMinus1s, currentANN.nextneuronMinus1s, sizeof(currentANN.neuronMinus1s)); copyMem(currentANN.neuronPlus1s, currentANN.nextNeuronPlus1s, sizeof(currentANN.neuronPlus1s)); @@ -963,16 +998,8 @@ struct ScoreFunction // Save the neuron value for comparison copyMem(previousNeuronValue, neurons, population * sizeof(Neuron)); - - const __m512i vPop = _mm512_set1_epi64(population); - const __m512i vPitch = _mm512_set1_epi64(static_cast(incommingSynapsesPitch)); - const __m512i vStrideL = _mm512_set1_epi64(incommingSynapsesPitch - 1); - const __m512i vStrideR = _mm512_set1_epi64(incommingSynapsesPitch + 1); - const __m512i vNeighbor = _mm512_set1_epi64(static_cast(numberOfNeighbors)); - - const __m512i lane01234567 = _mm512_set_epi64(7, 6, 5, 4, 3, 2, 1, 0); // constant - long long test[8] = { 0 }; { + //PROFILE_NAMED_SCOPE("convertSynapse"); // Compute the incomming synapse of each neurons setMem(paddingIncommingSynapses, sizeof(paddingIncommingSynapses), 0); for (unsigned long long n = 0; n < population; ++n) @@ -1000,6 +1027,7 @@ struct ScoreFunction // Prepare masks { + //PROFILE_NAMED_SCOPE("prepareMask"); packNegPosWithPadding(currentANN.neurons, population, radius, @@ -1014,6 +1042,7 @@ struct ScoreFunction } { + //PROFILE_NAMED_SCOPE("processTickLoop"); for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) { processTick(); @@ -1021,25 +1050,9 @@ struct ScoreFunction // - N ticks have passed (already in for loop) // - All neuron values are unchanged // - All output neurons have non-zero values - bool shouldExit = true; - bool allNeuronsUnchanged = true; - bool allOutputNeuronsIsNonZeros = true; - for (unsigned long long n = 0; n < population; ++n) - { - // Neuron unchanged check - if (previousNeuronValue[n] != neurons[n]) - { - allNeuronsUnchanged = false; - } - - // Ouput neuron value check - if (neuronTypes[n] == OUTPUT_NEURON_TYPE && neurons[n] == 0) - { - allOutputNeuronsIsNonZeros = false; - } - } - if (allOutputNeuronsIsNonZeros || allNeuronsUnchanged) + if (areAllNeuronsUnchanged((const char*)previousNeuronValue, (const char*)neurons, population) + || areAllNeuronsZeros((const char*)neurons, (const char*)neuronTypes, population)) { break; } @@ -1050,6 +1063,111 @@ struct ScoreFunction } } + bool areAllNeuronsZeros( + const char* neurons, + const char* neuronTypes, + unsigned long long population) + { + +#if defined (__AVX512F__) + const __m512i zero = _mm512_setzero_si512(); + const __m512i typeOutput = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); + + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); + __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); + + __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutput); + __mmask64 zero_mask = _mm512_cmpeq_epi8_mask(cur, zero); + + if (type_mask & zero_mask) + return false; + } +#else + const __m256i zero = _mm256_setzero_si256(); + const __m256i typeOutput = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); + + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m256i cur = _mm256_loadu_si256((const __m256i*)(neurons + i)); + __m256i types = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); + + // Compare for type == OUTPUT + __m256i type_cmp = _mm256_cmpeq_epi8(types, typeOutput); + int type_mask = _mm256_movemask_epi8(type_cmp); + + // Compare for neuron == 0 + __m256i zero_cmp = _mm256_cmpeq_epi8(cur, zero); + int zero_mask = _mm256_movemask_epi8(zero_cmp); + + // If both masks overlap → some output neuron is zero + if (type_mask & zero_mask) + { + return false; + } + } + +#endif + for (; i < population; i++) + { + // Neuron unchanged check + if (neuronTypes[i] == OUTPUT_NEURON_TYPE && neurons[i] == 0) + { + return false; + } + } + + return true; + } + + bool areAllNeuronsUnchanged( + const char* previousNeuronValue, + const char* neurons, + unsigned long long population) + { + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + +#if defined (__AVX512F__) + __m512i prev = _mm512_loadu_si512((const void*)(previousNeuronValue + i)); + __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); + + __mmask64 neq_mask = _mm512_cmpneq_epi8_mask(prev, cur); + if (neq_mask) + { + return false; + } +#else + __m256i v_prev = _mm256_loadu_si256((const __m256i*)(previousNeuronValue + i)); + __m256i v_curr = _mm256_loadu_si256((const __m256i*)(neurons + i)); + __m256i cmp = _mm256_cmpeq_epi8(v_prev, v_curr); + + int mask = _mm256_movemask_epi8(cmp); + + // -1 means all bytes equal + if (mask != -1) + { + return false; + } +#endif + } + + for (; i < population; i++) + { + // Neuron unchanged check + if (previousNeuronValue[i] != neurons[i]) + { + return false; + } + } + + return true; + } + unsigned int computeNonMatchingOutput() { unsigned long long population = currentANN.population; @@ -1060,7 +1178,61 @@ struct ScoreFunction // Because the output neuron order never changes, the order is preserved unsigned int R = 0; unsigned long long outputIdx = 0; - for (unsigned long long i = 0; i < population; i++) + unsigned long long i = 0; +#if defined (__AVX512F__) + const __m512i typeOutputAVX = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + // Load 64 neuron types and compare with OUTPUT_NEURON_TYPE + __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); + __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutputAVX); + + if (type_mask == 0) + { + continue; // no output neurons in this 64-wide block, just skip + } + + // Output neuron existed in this block + for (int k = 0; k < BATCH_SIZE; ++k) + { + if (type_mask & (1ULL << k)) + { + char neuronVal = neurons[i + k]; + if (neuronVal != outputNeuronExpectedValue[outputIdx]) + { + R++; + } + outputIdx++; + } + } + } +#else + const __m256i typeOutputAVX = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m256i types_vec = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); + __m256i cmp_vec = _mm256_cmpeq_epi8(types_vec, typeOutputAVX); + unsigned int type_mask = _mm256_movemask_epi8(cmp_vec); + + if (type_mask == 0) + { + continue; // no output neurons in this 32-wide block, just skip + } + for (int k = 0; k < BATCH_SIZE; ++k) + { + if (type_mask & (1U << k)) + { + char neuronVal = neurons[i + k]; + if (neuronVal != outputNeuronExpectedValue[outputIdx]) + R++; + outputIdx++; + } + } + } +#endif + + // remainder loop + for (; i < population; i++) { if (neuronTypes[i] == OUTPUT_NEURON_TYPE) { @@ -1071,6 +1243,7 @@ struct ScoreFunction outputIdx++; } } + return R; } @@ -1162,7 +1335,7 @@ struct ScoreFunction } void initializeRandom2( - const unsigned char* publicKey, + const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* pRandom2Pool) { @@ -1205,9 +1378,9 @@ struct ScoreFunction unsigned char extractValue = (unsigned char)((initValue->synapseWeight[i] >> shiftVal) & mask); switch (extractValue) { - case 2: synapses[32 * i + j] = -1; break; - case 3: synapses[32 * i + j] = 1; break; - default: synapses[32 * i + j] = 0; + case 2: synapses[32 * i + j] = -1; break; + case 3: synapses[32 * i + j] = 1; break; + default: synapses[32 * i + j] = 0; } } } @@ -1239,7 +1412,7 @@ struct ScoreFunction { // Setup the random starting point initializeRandom2(publicKey, nonce, pRandom2Pool); - + // Initialize unsigned int bestR = initializeANN(); @@ -1275,13 +1448,51 @@ struct ScoreFunction currentANN.copyDataTo(bestANN); } - ASSERT(bestANN.population <= populationThreshold); + //ASSERT(bestANN.population <= populationThreshold); } unsigned int score = numberOfOutputNeurons - bestR; return score; } + // returns last computed output neurons, only returns 256 non-zero neurons, neuron values are compressed to bit + m256i getLastOutput() + { + unsigned long long population = bestANN.population; + Neuron* neurons = bestANN.neurons; + NeuronType* neuronTypes = bestANN.neuronTypes; + int count = 0; + int byteCount = 0; + uint8_t A = 0; + m256i result; + result = m256i::zero(); + + for (unsigned long long i = 0; i < population; i++) + { + if (neuronTypes[i] == OUTPUT_NEURON_TYPE) + { + if (neurons[i]) + { + uint8_t v = (neurons[i] > 0); + v = v << (7 - count); + A |= v; + if (++count == 8) + { + result.m256i_u8[byteCount++] = A; + A = 0; + count = 0; + if (byteCount >= 32) + { + break; + } + } + } + } + } + + return result; + } + } _computeBuffer[solutionBufferCount]; m256i currentRandomSeed; @@ -1378,6 +1589,15 @@ struct ScoreFunction return _computeBuffer[solutionBufIdx].computeScore(publicKey.m256i_u8, nonce.m256i_u8, poolVec); } + m256i getLastOutput(const unsigned long long processor_Number) + { + ACQUIRE(solutionEngineLock[processor_Number]); + + m256i result = _computeBuffer[processor_Number].getLastOutput(); + + RELEASE(solutionEngineLock[processor_Number]); + return result; + } // main score function unsigned int operator()(const unsigned long long processor_Number, const m256i& publicKey, const m256i& miningSeed, const m256i& nonce) { @@ -1523,5 +1743,3 @@ struct ScoreFunction } } }; - - diff --git a/src/spectrum/spectrum.h b/src/spectrum/spectrum.h index 7676ab05d..e1a57dd2a 100644 --- a/src/spectrum/spectrum.h +++ b/src/spectrum/spectrum.h @@ -430,9 +430,11 @@ static void deinitSpectrum() if (spectrumDigests) { freePool(spectrumDigests); + spectrumDigests = nullptr; } if (spectrum) { freePool(spectrum); + spectrum = nullptr; } } diff --git a/src/system.h b/src/system.h index 4e5ebfc15..e4bbc3a57 100644 --- a/src/system.h +++ b/src/system.h @@ -15,7 +15,8 @@ struct System unsigned short epoch; unsigned int tick; unsigned int initialTick; - unsigned int latestCreatedTick, latestLedTick; + unsigned int latestCreatedTick; + unsigned int latestLedTick; // contains latest tick t in which TickData for tick (t + TICK_TRANSACTIONS_PUBLICATION_OFFSET) was broadcasted as tick leader unsigned short initialMillisecond; unsigned char initialSecond; diff --git a/src/ticking/execution_fee_report_collector.h b/src/ticking/execution_fee_report_collector.h new file mode 100644 index 000000000..7a0192864 --- /dev/null +++ b/src/ticking/execution_fee_report_collector.h @@ -0,0 +1,173 @@ +#pragma once +#include "platform/memory.h" +#include "platform/quorum_value.h" +#include "network_messages/execution_fees.h" +#include "contract_core/contract_def.h" +#include "contract_core/qpi_spectrum_impl.h" +#include "logging/logging.h" + +class ExecutionFeeReportCollector +{ +private: + unsigned long long executionFeeReports[contractCount][NUMBER_OF_COMPUTORS]; + +public: + void init() + { + setMem(executionFeeReports, sizeof(executionFeeReports), 0); + } + + void reset() + { + setMem(executionFeeReports, sizeof(executionFeeReports), 0); + } + + // Store execution fee report from a computor for a contract + void storeReport(unsigned int contractIndex, unsigned int computorIndex, unsigned long long executionFee) + { + if (contractIndex > 0 && contractIndex < contractCount && computorIndex < NUMBER_OF_COMPUTORS) + { + executionFeeReports[contractIndex][computorIndex] = executionFee; + } + } + + const unsigned long long* getReportsForContract(unsigned int contractIndex) + { + if (contractIndex < contractCount) + { + return executionFeeReports[contractIndex]; + } + return nullptr; + } + + bool validateReportEntries(const unsigned int* contractIndices, const unsigned long long* executionFees, unsigned int numEntries) + { + for (unsigned int i = 0; i < numEntries; i++) + { + if (contractIndices[i] == 0 || contractIndices[i] >= contractCount || executionFees[i] == 0) + { + return false; + } + } + return true; + } + + void storeReportEntries(const unsigned int* contractIndices, const unsigned long long* executionFees, unsigned int numEntries, unsigned int computorIndex) + { + for (unsigned int i = 0; i < numEntries; i++) + { + storeReport(contractIndices[i], computorIndex, executionFees[i]); + } + } + + void processTransactionData(const Transaction* transaction, const m256i& dataLock) + { + int computorIndex = transaction->tick % NUMBER_OF_COMPUTORS; + if (transaction->sourcePublicKey != broadcastedComputors.computors.publicKeys[computorIndex]) + { + // Report was sent by wrong src + return; + } + + if (!ExecutionFeeReportTransactionPrefix::isValidExecutionFeeReport(transaction)) + { + // Report amount or size + return; + } + + const unsigned char* dataLockPtr = transaction->inputPtr() + transaction->inputSize - sizeof(m256i); + m256i txDataLock = *((m256i*)dataLockPtr); + + if (txDataLock != dataLock) + { +#ifndef NDEBUG + CHAR16 dbg[256]; + setText(dbg, L"TRACE: [Execution fee report tx] Wrong datalock from comp "); + appendNumber(dbg, computorIndex, false); + addDebugMessage(dbg); +#endif + return; + } + + if (!ExecutionFeeReportTransactionPrefix::isValidEntryAlignment(transaction)) + { + // Entries array has incomplete entries. + return; + } + + const unsigned int numEntries = ExecutionFeeReportTransactionPrefix::getNumEntries(transaction); + const unsigned int* contractIndices = ExecutionFeeReportTransactionPrefix::getContractIndices(transaction); + const unsigned long long* executionFees = ExecutionFeeReportTransactionPrefix::getExecutionFees(transaction); + + if (!validateReportEntries(contractIndices, executionFees, numEntries)) + { + // Report contains invalid entries. E.g., Entry with negative fees or invalid contractIndex. + return; + } + + storeReportEntries(contractIndices, executionFees, numEntries, computorIndex); + } + + void processReports() + { + for (unsigned int contractIndex = 1; contractIndex < contractCount; contractIndex++) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + unsigned int numNonZero = 0; + int firstNonZero = -1; + int lastNonZero = -1; + for (unsigned int compIndex = 0; compIndex < NUMBER_OF_COMPUTORS; ++compIndex) + { + if (executionFeeReports[contractIndex][compIndex] > 0) + { + if (firstNonZero == -1) + firstNonZero = compIndex; + lastNonZero = compIndex; + numNonZero++; + } + } + + CHAR16 dbgMsgBuf[128]; + setText(dbgMsgBuf, L"Contract "); + appendNumber(dbgMsgBuf, contractIndex, FALSE); + appendText(dbgMsgBuf, L": "); + appendNumber(dbgMsgBuf, numNonZero, FALSE); + appendText(dbgMsgBuf, L" non-zero fee reports, first non-zero comp "); + appendNumber(dbgMsgBuf, firstNonZero, FALSE); + appendText(dbgMsgBuf, L", last non-zero comp "); + appendNumber(dbgMsgBuf, lastNonZero, FALSE); + appendText(dbgMsgBuf, L" (0-indexed)"); + addDebugMessage(dbgMsgBuf); +#endif + + unsigned long long quorumValue = calculateAscendingQuorumValue(executionFeeReports[contractIndex], NUMBER_OF_COMPUTORS); + + if (quorumValue > 0) + { + subtractFromContractFeeReserve(contractIndex, quorumValue); + ContractReserveDeduction message = { quorumValue, getContractFeeReserve(contractIndex), contractIndex }; + logger.logContractReserveDeduction(message); + } + } + + reset(); + } + + bool saveToFile(const CHAR16* fileName, const CHAR16* directory = NULL) + { + long long savedSize = save(fileName, sizeof(ExecutionFeeReportCollector), (unsigned char*)this, directory); + if (savedSize == sizeof(ExecutionFeeReportCollector)) + return true; + else + return false; + } + + bool loadFromFile(const CHAR16* fileName, const CHAR16* directory = NULL) + { + long long loadedSize = load(fileName, sizeof(ExecutionFeeReportCollector), (unsigned char*)this, directory); + if (loadedSize == sizeof(ExecutionFeeReportCollector)) + return true; + else + return false; + } +}; diff --git a/src/ticking/pending_txs_pool.h b/src/ticking/pending_txs_pool.h new file mode 100644 index 000000000..41b565540 --- /dev/null +++ b/src/ticking/pending_txs_pool.h @@ -0,0 +1,554 @@ +#pragma once + +#include "network_messages/transactions.h" + +#include "platform/memory_util.h" +#include "platform/concurrency.h" +#include "platform/console_logging.h" +#include "platform/debugging.h" + +#include "spectrum/spectrum.h" + +#include "mining/mining.h" + +#include "contracts/math_lib.h" +#include "contract_core/qpi_collection_impl.h" + +#include "public_settings.h" +#include "kangaroo_twelve.h" +#include "vote_counter.h" +#include "network_messages/execution_fees.h" + +// Mempool that saves pending transactions (txs) of all entities. +// This is a kind of singleton class with only static members (so all instances refer to the same data). +class PendingTxsPool +{ +protected: + // The PendingTxsPool will always leave space for the three protocol-level txs (tick votes, custom mining, contract execution fees). + static constexpr unsigned int maxNumTxsPerTick = NUMBER_OF_TRANSACTIONS_PER_TICK - 3; + static constexpr unsigned long long maxNumTxsTotal = PENDING_TXS_POOL_NUM_TICKS * maxNumTxsPerTick; + + // Sizes of different buffers in bytes + static constexpr unsigned long long tickTransactionsSize = maxNumTxsTotal * MAX_TRANSACTION_SIZE; + static constexpr unsigned long long txsDigestsSize = maxNumTxsTotal * sizeof(m256i); + + // `maxNumTxsTotal` priorities have to be saved at a time. Collection capacity has to be 2^N so find the next bigger power of 2. + static constexpr unsigned long long txsPrioritiesCapacity = math_lib::findNextPowerOf2(maxNumTxsTotal); + + // The pool stores the tick range [firstStoredTick, firstStoredTick + PENDING_TXS_POOL_NUM_TICKS[ + inline static unsigned int firstStoredTick = 0; + + // Allocated tickTransactions buffer with tickTransactionsSize bytes + inline static unsigned char* tickTransactionsBuffer = nullptr; + + // Allocated txsDigests buffer with maxNumTxs elements + inline static m256i* txsDigestsBuffer = nullptr; + + // Records the number of saved transactions for each tick + inline static unsigned int numSavedTxsPerTick[PENDING_TXS_POOL_NUM_TICKS]; + + // Begin index for tickTransactionOffsetsBuffer, txsDigestsBuffer, and numSavedTxsPerTick + // buffersBeginIndex corresponds to firstStoredTick + inline static unsigned int buffersBeginIndex = 0; + + // Lock for securing the data in the PendingTxsPool + inline static volatile char lock = 0; + + // Priority queues for transactions in each saved tick + inline static Collection* txsPriorities; + + static void cleanupTxsPriorities(unsigned int tickIndex) + { + sint64 elementIndex = txsPriorities->headIndex(m256i{ tickIndex, 0, 0, 0 }); + // use a `for` instead of a `while` loop to make sure it cannot run forever + // there can be at most `maxNumTxsPerTick` elements in one pov + for (unsigned int t = 0; t < maxNumTxsPerTick; ++t) + { + if (elementIndex != NULL_INDEX) + elementIndex = txsPriorities->remove(elementIndex); + else + break; + } + txsPriorities->cleanupIfNeeded(); + } + + static sint64 calculateTxPriority(const Transaction* tx) + { + sint64 priority = 0; + int sourceIndex = spectrumIndex(tx->sourcePublicKey); + if (sourceIndex >= 0) + { + sint64 balance = energy(sourceIndex); + if (balance > 0) + { + if (isZero(tx->destinationPublicKey) && tx->amount == 0LL + && (tx->inputType == VOTE_COUNTER_INPUT_TYPE || tx->inputType == CustomMiningSolutionTransaction::transactionType() || tx->inputType == ExecutionFeeReportTransactionPrefix::transactionType())) + { + // protocol-level tx always have max priority + return INT64_MAX; + } + else + { + // Calculate tx priority as: [balance of src] * [scheduledTick - latestTransferTick + 1] with + // latestTransferTick = latestOutgoingTransferTick if latestOutgoingTransferTick > 0, + // latestTransferTick = latestIncomingTransferTick otherwise (new entity). + const EntityRecord& entity = spectrum[sourceIndex]; + const unsigned int latestTransferTick = (entity.latestOutgoingTransferTick) ? entity.latestOutgoingTransferTick : entity.latestIncomingTransferTick; + priority = math_lib::smul(balance, static_cast(tx->tick - latestTransferTick + 1)); + // decrease by 1 to make sure no normal tx reaches max priority + priority--; + } + } + } + return priority; + } + + // Return pointer to Transaction based on tickIndex and transactionIndex (checking offset with ASSERT) + inline static Transaction* getTxPtr(unsigned int tickIndex, unsigned int transactionIndex) + { + ASSERT(tickIndex < PENDING_TXS_POOL_NUM_TICKS); + ASSERT(transactionIndex < maxNumTxsPerTick); + return (Transaction*)(tickTransactionsBuffer + (tickIndex * maxNumTxsPerTick + transactionIndex) * MAX_TRANSACTION_SIZE); + } + + // Return pointer to transaction digest based on tickIndex and transactionIndex (checking offset with ASSERT) + inline static m256i* getDigestPtr(unsigned int tickIndex, unsigned int transactionIndex) + { + ASSERT(tickIndex < PENDING_TXS_POOL_NUM_TICKS); + ASSERT(transactionIndex < maxNumTxsPerTick); + return &txsDigestsBuffer[tickIndex * maxNumTxsPerTick + transactionIndex]; + } + + // Check whether tick is stored in the pending txs pool + inline static bool tickInStorage(unsigned int tick) + { + return tick >= firstStoredTick && tick < firstStoredTick + PENDING_TXS_POOL_NUM_TICKS; + } + + // Return index of tick data in current storage window (does not check tick). + inline static unsigned int tickToIndex(unsigned int tick) + { + return ((tick - firstStoredTick) + buffersBeginIndex) % PENDING_TXS_POOL_NUM_TICKS; + } + +public: + + // Init at node startup. + static bool init() + { + if (!allocPoolWithErrorLog(L"PendingTxsPool::tickTransactionsPtr ", tickTransactionsSize, (void**)&tickTransactionsBuffer, __LINE__) + || !allocPoolWithErrorLog(L"PendingTxsPool::txsDigestsPtr ", txsDigestsSize, (void**)&txsDigestsBuffer, __LINE__) + || !allocPoolWithErrorLog(L"PendingTxsPool::txsPriorities", sizeof(Collection), (void**)&txsPriorities, __LINE__)) + { + return false; + } + + ASSERT(lock == 0); + + setMem(tickTransactionsBuffer, tickTransactionsSize, 0); + setMem(txsDigestsBuffer, txsDigestsSize, 0); + setMem(numSavedTxsPerTick, sizeof(numSavedTxsPerTick), 0); + + txsPriorities->reset(); + + firstStoredTick = 0; + buffersBeginIndex = 0; + + return true; + } + + // Cleanup at node shutdown. + static void deinit() + { + if (tickTransactionsBuffer) + { + freePool(tickTransactionsBuffer); + } + if (txsDigestsBuffer) + { + freePool(txsDigestsBuffer); + } + if (txsPriorities) + { + freePool(txsPriorities); + } + } + + // Acquire lock for returned pointers to transactions or digests. + inline static void acquireLock() + { + ACQUIRE(lock); + } + + // Release lock for returned pointers to transactions or digests. + inline static void releaseLock() + { + RELEASE(lock); + } + + // Return number of transactions scheduled for the specified tick. + static unsigned int getNumberOfPendingTickTxs(unsigned int tick) + { +//#if !defined(NDEBUG) && !defined(NO_UEFI) +// addDebugMessage(L"Begin pendingTxsPool.getNumberOfPendingTickTxs()"); +//#endif + unsigned int res = 0; + ACQUIRE(lock); + if (tickInStorage(tick)) + { + res = numSavedTxsPerTick[tickToIndex(tick)]; + } + RELEASE(lock); + +//#if !defined(NDEBUG) && !defined(NO_UEFI) +// CHAR16 dbgMsgBuf[200]; +// setText(dbgMsgBuf, L"End pendingTxsPool.getNumberOfPendingTickTxs() for tick="); +// appendNumber(dbgMsgBuf, tick, FALSE); +// appendText(dbgMsgBuf, L" -> res="); +// appendNumber(dbgMsgBuf, res, FALSE); +// addDebugMessage(dbgMsgBuf); +//#endif + return res; + } + + // Return number of transactions scheduled later than the specified tick. + static unsigned int getTotalNumberOfPendingTxs(unsigned int tick) + { +//#if !defined(NDEBUG) && !defined(NO_UEFI) +// addDebugMessage(L"Begin pendingTxsPool.getTotalNumberOfPendingTxs()"); +//#endif + unsigned int res = 0; + ACQUIRE(lock); + if (tickInStorage(tick + 1)) + { + unsigned int startIndex = tickToIndex(tick + 1); + + if (startIndex < buffersBeginIndex) + { + for (unsigned int t = startIndex; t < buffersBeginIndex; ++t) + res += numSavedTxsPerTick[t]; + } + else + { + for (unsigned int t = startIndex; t < PENDING_TXS_POOL_NUM_TICKS; ++t) + res += numSavedTxsPerTick[t]; + for (unsigned int t = 0; t < buffersBeginIndex; ++t) + res += numSavedTxsPerTick[t]; + } + } + RELEASE(lock); + +//#if !defined(NDEBUG) && !defined(NO_UEFI) +// CHAR16 dbgMsgBuf[200]; +// setText(dbgMsgBuf, L"End pendingTxsPool.getTotalNumberOfPendingTxs() for tick="); +// appendNumber(dbgMsgBuf, tick, FALSE); +// appendText(dbgMsgBuf, L" -> res="); +// appendNumber(dbgMsgBuf, res, FALSE); +// addDebugMessage(dbgMsgBuf); +//#endif + return res; + } + + // Check validity of transaction and add to the pool. Return boolean indicating whether transaction was added. + static bool add(const Transaction* tx) + { +//#if !defined(NDEBUG) && !defined(NO_UEFI) +// addDebugMessage(L"Begin pendingTxsPool.add()"); +//#endif + bool txAdded = false; + ACQUIRE(lock); + if (tx->checkValidity() && tickInStorage(tx->tick)) + { + unsigned int tickIndex = tickToIndex(tx->tick); + const unsigned int transactionSize = tx->totalSize(); + + // check if tx with same digest already exists + m256i digest; + KangarooTwelve(tx, transactionSize, &digest, sizeof(m256i)); + for (unsigned int txIndex = 0; txIndex < numSavedTxsPerTick[tickIndex]; ++txIndex) + { + if (*getDigestPtr(tickIndex, txIndex) == digest) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + CHAR16 dbgMsgBuf[100]; + setText(dbgMsgBuf, L"tx with the same digest already exists for tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + goto end_add_function; + } + } + + sint64 priority = calculateTxPriority(tx); + if (priority > 0) + { + m256i povIndex{ tickIndex, 0, 0, 0 }; + + if (numSavedTxsPerTick[tickIndex] < maxNumTxsPerTick) + { + copyMem(getDigestPtr(tickIndex, numSavedTxsPerTick[tickIndex]), &digest, sizeof(m256i)); + copyMem(getTxPtr(tickIndex, numSavedTxsPerTick[tickIndex]), tx, transactionSize); + txsPriorities->add(povIndex, numSavedTxsPerTick[tickIndex], priority); + + numSavedTxsPerTick[tickIndex]++; + txAdded = true; + } + else + { + // check if priority is higher than lowest priority tx in this tick and replace in this case + sint64 lowestElementIndex = txsPriorities->tailIndex(povIndex); + if (lowestElementIndex != NULL_INDEX) + { + if (txsPriorities->priority(lowestElementIndex) < priority) + { + unsigned int replacedTxIndex = txsPriorities->element(lowestElementIndex); + txsPriorities->remove(lowestElementIndex); + txsPriorities->add(povIndex, replacedTxIndex, priority); + + copyMem(getDigestPtr(tickIndex, replacedTxIndex), &digest, sizeof(m256i)); + copyMem(getTxPtr(tickIndex, replacedTxIndex), tx, transactionSize); + + txAdded = true; + } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + CHAR16 dbgMsgBuf[300]; + setText(dbgMsgBuf, L"tx could not be added, already saved "); + appendNumber(dbgMsgBuf, numSavedTxsPerTick[tickIndex], FALSE); + appendText(dbgMsgBuf, L" txs for tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + appendText(dbgMsgBuf, L" and priority "); + appendNumber(dbgMsgBuf, priority, FALSE); + appendText(dbgMsgBuf, L" is lower than lowest saved priority "); + appendNumber(dbgMsgBuf, txsPriorities->priority(lowestElementIndex), FALSE); + addDebugMessage(dbgMsgBuf); + } +#endif + } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + // debug log, this should never happen + CHAR16 dbgMsgBuf[300]; + setText(dbgMsgBuf, L"maximum number of txs "); + appendNumber(dbgMsgBuf, numSavedTxsPerTick[tickIndex], FALSE); + appendText(dbgMsgBuf, L" saved for tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + appendText(dbgMsgBuf, L" but povIndex is unknown. This should never happen."); + addDebugMessage(dbgMsgBuf); + } +#endif + } + } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + CHAR16 dbgMsgBuf[100]; + setText(dbgMsgBuf, L"tx with priority 0 was rejected for tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + addDebugMessage(dbgMsgBuf); + } +#endif + } + + end_add_function: + RELEASE(lock); + +//#if !defined(NDEBUG) && !defined(NO_UEFI) +// if (txAdded) +// addDebugMessage(L"End pendingTxsPool.add(), txAdded true"); +// else +// addDebugMessage(L"End pendingTxsPool.add(), txAdded false"); +//#endif + return txAdded; + } + + // Get a transaction for the specified tick. If no more transactions for this tick, return nullptr. + // ATTENTION: when running multiple threads, you need to have acquired the lock via acquireLock() before calling this function. + static Transaction* getTx(unsigned int tick, unsigned int index) + { + unsigned int tickIndex; + + if (tickInStorage(tick)) + tickIndex = tickToIndex(tick); + else + return nullptr; + + bool hasTx = index < numSavedTxsPerTick[tickIndex]; + + if (hasTx) + return getTxPtr(tickIndex, index); + else + return nullptr; + } + + // Get a transaction digest for the specified tick. If no more transactions for this tick, return nullptr. + // ATTENTION: when running multiple threads, you need to have acquired the lock via acquireLock() before calling this function. + static m256i* getDigest(unsigned int tick, unsigned int index) + { + unsigned int tickIndex; + + if (tickInStorage(tick)) + tickIndex = tickToIndex(tick); + else + return nullptr; + + bool hasTx = index < numSavedTxsPerTick[tickIndex]; + + if (hasTx) + return getDigestPtr(tickIndex, index); + else + return nullptr; + } + + static void incrementFirstStoredTick() + { + ACQUIRE(lock); + + // set memory at buffersBeginIndex to 0 + unsigned long long numTxsBeforeBegin = buffersBeginIndex * maxNumTxsPerTick; + setMem(tickTransactionsBuffer + numTxsBeforeBegin * MAX_TRANSACTION_SIZE, maxNumTxsPerTick * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer + numTxsBeforeBegin, maxNumTxsPerTick * sizeof(m256i), 0); + numSavedTxsPerTick[buffersBeginIndex] = 0; + + // remove txs priorities stored for firstStoredTick + cleanupTxsPriorities(tickToIndex(firstStoredTick)); + + // increment buffersBeginIndex and firstStoredTick + firstStoredTick++; + buffersBeginIndex = (buffersBeginIndex + 1) % PENDING_TXS_POOL_NUM_TICKS; + + RELEASE(lock); + } + + static void beginEpoch(unsigned int newInitialTick) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin pendingTxsPool.beginEpoch()"); +#endif + ACQUIRE(lock); + if (tickInStorage(newInitialTick)) + { + unsigned int newInitialIndex = tickToIndex(newInitialTick); + + // reset memory of discarded ticks + if (newInitialIndex < buffersBeginIndex) + { + unsigned long long numTxsBeforeNew = newInitialIndex * maxNumTxsPerTick; + setMem(tickTransactionsBuffer, numTxsBeforeNew * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer, numTxsBeforeNew * sizeof(m256i), 0); + setMem(numSavedTxsPerTick, newInitialIndex * sizeof(unsigned int), 0); + + for (unsigned int tickIndex = 0; tickIndex < newInitialIndex; ++tickIndex) + cleanupTxsPriorities(tickIndex); + + unsigned long long numTxsBeforeBegin = buffersBeginIndex * maxNumTxsPerTick; + unsigned long long numTxsStartingAtBegin = (PENDING_TXS_POOL_NUM_TICKS - buffersBeginIndex) * maxNumTxsPerTick; + setMem(tickTransactionsBuffer + numTxsBeforeBegin * MAX_TRANSACTION_SIZE, numTxsStartingAtBegin * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer + numTxsBeforeBegin, numTxsStartingAtBegin * sizeof(m256i), 0); + setMem(numSavedTxsPerTick + buffersBeginIndex, (PENDING_TXS_POOL_NUM_TICKS - buffersBeginIndex) * sizeof(unsigned int), 0); + + for (unsigned int tickIndex = buffersBeginIndex; tickIndex < PENDING_TXS_POOL_NUM_TICKS; ++tickIndex) + cleanupTxsPriorities(tickIndex); + } + else + { + unsigned long long numTxsBeforeBegin = buffersBeginIndex * maxNumTxsPerTick; + unsigned long long numTxsStartingAtBegin = (newInitialIndex - buffersBeginIndex) * maxNumTxsPerTick; + setMem(tickTransactionsBuffer + numTxsBeforeBegin * MAX_TRANSACTION_SIZE, numTxsStartingAtBegin * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer + numTxsBeforeBegin, numTxsStartingAtBegin * sizeof(m256i), 0); + setMem(numSavedTxsPerTick + buffersBeginIndex, (newInitialIndex - buffersBeginIndex) * sizeof(unsigned int), 0); + + for (unsigned int tickIndex = buffersBeginIndex; tickIndex < newInitialIndex; ++tickIndex) + cleanupTxsPriorities(tickIndex); + } + + buffersBeginIndex = newInitialIndex; + } + else + { + setMem(tickTransactionsBuffer, tickTransactionsSize, 0); + setMem(txsDigestsBuffer, txsDigestsSize, 0); + setMem(numSavedTxsPerTick, sizeof(numSavedTxsPerTick), 0); + + txsPriorities->reset(); + + buffersBeginIndex = 0; + } + + firstStoredTick = newInitialTick; + + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"End pendingTxsPool.beginEpoch()"); +#endif + } + + // Useful for debugging, but expensive: check that everything is as expected. + static void checkStateConsistencyWithAssert() + { + ACQUIRE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin tsxPool.checkStateConsistencyWithAssert()"); + CHAR16 dbgMsgBuf[200]; + setText(dbgMsgBuf, L"firstStoredTick="); + appendNumber(dbgMsgBuf, firstStoredTick, FALSE); + appendText(dbgMsgBuf, L", buffersBeginIndex="); + appendNumber(dbgMsgBuf, buffersBeginIndex, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + + ASSERT(buffersBeginIndex >= 0); + ASSERT(buffersBeginIndex < PENDING_TXS_POOL_NUM_TICKS); + + ASSERT(tickTransactionsBuffer != nullptr); + ASSERT(txsDigestsBuffer != nullptr); + + for (unsigned int tick = firstStoredTick; tick < firstStoredTick + PENDING_TXS_POOL_NUM_TICKS; ++tick) + { + ASSERT(tickInStorage(tick)); + if (tickInStorage(tick)) + { + unsigned int tickIndex = tickToIndex(tick); + unsigned int numSavedForTick = numSavedTxsPerTick[tickIndex]; + ASSERT(numSavedForTick <= maxNumTxsPerTick); + for (unsigned int txIndex = 0; txIndex < numSavedForTick; ++txIndex) + { + Transaction* transaction = (Transaction*)(tickTransactionsBuffer + (tickIndex * maxNumTxsPerTick + txIndex) * MAX_TRANSACTION_SIZE); + ASSERT(transaction->checkValidity()); + ASSERT(transaction->tick == tick); +#if !defined(NDEBUG) && !defined(NO_UEFI) + if (!transaction->checkValidity() || transaction->tick != tick) + { + setText(dbgMsgBuf, L"Error in previous epoch transaction "); + appendNumber(dbgMsgBuf, txIndex, FALSE); + appendText(dbgMsgBuf, L" in tick "); + appendNumber(dbgMsgBuf, tick, FALSE); + addDebugMessage(dbgMsgBuf); + + setText(dbgMsgBuf, L"t->tick "); + appendNumber(dbgMsgBuf, transaction->tick, FALSE); + appendText(dbgMsgBuf, L", t->inputSize "); + appendNumber(dbgMsgBuf, transaction->inputSize, FALSE); + appendText(dbgMsgBuf, L", t->inputType "); + appendNumber(dbgMsgBuf, transaction->inputType, FALSE); + appendText(dbgMsgBuf, L", t->amount "); + appendNumber(dbgMsgBuf, transaction->amount, TRUE); + addDebugMessage(dbgMsgBuf); + } +#endif + } + } + } + + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"End pendingTxsPool.checkStateConsistencyWithAssert()"); +#endif + } + +}; \ No newline at end of file diff --git a/src/ticking/stable_computor_index.h b/src/ticking/stable_computor_index.h new file mode 100644 index 000000000..1b4c0ee9d --- /dev/null +++ b/src/ticking/stable_computor_index.h @@ -0,0 +1,69 @@ +#pragma once + +#include "platform/m256.h" +#include "platform/memory.h" +#include "public_settings.h" + +// Minimum buffer size: NUMBER_OF_COMPUTORS * sizeof(m256i) + 2 * NUMBER_OF_COMPUTORS bytes (~23KB) +constexpr unsigned long long stableComputorIndexBufferSize() +{ + return NUMBER_OF_COMPUTORS * sizeof(m256i) + 2 * NUMBER_OF_COMPUTORS; +} + +// Reorders futureComputors so requalifying computors keep their current index. +// New computors fill remaining slots. See doc/stable_computor_index_diagram.svg +// Returns false if there aren't enough computors to fill all slots. +static bool calculateStableComputorIndex( + m256i* futureComputors, + const m256i* currentComputors, + void* tempBuffer) +{ + m256i* tempComputorList = (m256i*)tempBuffer; + bool* isIndexTaken = (bool*)(tempComputorList + NUMBER_OF_COMPUTORS); + bool* isFutureComputorUsed = isIndexTaken + NUMBER_OF_COMPUTORS; + + setMem(tempComputorList, NUMBER_OF_COMPUTORS * sizeof(m256i), 0); + setMem(isIndexTaken, NUMBER_OF_COMPUTORS, 0); + setMem(isFutureComputorUsed, NUMBER_OF_COMPUTORS, 0); + + // Step 1: Requalifying computors keep their current index + for (unsigned int futureIdx = 0; futureIdx < NUMBER_OF_COMPUTORS; futureIdx++) + { + for (unsigned int currentIdx = 0; currentIdx < NUMBER_OF_COMPUTORS; currentIdx++) + { + if (futureComputors[futureIdx] == currentComputors[currentIdx]) + { + tempComputorList[currentIdx] = futureComputors[futureIdx]; + isIndexTaken[currentIdx] = true; + isFutureComputorUsed[futureIdx] = true; + break; + } + } + } + + // Step 2: New computors fill remaining slots + unsigned int nextNewComputorIdx = 0; + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + if (!isIndexTaken[i]) + { + while (nextNewComputorIdx < NUMBER_OF_COMPUTORS && isFutureComputorUsed[nextNewComputorIdx]) + { + nextNewComputorIdx++; + } + + if (nextNewComputorIdx >= NUMBER_OF_COMPUTORS) + { + return false; + } + + tempComputorList[i] = futureComputors[nextNewComputorIdx]; + isFutureComputorUsed[nextNewComputorIdx] = true; + nextNewComputorIdx++; + } + } + + copyMem(futureComputors, tempComputorList, NUMBER_OF_COMPUTORS * sizeof(m256i)); + + return true; +} diff --git a/src/ticking/tick_storage.h b/src/ticking/tick_storage.h index 24f02ad8c..3c9116e87 100644 --- a/src/ticking/tick_storage.h +++ b/src/ticking/tick_storage.h @@ -28,7 +28,7 @@ constexpr unsigned short INVALIDATED_TICK_DATA = 0xffff; // - ticks (one Tick struct per tick and Computor) // - tickTransactions (continuous buffer efficiently storing the variable-size transactions) // - tickTransactionOffsets (offsets of transactions in buffer, order in tickTransactions may differ) -// - nextTickTransactionOffset (offset of next transition to be added) +// - nextTickTransactionOffset (offset of next transaction to be added) class TickStorage { private: @@ -167,7 +167,7 @@ class TickStorage } bool saveTransactions(unsigned long long nTick, long long& outTotalTransactionSize, unsigned long long& outNextTickTransactionOffset, CHAR16* directory = NULL) { - unsigned int toTick = tickBegin + (unsigned int)(nTick); + unsigned int toTick = tickBegin + (unsigned int)(nTick) - 1; unsigned long long toPtr = 0; outNextTickTransactionOffset = FIRST_TICK_TRANSACTION_OFFSET; lastCheckTransactionOffset = tickBegin > lastCheckTransactionOffset ? tickBegin : lastCheckTransactionOffset; diff --git a/src/ticking/ticking.h b/src/ticking/ticking.h index 1b5216926..4bbb0d61a 100644 --- a/src/ticking/ticking.h +++ b/src/ticking/ticking.h @@ -6,6 +6,7 @@ #include "network_messages/tick.h" #include "ticking/tick_storage.h" +#include "ticking/pending_txs_pool.h" #include "private_settings.h" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6723ec0a1..d429b8716 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable( # contract_qearn.cpp # contract_qvault.cpp # contract_qx.cpp + contract_vottunbridge.cpp # kangaroo_twelve.cpp m256.cpp math_lib.cpp diff --git a/test/assets.cpp b/test/assets.cpp index f72a10b59..7c2bafd71 100644 --- a/test/assets.cpp +++ b/test/assets.cpp @@ -9,9 +9,10 @@ #include "assets/assets.h" #include "contract_core/contract_exec.h" -#include "contract_core/qpi_asset_impl.h" +#include "contract_core/qpi_spectrum_impl.h" +#include "contract_core/qpi_asset_impl.h" + - class AssetsTest : public AssetStorage, LoggingTest diff --git a/test/contract_ccf.cpp b/test/contract_ccf.cpp new file mode 100644 index 000000000..8aacb777d --- /dev/null +++ b/test/contract_ccf.cpp @@ -0,0 +1,1215 @@ +#define NO_UEFI + +#include "contract_testing.h" + +#define PRINT_DETAILS 0 + +class CCFChecker : public CCF +{ +public: + void checkSubscriptions(bool printDetails = PRINT_DETAILS) + { + if (printDetails) + { + std::cout << "Active Subscriptions (total capacity: " << activeSubscriptions.capacity() << "):" << std::endl; + for (uint64 i = 0; i < activeSubscriptions.capacity(); ++i) + { + const SubscriptionData& sub = activeSubscriptions.get(i); + if (!isZero(sub.destination)) + { + std::cout << "- Index " << i << ": destination=" << sub.destination + << ", weeksPerPeriod=" << (int)sub.weeksPerPeriod + << ", numberOfPeriods=" << sub.numberOfPeriods + << ", amountPerPeriod=" << sub.amountPerPeriod + << ", startEpoch=" << sub.startEpoch + << ", currentPeriod=" << sub.currentPeriod << std::endl; + } + } + std::cout << "Subscription Proposals (total capacity: " << subscriptionProposals.capacity() << "):" << std::endl; + for (uint64 i = 0; i < subscriptionProposals.capacity(); ++i) + { + const SubscriptionProposalData& prop = subscriptionProposals.get(i); + if (!isZero(prop.proposerId)) + { + std::cout << "- Index " << i << ": proposerId=" << prop.proposerId + << ", destination=" << prop.destination + << ", weeksPerPeriod=" << (int)prop.weeksPerPeriod + << ", numberOfPeriods=" << prop.numberOfPeriods + << ", amountPerPeriod=" << prop.amountPerPeriod + << ", startEpoch=" << prop.startEpoch << std::endl; + } + } + } + } + + const SubscriptionData* getActiveSubscriptionByDestination(const id& destination) + { + for (uint64 i = 0; i < activeSubscriptions.capacity(); ++i) + { + const SubscriptionData& sub = activeSubscriptions.get(i); + if (sub.destination == destination && !isZero(sub.destination)) + return ⊂ + } + return nullptr; + } + + // Helper to find destination from a proposer's subscription proposal + id getDestinationByProposer(const id& proposerId) + { + // Use constant 128 which matches SubscriptionProposalsT capacity + for (uint64 i = 0; i < 128; ++i) + { + const SubscriptionProposalData& prop = subscriptionProposals.get(i); + if (prop.proposerId == proposerId && !isZero(prop.proposerId)) + return prop.destination; + } + return NULL_ID; + } + + bool hasActiveSubscription(const id& destination) + { + const SubscriptionData* sub = getActiveSubscriptionByDestination(destination); + return sub != nullptr; + } + + + sint32 getSubscriptionCurrentPeriod(const id& destination) + { + const SubscriptionData* sub = getActiveSubscriptionByDestination(destination); + return sub != nullptr ? sub->currentPeriod : -1; + } + + bool getSubscriptionIsActive(const id& destination) + { + const SubscriptionData* sub = getActiveSubscriptionByDestination(destination); + return sub != nullptr; + } + + // Overload for backward compatibility - use proposer ID + bool getSubscriptionIsActive(const id& proposerId, bool) + { + return getSubscriptionIsActiveByProposer(proposerId); + } + + uint8 getSubscriptionWeeksPerPeriod(const id& destination) + { + const SubscriptionData* sub = getActiveSubscriptionByDestination(destination); + return sub != nullptr ? sub->weeksPerPeriod : 0; + } + + uint32 getSubscriptionNumberOfPeriods(const id& destination) + { + const SubscriptionData* sub = getActiveSubscriptionByDestination(destination); + return sub != nullptr ? sub->numberOfPeriods : 0; + } + + sint64 getSubscriptionAmountPerPeriod(const id& destination) + { + const SubscriptionData* sub = getActiveSubscriptionByDestination(destination); + return sub != nullptr ? sub->amountPerPeriod : 0; + } + + uint32 getSubscriptionStartEpoch(const id& destination) + { + const SubscriptionData* sub = getActiveSubscriptionByDestination(destination); + return sub != nullptr ? sub->startEpoch : 0; + } + + uint32 countActiveSubscriptions() + { + uint32 count = 0; + for (uint64 i = 0; i < activeSubscriptions.capacity(); ++i) + { + if (!isZero(activeSubscriptions.get(i).destination)) + count++; + } + return count; + } + + // Helper function to check if proposer has a subscription proposal + bool hasSubscriptionProposal(const id& proposerId) + { + // Use constant 128 which matches SubscriptionProposalsT capacity + for (uint64 i = 0; i < 128; ++i) + { + const SubscriptionProposalData& prop = subscriptionProposals.get(i); + if (prop.proposerId == proposerId && !isZero(prop.proposerId)) + return true; + } + return false; + } + + // Helper function for backward compatibility - finds destination from proposer's proposal and checks active subscription + bool hasActiveSubscriptionByProposer(const id& proposerId) + { + id destination = getDestinationByProposer(proposerId); + if (isZero(destination)) + return false; + return hasActiveSubscription(destination); + } + + // Helper function that checks both subscription proposals and active subscriptions by proposer + bool hasSubscription(const id& proposerId) + { + return hasSubscriptionProposal(proposerId) || hasActiveSubscriptionByProposer(proposerId); + } + + // Helper functions that work with proposer ID (for backward compatibility with tests) + bool getSubscriptionIsActiveByProposer(const id& proposerId) + { + return hasActiveSubscriptionByProposer(proposerId); + } + + // Helper to get subscription proposal data by proposer ID + const SubscriptionProposalData* getSubscriptionProposalByProposer(const id& proposerId) + { + // Use constant 128 which matches SubscriptionProposalsT capacity + for (uint64 i = 0; i < 128; ++i) + { + const SubscriptionProposalData& prop = subscriptionProposals.get(i); + if (prop.proposerId == proposerId && !isZero(prop.proposerId)) + return ∝ + } + return nullptr; + } + + uint8 getSubscriptionWeeksPerPeriodByProposer(const id& proposerId) + { + // First check subscription proposal + const SubscriptionProposalData* prop = getSubscriptionProposalByProposer(proposerId); + if (prop != nullptr) + return prop->weeksPerPeriod; + + // Then check active subscription + id destination = getDestinationByProposer(proposerId); + if (!isZero(destination)) + return getSubscriptionWeeksPerPeriod(destination); + + return 0; + } + + uint32 getSubscriptionNumberOfPeriodsByProposer(const id& proposerId) + { + // First check subscription proposal + const SubscriptionProposalData* prop = getSubscriptionProposalByProposer(proposerId); + if (prop != nullptr) + return prop->numberOfPeriods; + + // Then check active subscription + id destination = getDestinationByProposer(proposerId); + if (!isZero(destination)) + return getSubscriptionNumberOfPeriods(destination); + + return 0; + } + + sint64 getSubscriptionAmountPerPeriodByProposer(const id& proposerId) + { + // First check subscription proposal + const SubscriptionProposalData* prop = getSubscriptionProposalByProposer(proposerId); + if (prop != nullptr) + return prop->amountPerPeriod; + + // Then check active subscription + id destination = getDestinationByProposer(proposerId); + if (!isZero(destination)) + return getSubscriptionAmountPerPeriod(destination); + + return 0; + } + + uint32 getSubscriptionStartEpochByProposer(const id& proposerId) + { + // First check subscription proposal + const SubscriptionProposalData* prop = getSubscriptionProposalByProposer(proposerId); + if (prop != nullptr) + return prop->startEpoch; + + // Then check active subscription + id destination = getDestinationByProposer(proposerId); + if (!isZero(destination)) + return getSubscriptionStartEpoch(destination); + + return 0; + } + + sint32 getSubscriptionCurrentPeriodByProposer(const id& proposerId) + { + // Only check active subscription (currentPeriod doesn't exist in proposals) + id destination = getDestinationByProposer(proposerId); + if (isZero(destination)) + return -1; // No active subscription yet + return getSubscriptionCurrentPeriod(destination); + } +}; + +class ContractTestingCCF : protected ContractTesting +{ +public: + ContractTestingCCF() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(CCF); + callSystemProcedure(CCF_CONTRACT_INDEX, INITIALIZE); + + // Setup computors + for (unsigned long long i = 0; i < NUMBER_OF_COMPUTORS; ++i) + { + broadcastedComputors.computors.publicKeys[i] = id(i, 1, 2, 3); + increaseEnergy(id(i, 1, 2, 3), 1000000); + } + } + + ~ContractTestingCCF() + { + checkContractExecCleanup(); + } + + CCFChecker* getState() + { + return (CCFChecker*)contractStates[CCF_CONTRACT_INDEX]; + } + + CCF::SetProposal_output setProposal(const id& originator, const CCF::SetProposal_input& input) + { + CCF::SetProposal_output output; + invokeUserProcedure(CCF_CONTRACT_INDEX, 1, input, output, originator, 1000000); + return output; + } + + CCF::GetProposal_output getProposal(uint32 proposalIndex, const id& subscriptionDestination = NULL_ID) + { + CCF::GetProposal_input input; + input.proposalIndex = (uint16)proposalIndex; + input.subscriptionDestination = subscriptionDestination; + CCF::GetProposal_output output; + callFunction(CCF_CONTRACT_INDEX, 2, input, output); + return output; + } + + CCF::GetVotingResults_output getVotingResults(uint32 proposalIndex) + { + CCF::GetVotingResults_input input; + CCF::GetVotingResults_output output; + + input.proposalIndex = (uint16)proposalIndex; + callFunction(CCF_CONTRACT_INDEX, 4, input, output); + return output; + } + + bool vote(const id& originator, const CCF::Vote_input& input) + { + CCF::Vote_output output; + invokeUserProcedure(CCF_CONTRACT_INDEX, 2, input, output, originator, 0); + return output.okay; + } + + CCF::GetLatestTransfers_output getLatestTransfers() + { + CCF::GetLatestTransfers_output output; + callFunction(CCF_CONTRACT_INDEX, 5, CCF::GetLatestTransfers_input(), output); + return output; + } + + CCF::GetRegularPayments_output getRegularPayments() + { + CCF::GetRegularPayments_output output; + callFunction(CCF_CONTRACT_INDEX, 7, CCF::GetRegularPayments_input(), output); + return output; + } + + CCF::GetProposalFee_output getProposalFee() + { + CCF::GetProposalFee_output output; + callFunction(CCF_CONTRACT_INDEX, 6, CCF::GetProposalFee_input(), output); + return output; + } + + CCF::GetProposalIndices_output getProposalIndices(bool activeProposals, sint32 prevProposalIndex = -1) + { + CCF::GetProposalIndices_input input; + input.activeProposals = activeProposals; + input.prevProposalIndex = prevProposalIndex; + CCF::GetProposalIndices_output output; + callFunction(CCF_CONTRACT_INDEX, 1, input, output); + return output; + } + + void beginEpoch(bool expectSuccess = true) + { + callSystemProcedure(CCF_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(CCF_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + uint32 setupRegularProposal(const id& proposer, const id& destination, sint64 amount, bool expectSuccess = true) + { + CCF::SetProposal_input input; + setMemory(input, 0); + input.proposal.epoch = system.epoch; + input.proposal.type = ProposalTypes::TransferYesNo; + input.proposal.transfer.destination = destination; + input.proposal.transfer.amount = amount; + input.isSubscription = false; + + auto output = setProposal(proposer, input); + if (expectSuccess) + EXPECT_NE((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + else + EXPECT_EQ((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + return output.proposalIndex; + } + + uint32 setupSubscriptionProposal(const id& proposer, const id& destination, sint64 amountPerPeriod, + uint32 numberOfPeriods, uint8 weeksPerPeriod, uint32 startEpoch, bool expectSuccess = true) + { + CCF::SetProposal_input input; + setMemory(input, 0); + input.proposal.epoch = system.epoch; + input.proposal.type = ProposalTypes::TransferYesNo; + input.proposal.transfer.destination = destination; + input.proposal.transfer.amount = amountPerPeriod; + input.isSubscription = true; + input.weeksPerPeriod = weeksPerPeriod; + input.numberOfPeriods = numberOfPeriods; + input.startEpoch = startEpoch; + input.amountPerPeriod = amountPerPeriod; + + auto output = setProposal(proposer, input); + if (expectSuccess) + EXPECT_NE((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + else + EXPECT_EQ((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + return output.proposalIndex; + } + + void voteMultipleComputors(uint32 proposalIndex, uint32 votesNo, uint32 votesYes) + { + EXPECT_LE((int)(votesNo + votesYes), (int)NUMBER_OF_COMPUTORS); + const auto proposal = getProposal(proposalIndex); + EXPECT_TRUE(proposal.okay); + + CCF::Vote_input voteInput; + voteInput.proposalIndex = (uint16)proposalIndex; + voteInput.proposalType = proposal.proposal.type; + voteInput.proposalTick = proposal.proposal.tick; + + uint32 compIdx = 0; + for (uint32 i = 0; i < votesNo; ++i, ++compIdx) + { + voteInput.voteValue = 0; // 0 = no vote + EXPECT_TRUE(vote(id(compIdx, 1, 2, 3), voteInput)); + } + for (uint32 i = 0; i < votesYes; ++i, ++compIdx) + { + voteInput.voteValue = 1; // 1 = yes vote + EXPECT_TRUE(vote(id(compIdx, 1, 2, 3), voteInput)); + } + + auto results = getVotingResults(proposalIndex); + EXPECT_TRUE(results.okay); + EXPECT_EQ(results.results.optionVoteCount.get(0), uint32(votesNo)); + EXPECT_EQ(results.results.optionVoteCount.get(1), uint32(votesYes)); + } +}; + +static id ENTITY0(7, 0, 0, 0); +static id ENTITY1(100, 0, 0, 0); +static id ENTITY2(123, 456, 789, 0); +static id ENTITY3(42, 69, 0, 13); +static id ENTITY4(3, 14, 2, 7); + +TEST(ContractCCF, BasicInitialization) +{ + ContractTestingCCF test; + + // Check initial state + auto fee = test.getProposalFee(); + EXPECT_EQ(fee.proposalFee, 1000000u); +} + +TEST(ContractCCF, RegularProposalAndVoting) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + + // Set a regular transfer proposal + increaseEnergy(PROPOSER1, 1000000); + uint32 proposalIndex = test.setupRegularProposal(PROPOSER1, ENTITY1, 10000); + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Get proposal + auto proposal = test.getProposal(proposalIndex); + EXPECT_TRUE(proposal.okay); + EXPECT_EQ(proposal.proposal.transfer.destination, ENTITY1); + + // Vote on proposal + test.voteMultipleComputors(proposalIndex, 200, 350); + + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + + // End epoch to process votes + test.endEpoch(); + + // Check that transfer was executed + auto transfers = test.getLatestTransfers(); + bool found = false; + for (uint64 i = 0; i < transfers.capacity(); ++i) + { + if (transfers.get(i).destination == ENTITY1 && transfers.get(i).amount == 10000) + { + found = true; + EXPECT_TRUE(transfers.get(i).success); + break; + } + } + EXPECT_TRUE(found); +} + +TEST(ContractCCF, SubscriptionProposalCreation) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Create a subscription proposal + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 12, 4, system.epoch + 1); // 4 weeks per period (monthly) + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Check that subscription proposal was stored + auto state = test.getState(); + EXPECT_TRUE(state->hasSubscription(PROPOSER1)); + EXPECT_FALSE(state->getSubscriptionIsActiveByProposer(PROPOSER1)); // Not active until accepted + EXPECT_EQ(state->getSubscriptionWeeksPerPeriodByProposer(PROPOSER1), 4u); + EXPECT_EQ(state->getSubscriptionNumberOfPeriodsByProposer(PROPOSER1), 12u); + EXPECT_EQ(state->getSubscriptionAmountPerPeriodByProposer(PROPOSER1), 1000); + EXPECT_EQ(state->getSubscriptionStartEpochByProposer(PROPOSER1), system.epoch + 1); + EXPECT_EQ(state->getSubscriptionCurrentPeriodByProposer(PROPOSER1), -1); + + // Get proposal with subscription data + auto proposal = test.getProposal(proposalIndex, NULL_ID); + EXPECT_TRUE(proposal.okay); + EXPECT_TRUE(proposal.hasSubscriptionProposal); + EXPECT_FALSE(isZero(proposal.subscriptionProposal.proposerId)); +} + +TEST(ContractCCF, SubscriptionProposalVotingAndActivation) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + // Create a subscription proposal starting next epoch + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, system.epoch + 1); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Vote to approve + test.voteMultipleComputors(proposalIndex, 200, 350); + + // End epoch + test.endEpoch(); + + auto state = test.getState(); + + // Check subscription is now active (identified by destination) + EXPECT_TRUE(state->hasActiveSubscription(ENTITY1)); + EXPECT_TRUE(state->getSubscriptionIsActive(ENTITY1)); +} + +TEST(ContractCCF, SubscriptionPaymentProcessing) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + // Create subscription starting in epoch 189, weekly payments + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 500, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Approve proposal + test.voteMultipleComputors(proposalIndex, 200, 350); + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + sint64 initialBalance = getBalance(ENTITY1); + + // Move to start epoch and activate + system.epoch = 189; + test.beginEpoch(); + test.endEpoch(); + + // Move to next epoch - should trigger first payment + system.epoch = 190; + test.beginEpoch(); + test.endEpoch(); + + // Check payment was made + sint64 newBalance = getBalance(ENTITY1); + EXPECT_GE(newBalance, initialBalance + 500 + 500); + + // Check regular payments log + auto payments = test.getRegularPayments(); + bool foundPayment = false; + for (uint64 i = 0; i < payments.capacity(); ++i) + { + const auto& payment = payments.get(i); + if (payment.destination == ENTITY1 && payment.amount == 500 && payment.periodIndex == 0) + { + foundPayment = true; + EXPECT_TRUE(payment.success); + break; + } + } + EXPECT_TRUE(foundPayment); + + // Check subscription currentPeriod was updated + auto state = test.getState(); + EXPECT_EQ(state->getSubscriptionCurrentPeriod(ENTITY1), 1); +} + +TEST(ContractCCF, MultipleSubscriptionPayments) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + // Create monthly subscription (4 epochs per period) + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 3, 4, 189); // 4 weeks per period (monthly) + + test.voteMultipleComputors(proposalIndex, 200, 350); + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + sint64 initialBalance = getBalance(ENTITY1); + + // Activate subscription + system.epoch = 189; + test.beginEpoch(); + test.endEpoch(); + + // Move through epochs - should trigger payments at epochs 189, 193, 197 + for (uint32 epoch = 189; epoch <= 197; ++epoch) + { + system.epoch = epoch; + test.beginEpoch(); + test.endEpoch(); + } + + // Should have made 3 payments (periods 0, 1, 2) + sint64 newBalance = getBalance(ENTITY1); + EXPECT_GE(newBalance, initialBalance + 1000 + 1000 + 1000); + + // Check subscription completed all periods + auto state = test.getState(); + EXPECT_EQ(state->getSubscriptionCurrentPeriod(ENTITY1), -1); +} + +TEST(ContractCCF, PreventMultipleActiveSubscriptions) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + // Create first subscription + uint32 proposalIndex1 = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex1, (int)INVALID_PROPOSAL_INDEX); + + increaseEnergy(PROPOSER1, 1000000); + // Try to create second subscription for same proposer - should overwrite the previous one + uint32 proposalIndex2 = test.setupSubscriptionProposal( + PROPOSER1, ENTITY2, 2000, 4, 1, 189, true); // 1 week per period (weekly) + EXPECT_EQ((int)proposalIndex2, (int)proposalIndex1); +} + +TEST(ContractCCF, CancelSubscription) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + // Create subscription + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + // Cancel proposal (epoch = 0) + CCF::SetProposal_input cancelInput; + setMemory(cancelInput, 0); + cancelInput.proposal.epoch = 0; + cancelInput.proposal.type = ProposalTypes::TransferYesNo; + cancelInput.isSubscription = true; + cancelInput.weeksPerPeriod = 1; // 1 week per period (weekly) + cancelInput.numberOfPeriods = 4; + cancelInput.startEpoch = 189; + cancelInput.amountPerPeriod = 1000; + auto cancelOutput = test.setProposal(PROPOSER1, cancelInput); + EXPECT_NE((int)cancelOutput.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Check subscription was deactivated + auto state = test.getState(); + EXPECT_FALSE(state->hasSubscriptionProposal(PROPOSER1)); // proposal is canceled, so no subscription proposal +} + +TEST(ContractCCF, SubscriptionValidation) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Test invalid weeksPerPeriod (must be > 0) + CCF::SetProposal_input input; + setMemory(input, 0); + input.proposal.epoch = system.epoch; + input.proposal.type = ProposalTypes::TransferYesNo; + input.proposal.transfer.destination = ENTITY1; + input.proposal.transfer.amount = 1000; + input.isSubscription = true; + input.weeksPerPeriod = 0; + input.numberOfPeriods = 4; + input.startEpoch = system.epoch; + input.amountPerPeriod = 1000; + + // Test start epoch in past + increaseEnergy(PROPOSER1, 1000000); + input.weeksPerPeriod = 1; // 1 week per period (weekly) + input.startEpoch = system.epoch - 1; // Should be >= current epoch + auto output = test.setProposal(PROPOSER1, input); + EXPECT_EQ((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Test that zero numberOfPeriods is allowed (will cancel subscription when accepted) + increaseEnergy(PROPOSER1, 1000000); + input.weeksPerPeriod = 1; + input.startEpoch = system.epoch; + input.numberOfPeriods = 0; // Allowed - will cancel subscription + input.amountPerPeriod = 1000; + output = test.setProposal(PROPOSER1, input); + EXPECT_NE((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Test that zero amountPerPeriod is allowed (will cancel subscription when accepted) + increaseEnergy(PROPOSER1, 1000000); + input.numberOfPeriods = 4; + input.amountPerPeriod = 0; // Allowed - will cancel subscription + output = test.setProposal(PROPOSER1, input); + EXPECT_NE((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); +} + +TEST(ContractCCF, MultipleProposers) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + id PROPOSER2 = broadcastedComputors.computors.publicKeys[1]; + increaseEnergy(PROPOSER2, 1000000); + + // Create subscriptions for different proposers + uint32 proposalIndex1 = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex1, (int)INVALID_PROPOSAL_INDEX); + uint32 proposalIndex2 = test.setupSubscriptionProposal( + PROPOSER2, ENTITY2, 2000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex2, (int)INVALID_PROPOSAL_INDEX); + + // Both proposals need to first be voted in before the subscriptions become active. + auto state = test.getState(); + EXPECT_FALSE(state->hasActiveSubscription(ENTITY1)); + EXPECT_FALSE(state->hasActiveSubscription(ENTITY2)); + EXPECT_EQ(state->countActiveSubscriptions(), 0u); + + // Vote in both subscription proposals to activate them + test.voteMultipleComputors(proposalIndex1, 200, 400); + test.voteMultipleComputors(proposalIndex2, 200, 400); + + // Increase energy so contract can execute the subscriptions + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 10000000); + test.endEpoch(); + + // Now both should be active subscriptions (by destination) + EXPECT_TRUE(state->hasActiveSubscription(ENTITY1)); + EXPECT_TRUE(state->hasActiveSubscription(ENTITY2)); + EXPECT_EQ(state->countActiveSubscriptions(), 2u); +} + +TEST(ContractCCF, ProposalRejectedNoQuorum) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + uint32 proposalIndex = test.setupRegularProposal(PROPOSER1, ENTITY1, 10000); + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Vote but not enough for quorum + test.voteMultipleComputors(proposalIndex, 100, 200); + + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Transfer should not have been executed + auto transfers = test.getLatestTransfers(); + bool found = false; + for (uint64 i = 0; i < transfers.capacity(); ++i) + { + if (transfers.get(i).destination == ENTITY1 && transfers.get(i).amount == 10000) + { + found = true; + break; + } + } + EXPECT_FALSE(found); +} + +TEST(ContractCCF, ProposalRejectedMoreNoVotes) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + uint32 proposalIndex = test.setupRegularProposal(PROPOSER1, ENTITY1, 10000); + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // More "no" votes than "yes" votes + test.voteMultipleComputors(proposalIndex, 350, 200); + + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Transfer should not have been executed + auto transfers = test.getLatestTransfers(); + bool found = false; + for (uint64 i = 0; i < transfers.capacity(); ++i) + { + if (transfers.get(i).destination == ENTITY1 && transfers.get(i).amount == 10000) + { + found = true; + break; + } + } + EXPECT_FALSE(found); +} + +TEST(ContractCCF, SubscriptionMaxEpochsValidation) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Try to create subscription that exceeds max epochs (52) + // Monthly subscription with 14 periods = 14 * 4 = 56 epochs > 52 + CCF::SetProposal_input input; + setMemory(input, 0); + input.proposal.epoch = system.epoch; + input.proposal.type = ProposalTypes::TransferYesNo; + input.proposal.transfer.destination = ENTITY1; + input.proposal.transfer.amount = 1000; + input.isSubscription = true; + input.weeksPerPeriod = 4; // 4 weeks per period (monthly) + input.numberOfPeriods = 14; // 14 * 4 = 56 epochs > 52 max + input.startEpoch = system.epoch + 1; + input.amountPerPeriod = 1000; + + auto output = test.setProposal(PROPOSER1, input); + EXPECT_EQ((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Try with valid number (12 months = 48 epochs < 52) + input.numberOfPeriods = 12; + output = test.setProposal(PROPOSER1, input); + EXPECT_NE((int)output.proposalIndex, (int)INVALID_PROPOSAL_INDEX); +} + +TEST(ContractCCF, SubscriptionExpiration) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Create weekly subscription with 3 periods + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 500, 3, 1, 189); // 1 week per period (weekly) + + test.voteMultipleComputors(proposalIndex, 200, 350); + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + sint64 initialBalance = getBalance(ENTITY1); + + // Activate and process payments + system.epoch = 189; + test.beginEpoch(); + test.endEpoch(); + + // Process first payment (epoch 190) + system.epoch = 190; + test.beginEpoch(); + test.endEpoch(); + + // Process second payment (epoch 191) + system.epoch = 191; + test.beginEpoch(); + test.endEpoch(); + + sint64 balanceAfter3Payments = getBalance(ENTITY1); + EXPECT_GE(balanceAfter3Payments, initialBalance + 500 + 500 + 500); + + // Move to epoch 192 - subscription should be expired, no more payments + system.epoch = 192; + test.beginEpoch(); + test.endEpoch(); + + sint64 balanceAfterExpiration = getBalance(ENTITY1); + EXPECT_EQ(balanceAfterExpiration, balanceAfter3Payments); // No new payment +} + +TEST(ContractCCF, GetProposalIndices) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + // Create multiple proposals + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + id PROPOSER2 = broadcastedComputors.computors.publicKeys[1]; + increaseEnergy(PROPOSER2, 1000000); + uint32 proposalIndex1 = test.setupRegularProposal(PROPOSER1, ENTITY1, 1000); + EXPECT_NE((int)proposalIndex1, (int)INVALID_PROPOSAL_INDEX); + uint32 proposalIndex2 = test.setupRegularProposal(PROPOSER2, ENTITY2, 2000); + EXPECT_NE((int)proposalIndex2, (int)INVALID_PROPOSAL_INDEX); + + auto output = test.getProposalIndices(true, -1); + + EXPECT_GE((int)output.numOfIndices, 2); + bool found1 = false, found2 = false; + for (uint32 i = 0; i < output.numOfIndices; ++i) + { + if (output.indices.get(i) == proposalIndex1) + found1 = true; + if (output.indices.get(i) == proposalIndex2) + found2 = true; + } + EXPECT_TRUE(found1); + EXPECT_TRUE(found2); +} + +TEST(ContractCCF, SubscriptionSlotReuse) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Create and cancel a subscription + uint32 proposalIndex1 = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex1, (int)INVALID_PROPOSAL_INDEX); + + // Cancel it + increaseEnergy(PROPOSER1, 1000000); + + CCF::SetProposal_input cancelInput; + setMemory(cancelInput, 0); + cancelInput.proposal.epoch = 0; + cancelInput.proposal.type = ProposalTypes::TransferYesNo; + cancelInput.proposal.transfer.destination = ENTITY1; + cancelInput.proposal.transfer.amount = 1000; + cancelInput.weeksPerPeriod = 1; // 1 week per period (weekly) + cancelInput.numberOfPeriods = 4; + cancelInput.startEpoch = 189; + cancelInput.amountPerPeriod = 1000; + cancelInput.isSubscription = true; + auto cancelOutput = test.setProposal(PROPOSER1, cancelInput); + EXPECT_NE((int)cancelOutput.proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Create a new subscription - should reuse the slot + increaseEnergy(PROPOSER1, 1000000); + + uint32 proposalIndex2 = test.setupSubscriptionProposal( + PROPOSER1, ENTITY2, 2000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_EQ((int)proposalIndex2, (int)proposalIndex1); + + // Vote in the new subscription proposal to activate it + test.voteMultipleComputors(proposalIndex2, 200, 400); + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Check that subscription was updated (identified by destination) + auto state = test.getState(); + EXPECT_EQ(state->countActiveSubscriptions(), 1u); + EXPECT_EQ(state->getSubscriptionAmountPerPeriod(ENTITY2), 2000); // New subscription for ENTITY2 +} + +TEST(ContractCCF, CancelSubscriptionByZeroAmount) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Create and activate a subscription + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Vote to approve + test.voteMultipleComputors(proposalIndex, 200, 350); + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Move to start epoch to activate + system.epoch = 189; + test.beginEpoch(); + test.endEpoch(); + + // Verify subscription is active + auto state = test.getState(); + EXPECT_TRUE(state->hasActiveSubscription(ENTITY1)); + EXPECT_EQ(state->getSubscriptionAmountPerPeriod(ENTITY1), 1000); + + // Propose cancellation by setting amountPerPeriod to 0 + increaseEnergy(PROPOSER1, 1000000); + CCF::SetProposal_input cancelInput; + setMemory(cancelInput, 0); + cancelInput.proposal.epoch = system.epoch; + cancelInput.proposal.type = ProposalTypes::TransferYesNo; + cancelInput.proposal.transfer.destination = ENTITY1; + cancelInput.proposal.transfer.amount = 0; + cancelInput.isSubscription = true; + cancelInput.weeksPerPeriod = 1; + cancelInput.numberOfPeriods = 4; + cancelInput.startEpoch = system.epoch + 1; + cancelInput.amountPerPeriod = 0; // Zero amount will cancel subscription + + uint32 cancelProposalIndex = test.setProposal(PROPOSER1, cancelInput).proposalIndex; + EXPECT_NE((int)cancelProposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Vote to approve cancellation + test.voteMultipleComputors(cancelProposalIndex, 200, 350); + // Increase energy for contract + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Verify subscription was deleted + state = test.getState(); + EXPECT_FALSE(state->hasActiveSubscription(ENTITY1)); + EXPECT_EQ(state->countActiveSubscriptions(), 0u); +} + +TEST(ContractCCF, CancelSubscriptionByZeroPeriods) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Create and activate a subscription + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, 189); // 1 week per period (weekly) + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Vote to approve + test.voteMultipleComputors(proposalIndex, 200, 350); + // Increase energy for contract to pay for the proposal + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Move to start epoch to activate + system.epoch = 189; + test.beginEpoch(); + test.endEpoch(); + + // Verify subscription is active + auto state = test.getState(); + EXPECT_TRUE(state->hasActiveSubscription(ENTITY1)); + + // Propose cancellation by setting numberOfPeriods to 0 + increaseEnergy(PROPOSER1, 1000000); + CCF::SetProposal_input cancelInput; + setMemory(cancelInput, 0); + cancelInput.proposal.epoch = system.epoch; + cancelInput.proposal.type = ProposalTypes::TransferYesNo; + cancelInput.proposal.transfer.destination = ENTITY1; + cancelInput.proposal.transfer.amount = 0; + cancelInput.isSubscription = true; + cancelInput.weeksPerPeriod = 1; + cancelInput.numberOfPeriods = 0; // Zero periods will cancel subscription + cancelInput.startEpoch = system.epoch + 1; + cancelInput.amountPerPeriod = 1000; + + uint32 cancelProposalIndex = test.setProposal(PROPOSER1, cancelInput).proposalIndex; + EXPECT_NE((int)cancelProposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Vote to approve cancellation + test.voteMultipleComputors(cancelProposalIndex, 200, 350); + // Increase energy for contract + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Verify subscription was deleted + state = test.getState(); + EXPECT_FALSE(state->hasActiveSubscription(ENTITY1)); + EXPECT_EQ(state->countActiveSubscriptions(), 0u); +} + +TEST(ContractCCF, SubscriptionWithDifferentWeeksPerPeriod) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + + // Create subscription with 2 weeks per period + uint32 proposalIndex = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 6, 2, 189); // 2 weeks per period, 6 periods + EXPECT_NE((int)proposalIndex, (int)INVALID_PROPOSAL_INDEX); + + // Verify proposal data + auto state = test.getState(); + EXPECT_EQ(state->getSubscriptionWeeksPerPeriodByProposer(PROPOSER1), 2u); + EXPECT_EQ(state->getSubscriptionNumberOfPeriodsByProposer(PROPOSER1), 6u); + + // Vote to approve + test.voteMultipleComputors(proposalIndex, 200, 350); + // Increase energy for contract + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + // Still period -1, no payment yet + EXPECT_EQ(state->getSubscriptionCurrentPeriod(ENTITY1), -1); + + // Move to start epoch + system.epoch = 189; + test.beginEpoch(); + test.endEpoch(); + + // period 0, it is the first payment period. + EXPECT_EQ(state->getSubscriptionCurrentPeriod(ENTITY1), 0); + + // Verify subscription is active with correct weeksPerPeriod + state = test.getState(); + EXPECT_TRUE(state->hasActiveSubscription(ENTITY1)); + EXPECT_EQ(state->getSubscriptionWeeksPerPeriod(ENTITY1), 2u); + EXPECT_EQ(state->getSubscriptionNumberOfPeriods(ENTITY1), 6u); + + system.epoch = 190; + test.beginEpoch(); + test.endEpoch(); + + system.epoch = 191; + test.beginEpoch(); + test.endEpoch(); + + // period 1, it is the second payment period. + EXPECT_EQ(state->getSubscriptionCurrentPeriod(ENTITY1), 1); + + sint64 balance = getBalance(ENTITY1); + EXPECT_GE(balance, 1000); // Payment was made +} + +TEST(ContractCCF, SubscriptionOverwriteByDestination) +{ + ContractTestingCCF test; + system.epoch = 188; + test.beginEpoch(); + + id PROPOSER1 = broadcastedComputors.computors.publicKeys[0]; + increaseEnergy(PROPOSER1, 1000000); + id PROPOSER2 = broadcastedComputors.computors.publicKeys[1]; + increaseEnergy(PROPOSER2, 1000000); + + // PROPOSER1 creates subscription for ENTITY1 + uint32 proposalIndex1 = test.setupSubscriptionProposal( + PROPOSER1, ENTITY1, 1000, 4, 1, 189); + EXPECT_NE((int)proposalIndex1, (int)INVALID_PROPOSAL_INDEX); + + // Vote and activate + test.voteMultipleComputors(proposalIndex1, 200, 350); + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + system.epoch = 189; + test.beginEpoch(); + test.endEpoch(); + + // Verify first subscription is active + auto state = test.getState(); + EXPECT_TRUE(state->hasActiveSubscription(ENTITY1)); + EXPECT_EQ(state->getSubscriptionAmountPerPeriod(ENTITY1), 1000); + + // PROPOSER2 creates a new subscription proposal for the same destination + // This should overwrite the existing subscription when accepted + uint32 proposalIndex2 = test.setupSubscriptionProposal( + PROPOSER2, ENTITY1, 2000, 6, 2, system.epoch + 1); // Different amount and schedule + EXPECT_NE((int)proposalIndex2, (int)INVALID_PROPOSAL_INDEX); + + // Vote and activate the new subscription + test.voteMultipleComputors(proposalIndex2, 200, 350); + increaseEnergy(id(CCF_CONTRACT_INDEX, 0, 0, 0), 1000000); + test.endEpoch(); + + system.epoch = 190; + test.beginEpoch(); + test.endEpoch(); + + // Verify the subscription was overwritten + state = test.getState(); + EXPECT_TRUE(state->hasActiveSubscription(ENTITY1)); + EXPECT_EQ(state->getSubscriptionAmountPerPeriod(ENTITY1), 2000); // New amount + EXPECT_EQ(state->getSubscriptionWeeksPerPeriod(ENTITY1), 2u); // New schedule + EXPECT_EQ(state->getSubscriptionNumberOfPeriods(ENTITY1), 6u); // New number of periods + EXPECT_EQ(state->countActiveSubscriptions(), 1u); // Still only one subscription per destination +} diff --git a/test/contract_msvault.cpp b/test/contract_msvault.cpp index 9c51671f8..e37260230 100644 --- a/test/contract_msvault.cpp +++ b/test/contract_msvault.cpp @@ -8,9 +8,13 @@ static const id OWNER2 = ID(_F, _X, _J, _F, _B, _T, _J, _M, _Y, _F, _J, _H, _P, static const id OWNER3 = ID(_K, _E, _F, _D, _Z, _T, _Y, _L, _F, _E, _R, _A, _H, _D, _V, _L, _N, _Q, _O, _R, _D, _H, _F, _Q, _I, _B, _S, _B, _Z, _C, _W, _S, _Z, _X, _Z, _F, _F, _A, _N, _O, _T, _F, _A, _H, _W, _M, _O, _V, _G, _T, _R, _Q, _J, _P, _X, _D); static const id TEST_VAULT_NAME = ID(_M, _Y, _M, _S, _V, _A, _U, _L, _U, _S, _E, _D, _F, _O, _R, _U, _N, _I, _T, _T, _T, _E, _S, _T, _I, _N, _G, _P, _U, _R, _P, _O, _S, _E, _S, _O, _N, _L, _Y, _U, _N, _I, _T, _T, _E, _S, _C, _O, _R, _E, _S, _M, _A, _R, _T, _T); -static constexpr uint64 TWO_OF_TWO = 2ULL; +static constexpr uint64 TWO_OF_TWO = 2ULL; static constexpr uint64 TWO_OF_THREE = 2ULL; +static const id DESTINATION = id::randomValue(); +static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ull; +static constexpr uint64 QX_MANAGEMENT_TRANSFER_FEE = 100ull; + class ContractTestingMsVault : protected ContractTesting { public: @@ -20,6 +24,8 @@ class ContractTestingMsVault : protected ContractTesting initEmptyUniverse(); INIT_CONTRACT(MSVAULT); callSystemProcedure(MSVAULT_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); } void beginEpoch(bool expectSuccess = true) @@ -32,7 +38,7 @@ class ContractTestingMsVault : protected ContractTesting callSystemProcedure(MSVAULT_CONTRACT_INDEX, END_EPOCH, expectSuccess); } - void registerVault(uint64 requiredApprovals, id vaultName, const std::vector& owners, uint64 fee) + MSVAULT::registerVault_output registerVault(uint64 requiredApprovals, id vaultName, const std::vector& owners, uint64 fee) { MSVAULT::registerVault_input input; for (uint64 i = 0; i < MSVAULT_MAX_OWNERS; i++) @@ -43,18 +49,20 @@ class ContractTestingMsVault : protected ContractTesting input.vaultName = vaultName; MSVAULT::registerVault_output regOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 1, input, regOut, owners[0], fee); + return regOut; } - void deposit(uint64 vaultId, uint64 amount, const id& from) + MSVAULT::deposit_output deposit(uint64 vaultId, uint64 amount, const id& from) { MSVAULT::deposit_input input; input.vaultId = vaultId; increaseEnergy(from, amount); MSVAULT::deposit_output depOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 2, input, depOut, from, amount); + return depOut; } - void releaseTo(uint64 vaultId, uint64 amount, const id& destination, const id& owner, uint64 fee = MSVAULT_RELEASE_FEE) + MSVAULT::releaseTo_output releaseTo(uint64 vaultId, uint64 amount, const id& destination, const id& owner, uint64 fee = MSVAULT_RELEASE_FEE) { MSVAULT::releaseTo_input input; input.vaultId = vaultId; @@ -64,9 +72,10 @@ class ContractTestingMsVault : protected ContractTesting increaseEnergy(owner, fee); MSVAULT::releaseTo_output relOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 3, input, relOut, owner, fee); + return relOut; } - void resetRelease(uint64 vaultId, const id& owner, uint64 fee = MSVAULT_RELEASE_RESET_FEE) + MSVAULT::resetRelease_output resetRelease(uint64 vaultId, const id& owner, uint64 fee = MSVAULT_RELEASE_RESET_FEE) { MSVAULT::resetRelease_input input; input.vaultId = vaultId; @@ -74,6 +83,7 @@ class ContractTestingMsVault : protected ContractTesting increaseEnergy(owner, fee); MSVAULT::resetRelease_output rstOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 4, input, rstOut, owner, fee); + return rstOut; } MSVAULT::getVaultName_output getVaultName(uint64 vaultId) const @@ -131,11 +141,115 @@ class ContractTestingMsVault : protected ContractTesting return -1; } + void issueAsset(const id& issuer, const std::string& assetNameStr, sint64 numberOfShares) + { + uint64 assetName = assetNameFromString(assetNameStr.c_str()); + QX::IssueAsset_input input{ assetName, numberOfShares, 0, 0 }; + QX::IssueAsset_output output; + increaseEnergy(issuer, QX_ISSUE_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); + } + + QX::TransferShareOwnershipAndPossession_output transferAsset(const id& from, const id& to, const Asset& asset, uint64_t amount) { + QX::TransferShareOwnershipAndPossession_input input; + input.issuer = asset.issuer; + input.newOwnerAndPossessor = to; + input.assetName = asset.assetName; + input.numberOfShares = amount; + QX::TransferShareOwnershipAndPossession_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, from, 1000000); + return output; + } + + int64_t transferShareManagementRights(const id& from, const Asset& asset, sint64 numberOfShares, uint32 newManagingContractIndex) + { + QX::TransferShareManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = newManagingContractIndex; + QX::TransferShareManagementRights_output output; + output.transferredNumberOfShares = 0; + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, from, 0); + return output.transferredNumberOfShares; + } + + MSVAULT::revokeAssetManagementRights_output revokeAssetManagementRights(const id& from, const Asset& asset, sint64 numberOfShares) + { + MSVAULT::revokeAssetManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + MSVAULT::revokeAssetManagementRights_output output; + output.transferredNumberOfShares = 0; + output.status = 0; + + // The fee required by QX is 100. Do this to ensure enough fee. + const uint64 fee = 100; + increaseEnergy(from, fee); + + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 25, input, output, from, fee); + return output; + } + + MSVAULT::depositAsset_output depositAsset(uint64 vaultId, const Asset& asset, uint64 amount, const id& from) + { + MSVAULT::depositAsset_input input; + input.vaultId = vaultId; + input.asset = asset; + input.amount = amount; + MSVAULT::depositAsset_output output; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 19, input, output, from, 0); + return output; + } + + MSVAULT::releaseAssetTo_output releaseAssetTo(uint64 vaultId, const Asset& asset, uint64 amount, const id& destination, const id& owner, uint64 fee = MSVAULT_RELEASE_FEE) + { + MSVAULT::releaseAssetTo_input input; + input.vaultId = vaultId; + input.asset = asset; + input.amount = amount; + input.destination = destination; + + increaseEnergy(owner, fee); + MSVAULT::releaseAssetTo_output output; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 20, input, output, owner, fee); + return output; + } + + MSVAULT::resetAssetRelease_output resetAssetRelease(uint64 vaultId, const id& owner, uint64 fee = MSVAULT_RELEASE_RESET_FEE) + { + MSVAULT::resetAssetRelease_input input; + input.vaultId = vaultId; + + increaseEnergy(owner, fee); + MSVAULT::resetAssetRelease_output output; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 21, input, output, owner, fee); + return output; + } + + MSVAULT::getVaultAssetBalances_output getVaultAssetBalances(uint64 vaultId) const + { + MSVAULT::getVaultAssetBalances_input input; + input.vaultId = vaultId; + MSVAULT::getVaultAssetBalances_output output; + callFunction(MSVAULT_CONTRACT_INDEX, 22, input, output); + return output; + } + + MSVAULT::getAssetReleaseStatus_output getAssetReleaseStatus(uint64 vaultId) const + { + MSVAULT::getAssetReleaseStatus_input input; + input.vaultId = vaultId; + MSVAULT::getAssetReleaseStatus_output output; + callFunction(MSVAULT_CONTRACT_INDEX, 23, input, output); + return output; + } + ~ContractTestingMsVault() { } }; + TEST(ContractMsVault, RegisterVault_InsufficientFee) { ContractTestingMsVault msVault; @@ -144,8 +258,10 @@ TEST(ContractMsVault, RegisterVault_InsufficientFee) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + // Attempt with insufficient fee - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, 5000ULL); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, 5000ULL); + EXPECT_EQ(regOut.status, 2ULL); // FAILURE_INSUFFICIENT_FEE // No new vault should be created auto vaultsO1After = msVault.getVaults(OWNER1); @@ -158,8 +274,10 @@ TEST(ContractMsVault, RegisterVault_OneOwner) ContractTestingMsVault msVault; auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + // Only one owner => should fail - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 5ULL); // FAILURE_INVALID_PARAMS // Should fail, no new vault auto vaultsO1After = msVault.getVaults(OWNER1); @@ -173,7 +291,8 @@ TEST(ContractMsVault, RegisterVault_Success) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); // SUCCESS auto vaultsO1After = msVault.getVaults(OWNER1); EXPECT_EQ(static_cast(vaultsO1After.numberOfVaults), @@ -202,7 +321,8 @@ TEST(ContractMsVault, GetVaultName) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1After = msVault.getVaults(OWNER1); EXPECT_EQ(static_cast(vaultsO1After.numberOfVaults), @@ -221,7 +341,8 @@ TEST(ContractMsVault, Deposit_InvalidVault) ContractTestingMsVault msVault; // deposit to a non-existent vault auto beforeBalance = msVault.getBalanceOf(999ULL); - msVault.deposit(999ULL, 5000ULL, OWNER1); + auto depOut = msVault.deposit(999ULL, 5000ULL, OWNER1); + EXPECT_EQ(depOut.status, 3ULL); // FAILURE_INVALID_VAULT // no change in balance auto afterBalance = msVault.getBalanceOf(999ULL); EXPECT_EQ(afterBalance.balance, beforeBalance.balance); @@ -234,13 +355,15 @@ TEST(ContractMsVault, Deposit_Success) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1After = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1After.vaultIds.get(vaultsO1Before.numberOfVaults); auto balBefore = msVault.getBalanceOf(vaultId); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto balAfter = msVault.getBalanceOf(vaultId); EXPECT_EQ(balAfter.balance, balBefore.balance + 10000ULL); } @@ -250,16 +373,19 @@ TEST(ContractMsVault, ReleaseTo_NonOwner) ContractTestingMsVault msVault; increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto releaseStatusBefore = msVault.getReleaseStatus(vaultId); // Non-owner attempt release - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER3); + auto relOut = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER3); + EXPECT_EQ(relOut.status, 4ULL); // FAILURE_NOT_AUTHORIZED auto releaseStatusAfter = msVault.getReleaseStatus(vaultId); // No approvals should be set @@ -273,21 +399,25 @@ TEST(ContractMsVault, ReleaseTo_InvalidParams) increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); // 2 out of 2 owners - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto releaseStatusBefore = msVault.getReleaseStatus(vaultId); // amount=0 - msVault.releaseTo(vaultId, 0ULL, OWNER2, OWNER1); + auto relOut1 = msVault.releaseTo(vaultId, 0ULL, OWNER2, OWNER1); + EXPECT_EQ(relOut1.status, 5ULL); // FAILURE_INVALID_PARAMS auto releaseStatusAfter1 = msVault.getReleaseStatus(vaultId); EXPECT_EQ(releaseStatusAfter1.amounts.get(0), releaseStatusBefore.amounts.get(0)); // destination NULL_ID - msVault.releaseTo(vaultId, 5000ULL, NULL_ID, OWNER1); + auto relOut2 = msVault.releaseTo(vaultId, 5000ULL, NULL_ID, OWNER1); + EXPECT_EQ(relOut2.status, 5ULL); // FAILURE_INVALID_PARAMS auto releaseStatusAfter2 = msVault.getReleaseStatus(vaultId); EXPECT_EQ(releaseStatusAfter2.amounts.get(0), releaseStatusBefore.amounts.get(0)); } @@ -300,13 +430,16 @@ TEST(ContractMsVault, ReleaseTo_PartialApproval) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 3 owners - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 15000ULL, OWNER1); - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + auto depOut = msVault.deposit(vaultId, 15000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + auto relOut = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + EXPECT_EQ(relOut.status, 9ULL); // PENDING_APPROVAL auto status = msVault.getReleaseStatus(vaultId); // Partial approval means just first owner sets the request @@ -323,18 +456,22 @@ TEST(ContractMsVault, ReleaseTo_FullApproval) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 3 - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); // OWNER1 requests 5000 Qubics to OWNER3 - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + auto relOut1 = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + EXPECT_EQ(relOut1.status, 9ULL); // PENDING_APPROVAL // Not approved yet - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER2); // second approval + auto relOut2 = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER2); // second approval + EXPECT_EQ(relOut2.status, 1ULL); // SUCCESS // After full approval, amount should be released auto bal = msVault.getBalanceOf(vaultId); @@ -350,16 +487,19 @@ TEST(ContractMsVault, ReleaseTo_InsufficientBalance) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 2 - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto balBefore = msVault.getBalanceOf(vaultId); // Attempt to release more than balance - msVault.releaseTo(vaultId, 20000ULL, OWNER3, OWNER1); + auto relOut = msVault.releaseTo(vaultId, 20000ULL, OWNER3, OWNER1); + EXPECT_EQ(relOut.status, 6ULL); // FAILURE_INSUFFICIENT_BALANCE // Should fail, balance no change auto balAfter = msVault.getBalanceOf(vaultId); @@ -375,17 +515,21 @@ TEST(ContractMsVault, ResetRelease_NonOwner) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 2 - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 5000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 5000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); - msVault.releaseTo(vaultId, 2000ULL, OWNER2, OWNER1); + auto relOut = msVault.releaseTo(vaultId, 2000ULL, OWNER2, OWNER1); + EXPECT_EQ(relOut.status, 9ULL); // PENDING_APPROVAL auto statusBefore = msVault.getReleaseStatus(vaultId); - msVault.resetRelease(vaultId, OWNER3); // Non owner tries to reset + auto rstOut = msVault.resetRelease(vaultId, OWNER3); // Non owner tries to reset + EXPECT_EQ(rstOut.status, 4ULL); // FAILURE_NOT_AUTHORIZED auto statusAfter = msVault.getReleaseStatus(vaultId); // No change in release requests @@ -401,17 +545,22 @@ TEST(ContractMsVault, ResetRelease_Success) increaseEnergy(OWNER2, 100000000ULL); increaseEnergy(OWNER3, 100000000ULL); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 5000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 5000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); // OWNER2 requests a releaseTo - msVault.releaseTo(vaultId, 2000ULL, OWNER1, OWNER2); + auto relOut = msVault.releaseTo(vaultId, 2000ULL, OWNER1, OWNER2); + EXPECT_EQ(relOut.status, 9ULL); + // Now reset by OWNER2 - msVault.resetRelease(vaultId, OWNER2); + auto rstOut = msVault.resetRelease(vaultId, OWNER2); + EXPECT_EQ(rstOut.status, 1ULL); auto status = msVault.getReleaseStatus(vaultId); // All cleared @@ -432,60 +581,678 @@ TEST(ContractMsVault, GetVaults_Multiple) auto vaultsForOwner2Before = msVault.getVaults(OWNER2); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut1 = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut1.status, 1ULL); + auto regOut2 = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut2.status, 1ULL); auto vaultsForOwner2After = msVault.getVaults(OWNER2); EXPECT_GE(static_cast(vaultsForOwner2After.numberOfVaults), - static_cast(vaultsForOwner2Before.numberOfVaults + 2U)); + static_cast(vaultsForOwner2Before.numberOfVaults + 2U)); } TEST(ContractMsVault, GetRevenue) { ContractTestingMsVault msVault; + const Asset assetTest = { OWNER1, assetNameFromString("TESTREV") }; + + increaseEnergy(OWNER1, 1000000000ULL); + increaseEnergy(OWNER2, 1000000000ULL); + + uint64 expectedRevenue = 0; + auto revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 0U); + + // Register a vault, generating the first fee + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + expectedRevenue += MSVAULT_REGISTERING_FEE; + + auto vaults = msVault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + + // Deposit QUs to ensure the vault can pay holding fees + const uint64 depositAmount = 10000000; // 10M QUs + auto depOut = msVault.deposit(vaultId, depositAmount, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + // expectedRevenue += state.liveDepositFee; // Fee is currently 0 + + // Generate Qubic-based fees + auto relOut = msVault.releaseTo(vaultId, 1000ULL, DESTINATION, OWNER1); + EXPECT_EQ(relOut.status, 9ULL); // Pending approval + expectedRevenue += MSVAULT_RELEASE_FEE; + auto rstOut = msVault.resetRelease(vaultId, OWNER1); + EXPECT_EQ(rstOut.status, 1ULL); + expectedRevenue += MSVAULT_RELEASE_RESET_FEE; + + // Generate Asset-based fees + msVault.issueAsset(OWNER1, "TESTREV", 10000); + msVault.transferShareManagementRights(OWNER1, assetTest, 5000, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msVault.depositAsset(vaultId, assetTest, 1000, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + // expectedRevenue += state.liveDepositFee; // Fee is currently 0 + auto relAssetOut = msVault.releaseAssetTo(vaultId, assetTest, 50, DESTINATION, OWNER2); + EXPECT_EQ(relAssetOut.status, 9ULL); // Pending approval + expectedRevenue += MSVAULT_RELEASE_FEE; + auto rstAssetOut = msVault.resetAssetRelease(vaultId, OWNER2); + EXPECT_EQ(rstAssetOut.status, 1ULL); + expectedRevenue += MSVAULT_RELEASE_RESET_FEE; + + // Verify revenue before the first epoch ends + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + + msVault.endEpoch(); msVault.beginEpoch(); - auto revenueOutput = msVault.getRevenueInfo(); - EXPECT_EQ(revenueOutput.totalRevenue, 0U); - EXPECT_EQ(revenueOutput.totalDistributedToShareholders, 0U); - EXPECT_EQ(revenueOutput.numberOfActiveVaults, 0U); + // Holding fee from the active vault is collected + expectedRevenue += MSVAULT_HOLDING_FEE; - increaseEnergy(OWNER1, 100000000ULL); - increaseEnergy(OWNER2, 100000000000ULL); - increaseEnergy(OWNER3, 100000ULL); + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 1U); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + + // Verify dividends were distributed correctly based on the total revenue so far + uint64 expectedDistribution = (expectedRevenue / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS; + EXPECT_EQ(revenueInfo.totalDistributedToShareholders, expectedDistribution); + + // Make more revenue generation actions in the new epoch + auto relOut2 = msVault.releaseTo(vaultId, 2000ULL, DESTINATION, OWNER2); + EXPECT_EQ(relOut2.status, 9ULL); + expectedRevenue += MSVAULT_RELEASE_FEE; + + auto rstOut2 = msVault.resetRelease(vaultId, OWNER2); + EXPECT_EQ(rstOut2.status, 1ULL); + expectedRevenue += MSVAULT_RELEASE_RESET_FEE; - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + // Revoke some of the previously granted management rights. + // This one has a fee, but it is paid to QX, not kept by MsVault. + // Therefore, expectedRevenue should NOT be incremented. + auto revokeOut = msVault.revokeAssetManagementRights(OWNER1, assetTest, 2000); + EXPECT_EQ(revokeOut.status, 1ULL); + // End the second epoch msVault.endEpoch(); + msVault.beginEpoch(); + + // Another holding fee is collected + expectedRevenue += MSVAULT_HOLDING_FEE; + + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 1U); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + + // Verify the new cumulative dividend distribution + expectedDistribution = (expectedRevenue / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS; + EXPECT_EQ(revenueInfo.totalDistributedToShareholders, expectedDistribution); + // No new transactions in this epoch + msVault.endEpoch(); msVault.beginEpoch(); - revenueOutput = msVault.getRevenueInfo(); - // first vault is destroyed after paying dividends - EXPECT_EQ(revenueOutput.totalRevenue, MSVAULT_REGISTERING_FEE); - EXPECT_EQ(revenueOutput.totalDistributedToShareholders, ((int)MSVAULT_REGISTERING_FEE / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS); - EXPECT_EQ(revenueOutput.numberOfActiveVaults, 0U); + // A third holding fee is collected + expectedRevenue += MSVAULT_HOLDING_FEE; - increaseEnergy(OWNER1, 100000000ULL); - increaseEnergy(OWNER2, 100000000ULL); - increaseEnergy(OWNER3, 100000000ULL); + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 1U); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + // Verify the final cumulative dividend distribution + expectedDistribution = (expectedRevenue / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS; + EXPECT_EQ(revenueInfo.totalDistributedToShareholders, expectedDistribution); +} - auto vaultsO1 = msVault.getVaults(OWNER1); - uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); +TEST(ContractMsVault, ManagementRightsVsDirectDeposit) +{ + ContractTestingMsVault msvault; + + // Create an issuer and two users. + const id ISSUER = id::randomValue(); + const id USER_WITH_RIGHTS = id::randomValue(); // This user will do it correctly + const id USER_WITHOUT_RIGHTS = id::randomValue(); // This user will attempt a direct deposit first + + Asset assetTest = { ISSUER, assetNameFromString("ASSET") }; + const sint64 initialDistribution = 50000; + + // Give everyone energy for fees + increaseEnergy(ISSUER, QX_ISSUE_ASSET_FEE + (1000000 * 2)); + increaseEnergy(USER_WITH_RIGHTS, MSVAULT_REGISTERING_FEE + (1000000 * 3)); + increaseEnergy(USER_WITHOUT_RIGHTS, 1000000 * 3); // More energy for the correct attempt later + + // Issue the asset and distribute it to the two users + msvault.issueAsset(ISSUER, "ASSET", initialDistribution * 2); + msvault.transferAsset(ISSUER, USER_WITH_RIGHTS, assetTest, initialDistribution); + msvault.transferAsset(ISSUER, USER_WITHOUT_RIGHTS, assetTest, initialDistribution); + + // Verify initial on-chain balances (both users' shares are managed by QX currently) + EXPECT_EQ(numberOfShares(assetTest, { USER_WITH_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, QX_CONTRACT_INDEX }), initialDistribution); + EXPECT_EQ(numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }), initialDistribution); + + // Create a simple vault owned by USER_WITH_RIGHTS + auto regOut = msvault.registerVault(2, TEST_VAULT_NAME, { USER_WITH_RIGHTS, OWNER1 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + + auto vaults = msvault.getVaults(USER_WITH_RIGHTS); + uint64 vaultId = vaults.vaultIds.get(0); + + // User with Management Rights + const sint64 sharesToManage1 = 10000; + msvault.transferShareManagementRights(USER_WITH_RIGHTS, assetTest, sharesToManage1, MSVAULT_CONTRACT_INDEX); + + // verify that management rights were transferred successfully + EXPECT_EQ(numberOfShares(assetTest, { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }), sharesToManage1); + EXPECT_EQ(numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }), 0); + + // This user now makes multiple deposits + const sint64 deposit1_U1 = 1000; + const sint64 deposit2_U1 = 2500; + auto depAssetOut1 = msvault.depositAsset(vaultId, assetTest, deposit1_U1, USER_WITH_RIGHTS); + EXPECT_EQ(depAssetOut1.status, 1ULL); + + // Verify balances after first deposit + sint64 sc_onchain_balance = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(sc_onchain_balance, deposit1_U1); + sint64 user_managed_balance = numberOfShares(assetTest, { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user_managed_balance, sharesToManage1 - deposit1_U1); + + auto depAssetOut2 = msvault.depositAsset(vaultId, assetTest, deposit2_U1, USER_WITH_RIGHTS); + EXPECT_EQ(depAssetOut2.status, 1ULL); + + // verify balances after second deposit + sc_onchain_balance = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(sc_onchain_balance, deposit1_U1 + deposit2_U1); + sint64 user1_managed_balance = numberOfShares(assetTest, { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user1_managed_balance, sharesToManage1 - deposit1_U1 - deposit2_U1); + + // user without management rights + sint64 sc_balance_before_direct_attempt = sc_onchain_balance; + sint64 user3_balance_before = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }); + + // This user attempts to deposit directly + auto depAssetOut3 = msvault.depositAsset(vaultId, assetTest, 500, USER_WITHOUT_RIGHTS); + EXPECT_EQ(depAssetOut3.status, 6ULL); // FAILURE_INSUFFICIENT_BALANCE + + // Verify that no shares were transferred + sint64 sc_balance_after_direct_attempt = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(sc_balance_after_direct_attempt, sc_balance_before_direct_attempt); + + sint64 user3_balance_after = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }); + EXPECT_EQ(user3_balance_after, user3_balance_before); // User's balance should be unchanged + + sint64 user3_balance_after_msvault = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user3_balance_after_msvault, 0); + + // the second user now does it the correct way + const sint64 sharesToManage2 = 8000; + msvault.transferShareManagementRights(USER_WITHOUT_RIGHTS, assetTest, sharesToManage2, MSVAULT_CONTRACT_INDEX); + + // Verify their management rights were transferred successfully + EXPECT_EQ(numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }), sharesToManage2); + + const sint64 deposit1_U2 = 4000; + auto depAssetOut4 = msvault.depositAsset(vaultId, assetTest, deposit1_U2, USER_WITHOUT_RIGHTS); + EXPECT_EQ(depAssetOut4.status, 1ULL); + + // check the total balance in the smart contract + sint64 final_sc_balance = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + sint64 total_deposited = deposit1_U1 + deposit2_U1 + deposit1_U2; + EXPECT_EQ(final_sc_balance, total_deposited); + + // Also verify the second user's remaining managed balance + sint64 user2_managed_balance = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user2_managed_balance, sharesToManage2 - deposit1_U2); +} - msVault.deposit(vaultId, 500000000ULL, OWNER1); +TEST(ContractMsVault, DepositAsset_Success) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; - msVault.endEpoch(); + // Create a vault and issue an asset to OWNER1 + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + QX_ISSUE_ASSET_FEE); + auto regOut = msvault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); - msVault.beginEpoch(); + msvault.issueAsset(OWNER1, "ASSET", 1000000); + + auto OWNER4 = id::randomValue(); + + auto transfered = msvault.transferShareManagementRights(OWNER1, assetTest, 5000, MSVAULT_CONTRACT_INDEX); + + // Deposit the asset into the vault + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 500, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // Check the vault's asset balance + auto assetBalances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(assetBalances.status, 1ULL); + EXPECT_EQ(assetBalances.numberOfAssetTypes, 1ULL); + + auto firstAssetBalance = assetBalances.assetBalances.get(0); + EXPECT_EQ(firstAssetBalance.asset.issuer, assetTest.issuer); + EXPECT_EQ(firstAssetBalance.asset.assetName, assetTest.assetName); + EXPECT_EQ(firstAssetBalance.balance, 500ULL); + + // Check SC's shares + sint64 scShares = numberOfShares(assetTest, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(scShares, 500LL); +} + +TEST(ContractMsVault, DepositAsset_MaxTypes) +{ + ContractTestingMsVault msvault; + + // Create a vault + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + QX_ISSUE_ASSET_FEE * (MSVAULT_MAX_ASSET_TYPES + 1)); // Extra energy for fees + auto regOut = msvault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + + // Deposit the maximum number of different asset types + for (uint64 i = 0; i < MSVAULT_MAX_ASSET_TYPES; i++) + { + std::string assetName = "ASSET" + std::to_string(i); + Asset currentAsset = { OWNER1, assetNameFromString(assetName.c_str()) }; + msvault.issueAsset(OWNER1, assetName, 1000000); + msvault.transferShareManagementRights(OWNER1, currentAsset, 100000, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, currentAsset, 1000, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + } + + // Check if max asset types reached + auto balances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(balances.numberOfAssetTypes, MSVAULT_MAX_ASSET_TYPES); + + // Try to deposit one more asset type + Asset extraAsset = { OWNER1, assetNameFromString("ASSETE") }; + msvault.issueAsset(OWNER1, "ASSETE", 100000); + msvault.transferShareManagementRights(OWNER1, extraAsset, 100000, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, extraAsset, 1000, OWNER1); + EXPECT_EQ(depAssetOut.status, 7ULL); // FAILURE_LIMIT_REACHED + + // The number of asset types should not have increased + auto balancesAfter = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(balancesAfter.numberOfAssetTypes, MSVAULT_MAX_ASSET_TYPES); +} + +TEST(ContractMsVault, ReleaseAssetTo_FullApproval) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; + + // Create a 2-of-3 vault, issue and deposit an asset + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + MSVAULT_RELEASE_FEE + QX_ISSUE_ASSET_FEE + QX_MANAGEMENT_TRANSFER_FEE); + increaseEnergy(OWNER2, MSVAULT_RELEASE_FEE); + auto regOut = msvault.registerVault(TWO_OF_THREE, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + + msvault.issueAsset(OWNER1, "ASSET", 1000000); + msvault.transferShareManagementRights(OWNER1, assetTest, 800, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 800, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // Deposit funds into the vault to cover the upcoming management transfer fee. + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + + // Check initial balances for the destination + EXPECT_EQ(numberOfShares(assetTest, { DESTINATION, QX_CONTRACT_INDEX }, { DESTINATION, QX_CONTRACT_INDEX }), 0LL); + EXPECT_EQ(numberOfShares(assetTest, { DESTINATION, MSVAULT_CONTRACT_INDEX }, { DESTINATION, MSVAULT_CONTRACT_INDEX }), 0LL); + auto vaultAssetBalanceBefore = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultAssetBalanceBefore, 800ULL); + + // Owners approve the release + auto relAssetOut1 = msvault.releaseAssetTo(vaultId, assetTest, 800, DESTINATION, OWNER1); + EXPECT_EQ(relAssetOut1.status, 9ULL); + auto relAssetOut2 = msvault.releaseAssetTo(vaultId, assetTest, 800, DESTINATION, OWNER2); + EXPECT_EQ(relAssetOut2.status, 1ULL); + + // Check final balances + sint64 destBalanceManagedByQx = numberOfShares(assetTest, { DESTINATION, QX_CONTRACT_INDEX }, { DESTINATION, QX_CONTRACT_INDEX }); + sint64 destBalanceManagedByMsVault = numberOfShares(assetTest, { DESTINATION, MSVAULT_CONTRACT_INDEX }, { DESTINATION, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(destBalanceManagedByQx, 800LL); + EXPECT_EQ(destBalanceManagedByMsVault, 0LL); + + auto vaultAssetBalanceAfter = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultAssetBalanceAfter, 0ULL); // 800 - 800 + + // The vault's qubic balance should be 0 after paying the management transfer fee + auto vaultQubicBalanceAfter = msvault.getBalanceOf(vaultId); + EXPECT_EQ(vaultQubicBalanceAfter.balance, 0LL); + + // Release status should be reset + auto releaseStatus = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(releaseStatus.amounts.get(0), 0ULL); + EXPECT_EQ(releaseStatus.destinations.get(0), NULL_ID); +} + +TEST(ContractMsVault, ReleaseAssetTo_PartialApproval) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; + + // Setup + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + MSVAULT_RELEASE_FEE + QX_MANAGEMENT_TRANSFER_FEE); + auto regOut = msvault.registerVault(TWO_OF_THREE, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + msvault.issueAsset(OWNER1, "ASSET", 1000000); + msvault.transferShareManagementRights(OWNER1, assetTest, 800, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 800, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // Deposit the fee into the vault so it can process release requests. + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + + // Only one owner approves + auto relAssetOut = msvault.releaseAssetTo(vaultId, assetTest, 500, DESTINATION, OWNER1); + EXPECT_EQ(relAssetOut.status, 9ULL); // PENDING_APPROVAL + + // Check release status is pending + auto status = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(status.status, 1ULL); + // Owner 1 is at index 0 + EXPECT_EQ(status.assets.get(0).assetName, assetTest.assetName); + EXPECT_EQ(status.amounts.get(0), 500ULL); + EXPECT_EQ(status.destinations.get(0), DESTINATION); + // Other owner slots are empty + EXPECT_EQ(status.amounts.get(1), 0ULL); + + // Balances should be unchanged + sint64 destinationBalance = numberOfShares(assetTest, { DESTINATION, QX_CONTRACT_INDEX }, + { DESTINATION, QX_CONTRACT_INDEX }); + EXPECT_EQ(destinationBalance, 0LL); + auto vaultBalance = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultBalance, 800ULL); +} + +TEST(ContractMsVault, ResetAssetRelease_Success) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; + + // Setup + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + MSVAULT_RELEASE_RESET_FEE + MSVAULT_RELEASE_FEE + QX_MANAGEMENT_TRANSFER_FEE); + auto regOut = msvault.registerVault(TWO_OF_TWO, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + msvault.issueAsset(OWNER1, "ASSET", 1000000); + msvault.transferShareManagementRights(OWNER1, assetTest, 100, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 100, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + + // Propose and then reset a release + auto relAssetOut = msvault.releaseAssetTo(vaultId, assetTest, 50, DESTINATION, OWNER1); + EXPECT_EQ(relAssetOut.status, 9ULL); + + // Check status is pending before reset + auto statusBefore = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(statusBefore.amounts.get(0), 50ULL); + + // Reset the release + auto rstAssetOut = msvault.resetAssetRelease(vaultId, OWNER1); + EXPECT_EQ(rstAssetOut.status, 1ULL); + + // Status should be cleared for that owner + auto statusAfter = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(statusAfter.amounts.get(0), 0ULL); + EXPECT_EQ(statusAfter.destinations.get(0), NULL_ID); + + // Vault balance should be unchanged + auto vaultBalance = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultBalance, 100ULL); +} + +TEST(ContractMsVault, FullLifecycle_BalanceVerification) +{ + ContractTestingMsVault msvault; + const id USER = OWNER1; + const id PARTNER = OWNER2; + const id DESTINATION_ACC = OWNER3; + Asset assetTest = { USER, assetNameFromString("ASSET") }; + const sint64 initialShares = 10000; + const sint64 sharesToManage = 5000; + const sint64 sharesToDeposit = 4000; + const sint64 sharesToRelease = 1500; + + // Issue asset and create a type 2 vault + increaseEnergy(USER, MSVAULT_REGISTERING_FEE + QX_ISSUE_ASSET_FEE + QX_MANAGEMENT_TRANSFER_FEE); + increaseEnergy(PARTNER, MSVAULT_RELEASE_FEE); + + msvault.issueAsset(USER, "ASSET", initialShares); + auto regOut = msvault.registerVault(TWO_OF_TWO, TEST_VAULT_NAME, { USER, PARTNER }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + + auto vaults = msvault.getVaults(USER); + uint64 vaultId = vaults.vaultIds.get(0); + + // Verify user has full on-chain balance under QX management + sint64 userShares_QX = numberOfShares(assetTest, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }); + EXPECT_EQ(userShares_QX, initialShares); + + // Fund the vault for the future management transfer fee + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, USER); + EXPECT_EQ(depOut.status, 1ULL); + + // User gives MsVault management rights over a portion of their shares + msvault.transferShareManagementRights(USER, assetTest, sharesToManage, MSVAULT_CONTRACT_INDEX); + + // Verify on-chain balances after management transfer + sint64 userShares_MSVAULT_Managed = numberOfShares(assetTest, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }); + userShares_QX = numberOfShares(assetTest, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }); + EXPECT_EQ(userShares_MSVAULT_Managed, sharesToManage); + EXPECT_EQ(userShares_QX, initialShares - sharesToManage); + + // User deposits the MsVault-managed shares into the vault + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, sharesToDeposit, USER); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // User's on-chain balance of MsVault-managed shares should decrease + userShares_MSVAULT_Managed = numberOfShares(assetTest, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(userShares_MSVAULT_Managed, sharesToManage - sharesToDeposit); // 5000 - 4000 = 1000 + + // MsVault contract's on-chain balance should increase + sint64 scShares_onchain = numberOfShares(assetTest, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(scShares_onchain, sharesToDeposit); + + // Vault's internal balance should match the on-chain balance + auto vaultBalances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(vaultBalances.status, 1ULL); + EXPECT_EQ(vaultBalances.numberOfAssetTypes, 1ULL); + EXPECT_EQ(vaultBalances.assetBalances.get(0).balance, sharesToDeposit); + + // Both owners approve a release to the destination + auto relAssetOut1 = msvault.releaseAssetTo(vaultId, assetTest, sharesToRelease, DESTINATION_ACC, USER); + EXPECT_EQ(relAssetOut1.status, 9ULL); + auto relAssetOut2 = msvault.releaseAssetTo(vaultId, assetTest, sharesToRelease, DESTINATION_ACC, PARTNER); + EXPECT_EQ(relAssetOut2.status, 1ULL); + + // MsVault contract's on-chain balance should decrease + scShares_onchain = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(scShares_onchain, sharesToDeposit - sharesToRelease); + + // Vault's internal balance should be updated correctly + vaultBalances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(vaultBalances.assetBalances.get(0).balance, sharesToDeposit - sharesToRelease); + + // Vault's internal qubic balance should decrease by the fee + EXPECT_EQ(msvault.getBalanceOf(vaultId).balance, 0); + + // Destination's on-chain balance should increase, and it should be managed by QX + sint64 destinationSharesManagedByQx = numberOfShares(assetTest, { DESTINATION_ACC, QX_CONTRACT_INDEX }, { DESTINATION_ACC, QX_CONTRACT_INDEX }); + sint64 destinationSharesManagedByMsVault = numberOfShares(assetTest, { DESTINATION_ACC, MSVAULT_CONTRACT_INDEX }, { DESTINATION_ACC, MSVAULT_CONTRACT_INDEX }); + + EXPECT_EQ(destinationSharesManagedByQx, sharesToRelease); + EXPECT_EQ(destinationSharesManagedByMsVault, 0); +} + +TEST(ContractMsVault, StressTest_MultiUser_MultiAsset) +{ + ContractTestingMsVault msvault; + + // Define users, assets, and vaults + const int USER_COUNT = 16; + const int ASSET_COUNT = 8; + const int VAULT_COUNT = 3; + std::vector users; + std::vector assets; + + for (int i = 0; i < USER_COUNT; ++i) + { + users.push_back(id::randomValue()); + increaseEnergy(users[i], 1000000000000ULL); + } + + for (int i = 0; i < ASSET_COUNT; ++i) + { + // Issue each asset from a different user for variety + id issuer = users[i]; + std::string assetName = "ASSET" + std::to_string(i); + assets.push_back({ issuer, assetNameFromString(assetName.c_str()) }); + msvault.issueAsset(issuer, assetName, 1000000); // Issue 1M of each token + } + + // Create 3 vaults with different sets of 4 owners each + EXPECT_EQ(msvault.registerVault(3, id::randomValue(), { users[0], users[1], users[2], users[3] }, MSVAULT_REGISTERING_FEE).status, 1ULL); + EXPECT_EQ(msvault.registerVault(2, id::randomValue(), { users[4], users[5], users[6], users[7] }, MSVAULT_REGISTERING_FEE).status, 1ULL); + EXPECT_EQ(msvault.registerVault(4, id::randomValue(), { users[8], users[9], users[10], users[11] }, MSVAULT_REGISTERING_FEE).status, 1ULL); + + // Each of the 8 assets is deposited twice by its owner + uint64 targetVaultId = 0; + const sint64 depositAmount = 100; + + for (int i = 0; i < USER_COUNT; ++i) + { + int assetIndex = i % ASSET_COUNT; + Asset assetToDeposit = assets[assetIndex]; + id owner_of_asset = users[assetIndex]; + + msvault.transferShareManagementRights(owner_of_asset, assetToDeposit, depositAmount, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(targetVaultId, assetToDeposit, depositAmount, owner_of_asset); + EXPECT_EQ(depAssetOut.status, 1ULL); + } + + auto depOut = msvault.deposit(targetVaultId, QX_MANAGEMENT_TRANSFER_FEE, users[0]); + EXPECT_EQ(depOut.status, 1ULL); + + // Check the state of the target vault + auto vaultBalances = msvault.getVaultAssetBalances(targetVaultId); + EXPECT_EQ(vaultBalances.status, 1ULL); + EXPECT_EQ(vaultBalances.numberOfAssetTypes, (uint64_t)ASSET_COUNT); + for (uint64 i = 0; i < ASSET_COUNT; ++i) + { + // Verify each asset has a balance of 200 (deposited twice) + EXPECT_EQ(vaultBalances.assetBalances.get(i).balance, depositAmount * 2); + } + + // From Vault 0, owners 0, 1, and 2 approve a release + const id releaseDestination = users[15]; + const Asset assetToRelease = assets[0]; // Release ASSET_0 + const sint64 releaseAmount = 75; + + // A 3-of-4 vault, so we need 3 approvals + EXPECT_EQ(msvault.releaseAssetTo(targetVaultId, assetToRelease, releaseAmount, releaseDestination, users[0]).status, 9ULL); + EXPECT_EQ(msvault.releaseAssetTo(targetVaultId, assetToRelease, releaseAmount, releaseDestination, users[1]).status, 9ULL); + EXPECT_EQ(msvault.releaseAssetTo(targetVaultId, assetToRelease, releaseAmount, releaseDestination, users[2]).status, 1ULL); + + // Check destination on-chain balance + sint64 destBalance = numberOfShares(assetToRelease, { releaseDestination, QX_CONTRACT_INDEX }, + { releaseDestination, QX_CONTRACT_INDEX }); + EXPECT_EQ(destBalance, releaseAmount); + + // Check vault's internal accounting for the released asset + vaultBalances = msvault.getVaultAssetBalances(targetVaultId); + bool foundReleasedAsset = false; + for (uint64 i = 0; i < vaultBalances.numberOfAssetTypes; ++i) + { + auto bal = vaultBalances.assetBalances.get(i); + if (bal.asset.assetName == assetToRelease.assetName && bal.asset.issuer == assetToRelease.issuer) + { + // Expected balance is (100 * 2) - 75 = 125 + EXPECT_EQ(bal.balance, (depositAmount * 2) - releaseAmount); + foundReleasedAsset = true; + break; + } + } + EXPECT_TRUE(foundReleasedAsset); + + // Check MsVault's on-chain balance for the released asset + sint64 scOnChainBalance = numberOfShares(assetToRelease, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + // The total on-chain balance should also be (100 * 2) - 75 = 125 + EXPECT_EQ(scOnChainBalance, (depositAmount * 2) - releaseAmount); +} + +TEST(ContractMsVault, RevokeAssetManagementRights_Success) +{ + ContractTestingMsVault msvault; + + const id USER = OWNER1; + const Asset asset = { USER, assetNameFromString("REVOKE") }; + const sint64 initialShares = 10000; + const sint64 sharesToManage = 4000; + const sint64 sharesToRevoke = 3000; + + // Issue asset and transfer management rights to MsVault + increaseEnergy(USER, QX_ISSUE_ASSET_FEE + 1000000 + 100); // Energy for all fees + msvault.issueAsset(USER, "REVOKE", initialShares); + + // Verify initial state: all shares managed by QX + EXPECT_EQ(numberOfShares(asset, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }), initialShares); + EXPECT_EQ(numberOfShares(asset, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }), 0); + + // User gives MsVault management rights over a portion of their shares + msvault.transferShareManagementRights(USER, asset, sharesToManage, MSVAULT_CONTRACT_INDEX); + + // Verify intermediate state: rights are split between QX and MsVault + EXPECT_EQ(numberOfShares(asset, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }), initialShares - sharesToManage); + EXPECT_EQ(numberOfShares(asset, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }), sharesToManage); + + // User revokes a portion of the managed rights from MsVault. The helper now handles the fee. + auto revokeOut = msvault.revokeAssetManagementRights(USER, asset, sharesToRevoke); - revenueOutput = msVault.getRevenueInfo(); + // Verify the outcome + EXPECT_EQ(revokeOut.status, 1ULL); + EXPECT_EQ(revokeOut.transferredNumberOfShares, sharesToRevoke); - auto total_revenue = MSVAULT_REGISTERING_FEE * 2 + MSVAULT_HOLDING_FEE; - EXPECT_EQ(revenueOutput.totalRevenue, total_revenue); - EXPECT_EQ(revenueOutput.totalDistributedToShareholders, ((int)(total_revenue) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS); - EXPECT_EQ(revenueOutput.numberOfActiveVaults, 1U); + // The amount managed by MsVault should decrease + sint64 finalManagedByMsVault = numberOfShares(asset, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(finalManagedByMsVault, sharesToManage - sharesToRevoke); // 4000 - 3000 = 1000 + // The amount managed by QX should increase accordingly + sint64 finalManagedByQx = numberOfShares(asset, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }); + EXPECT_EQ(finalManagedByQx, (initialShares - sharesToManage) + sharesToRevoke); // 6000 + 3000 = 9000 } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index a1148e20f..81ff9db6f 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -227,7 +227,7 @@ class NostromoChecker : public NOST assetInfo.assetName = assetName; assetInfo.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); EXPECT_EQ(numberOfShares(assetInfo), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken); - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, 13, 13), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - fundaraisings.get(indexOfFundraising).soldAmount); + EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - fundaraisings.get(indexOfFundraising).soldAmount); } } void endEpochSucceedFundraisingChecker(id creator, uint32 indexOfFundraising, uint64 totalInvestedFund, uint64 originalCreatorBalance, uint64 assetName) @@ -1058,7 +1058,7 @@ TEST(TestContractNostromo, createFundraisingAndInvestInProjectAndClaimTokenCheck increaseEnergy(user, 180000000000); uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - if (ct = 4000) + if (ct >= 4000) { // Phase 2 Investment utcTime.Year = 2025; @@ -1411,7 +1411,7 @@ TEST(TestContractNostromo, createFundraisingAndInvestInProjectAndClaimTokenCheck increaseEnergy(user, 180000000000); uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - if (ct = 4000) + if (ct >= 4000) { // Phase 2 Investment @@ -1589,7 +1589,7 @@ TEST(TestContractNostromo, createFundraisingAndInvestInProjectAndClaimTokenCheck increaseEnergy(user, 180000000000); uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - if (ct = 4000) + if (ct >= 4000) { // Phase 2 Investment diff --git a/test/contract_qbond.cpp b/test/contract_qbond.cpp new file mode 100644 index 000000000..9eb9d0644 --- /dev/null +++ b/test/contract_qbond.cpp @@ -0,0 +1,444 @@ +#define NO_UEFI + +#include "contract_testing.h" + +std::string assetNameFromInt64(uint64 assetName); +std::string getCurrentMbondIndex(uint16_t epoch) +{ + if (epoch < QBOND_CYCLIC_START_EPOCH) + { + return std::to_string(epoch); + } + else + { + uint16_t index = (epoch - QBOND_CYCLIC_START_EPOCH + 1) % 53 == 0 ? 53 : (epoch - QBOND_CYCLIC_START_EPOCH + 1) % 53; + return index < 10 ? std::string("0").append(std::to_string(index)) : std::to_string(index); + } +} +const id adminAddress = ID(_B, _O, _N, _D, _A, _A, _F, _B, _U, _G, _H, _E, _L, _A, _N, _X, _G, _H, _N, _L, _M, _S, _U, _I, _V, _B, _K, _B, _H, _A, _Y, _E, _Q, _S, _Q, _B, _V, _P, _V, _N, _B, _H, _L, _F, _J, _I, _A, _Z, _F, _Q, _C, _W, _W, _B, _V, _E); +const id testAddress1 = ID(_H, _O, _G, _T, _K, _D, _N, _D, _V, _U, _U, _Z, _U, _F, _L, _A, _M, _L, _V, _B, _L, _Z, _D, _S, _G, _D, _D, _A, _E, _B, _E, _K, _K, _L, _N, _Z, _J, _B, _W, _S, _C, _A, _M, _D, _S, _X, _T, _C, _X, _A, _M, _A, _X, _U, _D, _F); +const id testAddress2 = ID(_E, _Q, _M, _B, _B, _V, _Y, _G, _Z, _O, _F, _U, _I, _H, _E, _X, _F, _O, _X, _K, _T, _F, _T, _A, _N, _E, _K, _B, _X, _L, _B, _X, _H, _A, _Y, _D, _F, _F, _M, _R, _E, _E, _M, _R, _Q, _E, _V, _A, _D, _Y, _M, _M, _E, _W, _A, _C); + +class QBondChecker : public QBOND +{ +public: + int64_t getCFAPopulation() + { + return _commissionFreeAddresses.population(); + } +}; + +class ContractTestingQBond : protected ContractTesting +{ +public: + ContractTestingQBond() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QBOND); + callSystemProcedure(QBOND_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QEARN); + callSystemProcedure(QEARN_CONTRACT_INDEX, INITIALIZE); + } + + QBondChecker* getState() + { + return (QBondChecker*)contractStates[QBOND_CONTRACT_INDEX]; + } + + void beginEpoch(bool expectSuccess = true) + { + callSystemProcedure(QBOND_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QBOND_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + void stake(const id& staker, const int64_t& quMillions, const int64_t& quAmount) + { + QBOND::Stake_input input{ quMillions }; + QBOND::Stake_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 1, input, output, staker, quAmount); + } + + QBOND::TransferMBondOwnershipAndPossession_output transfer(const id& from, const id& to, const uint16_t& epoch, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::TransferMBondOwnershipAndPossession_input input{ to, epoch, mbondsAmount }; + QBOND::TransferMBondOwnershipAndPossession_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 2, input, output, from, quAmount); + return output; + } + + QBOND::AddAskOrder_output addAskOrder(const id& asker, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::AddAskOrder_input input{ epoch, price, mbondsAmount }; + QBOND::AddAskOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 3, input, output, asker, quAmount); + return output; + } + + QBOND::RemoveAskOrder_output removeAskOrder(const id& asker, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::RemoveAskOrder_input input{ epoch, price, mbondsAmount }; + QBOND::RemoveAskOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 4, input, output, asker, quAmount); + return output; + } + + QBOND::AddBidOrder_output addBidOrder(const id& bider, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::AddBidOrder_input input{ epoch, price, mbondsAmount }; + QBOND::AddBidOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 5, input, output, bider, quAmount); + return output; + } + + QBOND::RemoveBidOrder_output removeBidOrder(const id& bider, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::RemoveBidOrder_input input{ epoch, price, mbondsAmount }; + QBOND::RemoveBidOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 6, input, output, bider, quAmount); + return output; + } + + QBOND::BurnQU_output burnQU(const id& invocator, const int64_t& quToBurn, const int64_t& quAmount) + { + QBOND::BurnQU_input input{ quToBurn }; + QBOND::BurnQU_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 7, input, output, invocator, quAmount); + return output; + } + + bool updateCFA(const id& invocator, const id& address, const bool operation) + { + QBOND::UpdateCFA_input input{ address, operation }; + QBOND::UpdateCFA_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 8, input, output, invocator, 0); + return output.result; + } + + QBOND::GetEarnedFees_output getEarnedFees() + { + QBOND::GetEarnedFees_input input; + QBOND::GetEarnedFees_output output; + callFunction(QBOND_CONTRACT_INDEX, 2, input, output); + return output; + } + + QBOND::GetInfoPerEpoch_output getInfoPerEpoch(const uint16_t& epoch) + { + QBOND::GetInfoPerEpoch_input input{ epoch }; + QBOND::GetInfoPerEpoch_output output; + callFunction(QBOND_CONTRACT_INDEX, 3, input, output); + return output; + } + + QBOND::GetOrders_output getOrders(const uint16_t& epoch, const int64_t& asksOffset, const int64_t& bidsOffset) + { + QBOND::GetOrders_input input{ epoch, asksOffset, bidsOffset }; + QBOND::GetOrders_output output; + callFunction(QBOND_CONTRACT_INDEX, 4, input, output); + return output; + } + + QBOND::GetUserOrders_output getUserOrders(const id& user, const int64_t& asksOffset, const int64_t& bidsOffset) + { + QBOND::GetUserOrders_input input{ user, asksOffset, bidsOffset }; + QBOND::GetUserOrders_output output; + callFunction(QBOND_CONTRACT_INDEX, 5, input, output); + return output; + } + + QBOND::GetMBondsTable_output getMBondsTable() + { + QBOND::GetMBondsTable_input input; + QBOND::GetMBondsTable_output output; + callFunction(QBOND_CONTRACT_INDEX, 6, input, output); + return output; + } + + QBOND::GetUserMBonds_output getUserMBonds(const id& user) + { + QBOND::GetUserMBonds_input input{ user }; + QBOND::GetUserMBonds_output output; + callFunction(QBOND_CONTRACT_INDEX, 7, input, output); + return output; + } + + QBOND::GetCFA_output getCFA() + { + QBOND::GetCFA_input input; + QBOND::GetCFA_output output; + callFunction(QBOND_CONTRACT_INDEX, 8, input, output); + return output; + } +}; + +TEST(ContractQBond, Stake) +{ + system.epoch = QBOND_CYCLIC_START_EPOCH; + ContractTestingQBond qbond; + qbond.beginEpoch(); + + increaseEnergy(testAddress1, 100000000LL); + increaseEnergy(testAddress2, 100000000LL); + + // scenario 1: testAddress1 want to stake 50 millions, but send to sc 30 millions + qbond.stake(testAddress1, 50, 30000000LL); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 2: testAddress1 want to stake 50 millions, but send to sc 50 millions (without commission) + qbond.stake(testAddress1, 50, 50000000LL); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 3: testAddress1 want to stake 50 millions and send full amount with commission + qbond.stake(testAddress1, 50, 50250000LL); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 50LL); + + // scenario 4.1: testAddress2 want to stake 5 millions, recieve 0 MBonds, because minimum is 10 and 5 were put in queue + qbond.stake(testAddress2, 5, 5025000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 4.2: testAddress1 want to stake 7 millions, testAddress1 recieve 7 MBonds and testAddress2 recieve 5 MBonds, because the total qu millions in the queue became more than 10 + qbond.stake(testAddress1, 7, 7035000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 57); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 5); +} + + +TEST(ContractQBond, TransferMBondOwnershipAndPossession) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + qbond.stake(testAddress1, 50, 50250000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 50); + + // scenario 1: not enough gas, 100 needed + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 10, 50).transferredMBonds, 0); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 2: enough gas, not enough mbonds + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 70, 100).transferredMBonds, 0); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 3: success + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 40, 100).transferredMBonds, 40); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 40); +} + +TEST(ContractQBond, AddRemoveAskOrder) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + qbond.stake(testAddress1, 50, 50250000); + + // scenario 1: not enough mbonds + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1500000, 100, 0).addedMBondsAmount, 0); + + // scenario 2: success to add ask, asked mbonds are blocked and cannot be transferred to another address + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1500000, 30, 0).addedMBondsAmount, 30); + // not enough free mbonds + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 21, 100).transferredMBonds, 0); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + // successful transfer + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 20, 100).transferredMBonds, 20); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 20); + + // scenario 3: no orders to remove at this price + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1400000, 30, 0).removedMBondsAmount, 0); + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1600000, 30, 0).removedMBondsAmount, 0); + + // scenario 4: no free mbonds, then successful removal ask order and transfer to another address + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 1, 100).transferredMBonds, 0); + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1500000, 5, 0).removedMBondsAmount, 5); + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 5, 100).transferredMBonds, 5); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 25); + + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1500000, 500, 0).removedMBondsAmount, 25); +} + +TEST(ContractQBond, AddRemoveBidOrder) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + increaseEnergy(testAddress2, 1000000000); + qbond.stake(testAddress1, 50, 50250000); + + // scenario 1: not enough qu + EXPECT_EQ(qbond.addBidOrder(testAddress2, system.epoch, 1500000, 10, 100).addedMBondsAmount, 0); + + // scenario 2: success to add bid + EXPECT_EQ(qbond.addBidOrder(testAddress2, system.epoch, 1500000, 10, 15000000).addedMBondsAmount, 10); + + // scenario 3: testAddress1 add ask order which matches the bid order + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1500000, 3, 0).addedMBondsAmount, 3); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 47); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(getCurrentMbondIndex(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 3); + + // scenario 3: no orders to remove at this price + EXPECT_EQ(qbond.removeBidOrder(testAddress2, system.epoch, 1400000, 30, 0).removedMBondsAmount, 0); + EXPECT_EQ(qbond.removeBidOrder(testAddress2, system.epoch, 1600000, 30, 0).removedMBondsAmount, 0); + + // scenario 4: successful removal bid order, qu are returned (7 mbonds per 1500000 each) + int64_t prevBalance = getBalance(testAddress2); + EXPECT_EQ(qbond.removeBidOrder(testAddress2, system.epoch, 1500000, 100, 0).removedMBondsAmount, 7); + EXPECT_EQ(getBalance(testAddress2) - prevBalance, 10500000); + + // check earned fees + auto fees = qbond.getEarnedFees(); + EXPECT_EQ(fees.stakeFees, 250000); + EXPECT_EQ(fees.tradeFees, 1350); // 1500000 (MBond price) * 3 (MBonds) * 0.0003 (0.03% fees for trade) + + // getOrders checks + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1600000, 5, 0).addedMBondsAmount, 5); + EXPECT_EQ(qbond.addAskOrder(testAddress2, system.epoch, 1500000, 3, 0).addedMBondsAmount, 3); + EXPECT_EQ(qbond.addBidOrder(testAddress1, system.epoch, 1400000, 10, 14000000).addedMBondsAmount, 10); + EXPECT_EQ(qbond.addBidOrder(testAddress2, system.epoch, 1300000, 5, 6500000).addedMBondsAmount, 5); + + // all orders sorted by price, therefore the element with index 0 contains an order with a price of 1500000 + auto orders = qbond.getOrders(system.epoch, 0, 0); + EXPECT_EQ(orders.askOrders.get(0).epoch, (sint64) QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(orders.askOrders.get(0).numberOfMBonds, 3); + EXPECT_EQ(orders.askOrders.get(0).owner, testAddress2); + EXPECT_EQ(orders.askOrders.get(0).price, 1500000); + + EXPECT_EQ(orders.bidOrders.get(0).epoch, (sint64) QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(orders.bidOrders.get(0).numberOfMBonds, 10); + EXPECT_EQ(orders.bidOrders.get(0).owner, testAddress1); + EXPECT_EQ(orders.bidOrders.get(0).price, 1400000); + + // with offset + orders = qbond.getOrders(system.epoch, 1, 1); + EXPECT_EQ(orders.askOrders.get(0).epoch, (sint64)QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(orders.askOrders.get(0).numberOfMBonds, 5); + EXPECT_EQ(orders.askOrders.get(0).owner, testAddress1); + EXPECT_EQ(orders.askOrders.get(0).price, 1600000); + + EXPECT_EQ(orders.bidOrders.get(0).epoch, (sint64)QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(orders.bidOrders.get(0).numberOfMBonds, 5); + EXPECT_EQ(orders.bidOrders.get(0).owner, testAddress2); + EXPECT_EQ(orders.bidOrders.get(0).price, 1300000); + + // user orders + auto userOrders = qbond.getUserOrders(testAddress1, 0, 0); + EXPECT_EQ(userOrders.askOrders.get(0).epoch, (sint64)QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(userOrders.askOrders.get(0).numberOfMBonds, 5); + EXPECT_EQ(userOrders.askOrders.get(0).owner, testAddress1); + EXPECT_EQ(userOrders.askOrders.get(0).price, 1600000); + + EXPECT_EQ(userOrders.bidOrders.get(0).epoch, (sint64)QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(userOrders.bidOrders.get(0).numberOfMBonds, 10); + EXPECT_EQ(userOrders.bidOrders.get(0).owner, testAddress1); + EXPECT_EQ(userOrders.bidOrders.get(0).price, 1400000); + + // with offset + userOrders = qbond.getUserOrders(testAddress1, 1, 1); + EXPECT_EQ(userOrders.askOrders.get(0).epoch, 0); + EXPECT_EQ(userOrders.askOrders.get(0).numberOfMBonds, 0); + EXPECT_EQ(userOrders.askOrders.get(0).owner, NULL_ID); + EXPECT_EQ(userOrders.askOrders.get(0).price, 0); + + EXPECT_EQ(userOrders.bidOrders.get(0).epoch, 0); + EXPECT_EQ(userOrders.bidOrders.get(0).numberOfMBonds, 0); + EXPECT_EQ(userOrders.bidOrders.get(0).owner, NULL_ID); + EXPECT_EQ(userOrders.bidOrders.get(0).price, 0); +} + +TEST(ContractQBond, BurnQu) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + + // scenario 1: not enough qu + EXPECT_EQ(qbond.burnQU(testAddress1, 1000000, 1000).amount, -1); + + // scenario 2: successful burning + EXPECT_EQ(qbond.burnQU(testAddress1, 1000000, 1000000).amount, 1000000); + + // scenario 3: successful burning, the surplus is returned + int64_t prevBalance = getBalance(testAddress1); + EXPECT_EQ(qbond.burnQU(testAddress1, 1000000, 10000000).amount, 1000000); + EXPECT_EQ(prevBalance - getBalance(testAddress1), 1000000); +} + +TEST(ContractQBond, UpdateCFA) +{ + ContractTestingQBond qbond; + increaseEnergy(testAddress1, 1000); + increaseEnergy(adminAddress, 1000); + + // only adminAddress can update CFA + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 1); + EXPECT_FALSE(qbond.updateCFA(testAddress1, testAddress2, 1)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 1); + EXPECT_TRUE(qbond.updateCFA(adminAddress, testAddress2, 1)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 2); + + auto cfa = qbond.getCFA(); + EXPECT_EQ(cfa.commissionFreeAddresses.get(0), testAddress2); + EXPECT_EQ(cfa.commissionFreeAddresses.get(1), adminAddress); + EXPECT_EQ(cfa.commissionFreeAddresses.get(2), NULL_ID); + + EXPECT_FALSE(qbond.updateCFA(testAddress1, testAddress2, 0)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 2); + EXPECT_TRUE(qbond.updateCFA(adminAddress, testAddress2, 0)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 1); +} + +TEST(ContractQBond, GetInfoPerEpoch) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + increaseEnergy(testAddress2, 1000000000); + + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 0); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 0); + + qbond.stake(testAddress1, 50, 50250000); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 1); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 50); + + qbond.stake(testAddress2, 100, 100500000); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 2); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 150); + + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 50, 100).transferredMBonds, 50); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 1); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 150); +} + +TEST(ContractQBond, GetMBondsTable) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + increaseEnergy(testAddress2, 1000000000); + + qbond.stake(testAddress1, 50, 50250000); + qbond.stake(testAddress2, 100, 100500000); + qbond.endEpoch(); + + system.epoch++; + qbond.beginEpoch(); + qbond.stake(testAddress1, 10, 10050000); + qbond.stake(testAddress2, 20, 20100000); + + auto table = qbond.getMBondsTable(); + EXPECT_EQ(table.info.get(0).epoch, (sint64)QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(table.info.get(1).epoch, (sint64)QBOND_CYCLIC_START_EPOCH + 1); + EXPECT_EQ(table.info.get(2).epoch, 0); + + auto userMBonds = qbond.getUserMBonds(testAddress1); + EXPECT_EQ(userMBonds.totalMBondsAmount, 60); + EXPECT_EQ(userMBonds.mbonds.get(0).epoch, (sint64)QBOND_CYCLIC_START_EPOCH); + EXPECT_EQ(userMBonds.mbonds.get(0).amount, 50); + EXPECT_EQ(userMBonds.mbonds.get(1).epoch, (sint64)QBOND_CYCLIC_START_EPOCH + 1); + EXPECT_EQ(userMBonds.mbonds.get(1).amount, 10); +} diff --git a/test/contract_qearn.cpp b/test/contract_qearn.cpp index 0fb474dbc..330814de3 100644 --- a/test/contract_qearn.cpp +++ b/test/contract_qearn.cpp @@ -154,6 +154,11 @@ class QearnChecker : public QEARN EXPECT_EQ(result.averageBurnedPercent, div(sumBurnedPercent, system.epoch - 138ULL)); EXPECT_EQ(result.averageRewardedPercent, div(sumRewardedPercent, system.epoch - 138ULL)); } + + QEARN::EpochIndexInfo getEpochIndex(uint32 epoch) const + { + return _epochIndex.get(epoch); + } }; class ContractTestingQearn : protected ContractTesting @@ -771,6 +776,101 @@ TEST(TestContractQearn, ErrorChecking) EXPECT_EQ(qearn.unlock(otherUser, QEARN_MINIMUM_LOCKING_AMOUNT, system.epoch), QEARN_UNLOCK_SUCCESS); } +// Test case for gap removal logic in overflow check (lines 635-656 in Qearn.h) +// This test verifies that when the locker array is near capacity and contains gaps, +// attempting to lock triggers gap removal, allowing the lock to succeed. +// Note: This test is disabled by default because it requires filling many slots (QEARN_MAX_LOCKS - 1) +// Enable with LARGE_SCALE_TEST >= 4 to run this comprehensive test + +#if LARGE_SCALE_TEST >= 4 +TEST(TestContractQearn, GapRemovalOnOverflow) +{ + std::cout << "gap removal test. If you want to test this case as soon, please set the QEARN_MAX_LOCKS to a smaller value on the contract." << std::endl; + ContractTestingQearn qearn; + + system.epoch = contractDescriptions[QEARN_CONTRACT_INDEX].constructionEpoch; + qearn.beginEpoch(); + qearn.endEpoch(); + + system.epoch = QEARN_INITIAL_EPOCH; + + qearn.beginEpoch(); + + // Create a scenario where we fill up the locker array and create gaps + // Strategy: Fill up to near capacity, unlock some to create gaps, + // then try to lock again which triggers gap removal + + const uint64 numGapsToCreate = 100; // Create some gaps by unlocking + // Fill up to QEARN_MAX_LOCKS - 1 so that after unlocking (which doesn't change endIndex), + // the next lock attempt will trigger the overflow check (endIndex >= QEARN_MAX_LOCKS - 1) + const uint64 targetEndIndex = QEARN_MAX_LOCKS - 1; + + std::vector usersToUnlock; + usersToUnlock.reserve(numGapsToCreate); + + // Step 1: Fill up the array to near capacity + // We'll fill up to targetEndIndex, then unlock some to create gaps + // The endIndex will stay high, so when we try to lock again, it will trigger overflow check + for (uint64 i = 0; i < targetEndIndex; ++i) + { + id testUser(i, 100, 200, 300); + uint64 amount = QEARN_MINIMUM_LOCKING_AMOUNT + 1; + increaseEnergy(testUser, amount); + EXPECT_TRUE(qearn.lockAndCheck(testUser, amount)); + + // Store some users to unlock later (to create gaps) + if (i < numGapsToCreate) + { + usersToUnlock.push_back(testUser); + } + } + + // Step 2: Verify we're near capacity + QearnChecker* state = qearn.getState(); + uint32 endIndexBeforeUnlock = state->getEpochIndex(system.epoch).endIndex; + EXPECT_GE(endIndexBeforeUnlock, targetEndIndex); + + // Step 3: Unlock some users to create gaps in the locker array + // Note: endIndex doesn't decrease when unlocking, so gaps are created but endIndex stays high + for (const auto& userToUnlock : usersToUnlock) + { + uint64 unlockAmount = QEARN_MINIMUM_LOCKING_AMOUNT + 1; + EXPECT_EQ(qearn.unlock(userToUnlock, unlockAmount, system.epoch), QEARN_UNLOCK_SUCCESS); + } + + // Step 4: Verify endIndex is still high (gaps created but not removed yet) + uint32 endIndexAfterUnlock = state->getEpochIndex(system.epoch).endIndex; + EXPECT_EQ(endIndexAfterUnlock, endIndexBeforeUnlock); // endIndex doesn't change on unlock + + // Step 5: Try to lock one more user - this should trigger overflow check and gap removal + // After gap removal, the lock should succeed because we created gaps earlier + id finalUser(targetEndIndex + 1, 100, 200, 300); + uint64 finalAmount = QEARN_MINIMUM_LOCKING_AMOUNT + 1; + increaseEnergy(finalUser, finalAmount); + + // The lock should succeed after gap removal + sint32 retCode = qearn.lock(finalUser, finalAmount); + + // Verify that gap removal happened and lock succeeded + // After gap removal, endIndex should be less than QEARN_MAX_LOCKS - 1 + uint32 endIndexAfterGapRemoval = state->getEpochIndex(system.epoch).endIndex; + + // The lock should succeed because gaps were removed + EXPECT_EQ(retCode, QEARN_LOCK_SUCCESS); + EXPECT_EQ(endIndexAfterGapRemoval, QEARN_MAX_LOCKS - numGapsToCreate); + EXPECT_LT(endIndexAfterGapRemoval, QEARN_MAX_LOCKS - 1); + EXPECT_LT(endIndexAfterGapRemoval, endIndexAfterUnlock); // endIndex should decrease after gap removal + + // Verify the locker array is consistent after gap removal + qearn.getState()->checkLockerArray(true, false); + + // Verify the final user's lock was successful + EXPECT_EQ(qearn.getUserLockedInfo(system.epoch, finalUser), finalAmount); + + qearn.endEpoch(); +} +#endif + void testRandomLockWithoutUnlock(const uint16 numEpochs, const unsigned int totalUsers, const unsigned int maxUserLocking) { std::cout << "random test without early unlock for " << numEpochs << " epochs with " << totalUsers << " total users and up to " << maxUserLocking << " lock calls per epoch" << std::endl; diff --git a/test/contract_qip.cpp b/test/contract_qip.cpp new file mode 100644 index 000000000..3a667c7c9 --- /dev/null +++ b/test/contract_qip.cpp @@ -0,0 +1,1436 @@ +#define NO_UEFI + +#include "contract_testing.h" + +static constexpr uint64 QIP_ISSUE_ASSET_FEE = 1000000000ull; +static constexpr uint64 QIP_TRANSFER_ASSET_FEE = 100ull; +static constexpr uint64 QIP_TRANSFER_RIGHTS_FEE = 100ull; + +static const id QIP_CONTRACT_ID(QIP_CONTRACT_INDEX, 0, 0, 0); + +const id QIP_testIssuer = ID(_T, _E, _S, _T, _I, _S, _S, _U, _E, _R, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T); +const id QIP_testAddress1 = ID(_A, _D, _D, _R, _A, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y); +const id QIP_testAddress2 = ID(_A, _D, _D, _R, _B, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y); +const id QIP_testAddress3 = ID(_A, _D, _D, _R, _C, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y); +const id QIP_testBuyer = ID(_B, _U, _Y, _E, _R, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y); + +class QIPChecker : public QIP +{ +public: + uint32 getNumberOfICO() const { return numberOfICO; } + + void checkICOInfo(const QIP::getICOInfo_output& output, const QIP::createICO_input& input, const id& creator) + { + EXPECT_EQ(output.creatorOfICO, creator); + EXPECT_EQ(output.issuer, input.issuer); + EXPECT_EQ(output.address1, input.address1); + EXPECT_EQ(output.address2, input.address2); + EXPECT_EQ(output.address3, input.address3); + EXPECT_EQ(output.address4, input.address4); + EXPECT_EQ(output.address5, input.address5); + EXPECT_EQ(output.address6, input.address6); + EXPECT_EQ(output.address7, input.address7); + EXPECT_EQ(output.address8, input.address8); + EXPECT_EQ(output.address9, input.address9); + EXPECT_EQ(output.address10, input.address10); + EXPECT_EQ(output.assetName, input.assetName); + EXPECT_EQ(output.price1, input.price1); + EXPECT_EQ(output.price2, input.price2); + EXPECT_EQ(output.price3, input.price3); + EXPECT_EQ(output.saleAmountForPhase1, input.saleAmountForPhase1); + EXPECT_EQ(output.saleAmountForPhase2, input.saleAmountForPhase2); + EXPECT_EQ(output.saleAmountForPhase3, input.saleAmountForPhase3); + EXPECT_EQ(output.remainingAmountForPhase1, input.saleAmountForPhase1); + EXPECT_EQ(output.remainingAmountForPhase2, input.saleAmountForPhase2); + EXPECT_EQ(output.remainingAmountForPhase3, input.saleAmountForPhase3); + EXPECT_EQ(output.percent1, input.percent1); + EXPECT_EQ(output.percent2, input.percent2); + EXPECT_EQ(output.percent3, input.percent3); + EXPECT_EQ(output.percent4, input.percent4); + EXPECT_EQ(output.percent5, input.percent5); + EXPECT_EQ(output.percent6, input.percent6); + EXPECT_EQ(output.percent7, input.percent7); + EXPECT_EQ(output.percent8, input.percent8); + EXPECT_EQ(output.percent9, input.percent9); + EXPECT_EQ(output.percent10, input.percent10); + EXPECT_EQ(output.startEpoch, input.startEpoch); + } +}; + +class ContractTestingQIP : protected ContractTesting +{ +public: + ContractTestingQIP() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QIP); + callSystemProcedure(QIP_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + } + + QIPChecker* getState() + { + return (QIPChecker*)contractStates[QIP_CONTRACT_INDEX]; + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QIP_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares) + { + QX::IssueAsset_input input; + input.assetName = assetName; + input.numberOfShares = numberOfShares; + input.unitOfMeasurement = 0; + input.numberOfDecimalPlaces = 0; + QX::IssueAsset_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QIP_ISSUE_ASSET_FEE); + return output.issuedNumberOfShares; + } + + sint64 transferAsset(const id& from, const id& to, uint64 assetName, const id& issuer, sint64 numberOfShares) + { + QX::TransferShareOwnershipAndPossession_input input; + input.assetName = assetName; + input.issuer = issuer; + input.newOwnerAndPossessor = to; + input.numberOfShares = numberOfShares; + QX::TransferShareOwnershipAndPossession_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, from, QIP_TRANSFER_ASSET_FEE); + return output.transferredNumberOfShares; + } + + QIP::createICO_output createICO(const id& creator, const QIP::createICO_input& input) + { + QIP::createICO_output output; + invokeUserProcedure(QIP_CONTRACT_INDEX, 1, input, output, creator, 0); + return output; + } + + QIP::buyToken_output buyToken(const id& buyer, uint32 indexOfICO, uint64 amount, sint64 invocationReward) + { + QIP::buyToken_input input; + input.indexOfICO = indexOfICO; + input.amount = amount; + QIP::buyToken_output output; + invokeUserProcedure(QIP_CONTRACT_INDEX, 2, input, output, buyer, invocationReward); + return output; + } + + QIP::getICOInfo_output getICOInfo(uint32 indexOfICO) + { + QIP::getICOInfo_input input; + input.indexOfICO = indexOfICO; + QIP::getICOInfo_output output; + callFunction(QIP_CONTRACT_INDEX, 1, input, output); + return output; + } + + sint64 transferShareManagementRightsQX(const id& invocator, const Asset& asset, sint64 numberOfShares, uint32 newManagingContractIndex, sint64 fee) + { + QX::TransferShareManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = newManagingContractIndex; + QX::TransferShareManagementRights_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, invocator, fee); + return output.transferredNumberOfShares; + } + + sint64 transferShareManagementRights(const id& invocator, const Asset& asset, sint64 numberOfShares, uint32 newManagingContractIndex, sint64 invocationReward) + { + QIP::TransferShareManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = newManagingContractIndex; + QIP::TransferShareManagementRights_output output; + invokeUserProcedure(QIP_CONTRACT_INDEX, 3, input, output, invocator, invocationReward); + return output.transferredNumberOfShares; + } +}; + +TEST(ContractQIP, createICO_Success) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + // Issue asset and transfer to creator + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + // Prepare ICO input + QIP::createICO_input input; + input.issuer = issuer; + input.address1 = QIP_testAddress1; + input.address2 = QIP_testAddress2; + input.address3 = QIP_testAddress3; + input.address4 = QIP_testAddress1; + input.address5 = QIP_testAddress2; + input.address6 = QIP_testAddress3; + input.address7 = QIP_testAddress1; + input.address8 = QIP_testAddress2; + input.address9 = QIP_testAddress3; + input.address10 = QIP_testAddress1; + input.assetName = assetName; + input.price1 = 100; + input.price2 = 200; + input.price3 = 300; + input.saleAmountForPhase1 = 300000; + input.saleAmountForPhase2 = 300000; + input.saleAmountForPhase3 = 400000; + input.percent1 = 10; + input.percent2 = 10; + input.percent3 = 10; + input.percent4 = 10; + input.percent5 = 10; + input.percent6 = 10; + input.percent7 = 10; + input.percent8 = 10; + input.percent9 = 10; + input.percent10 = 5; + input.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output output = QIP.createICO(creator, input); + EXPECT_EQ(output.returnCode, QIPLogInfo::QIP_success); + + // Check ICO info + QIP::getICOInfo_output icoInfo = QIP.getICOInfo(0); + QIP.getState()->checkICOInfo(icoInfo, input, creator); + + // Verify shares were transferred to contract + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, creator, creator, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 0); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, QIP_CONTRACT_ID, QIP_CONTRACT_ID, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), totalShares); +} + +TEST(ContractQIP, createICO_InvalidStartEpoch) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input input; + input.issuer = issuer; + input.address1 = QIP_testAddress1; + input.address2 = QIP_testAddress2; + input.address3 = QIP_testAddress3; + input.address4 = QIP_testAddress1; + input.address5 = QIP_testAddress2; + input.address6 = QIP_testAddress3; + input.address7 = QIP_testAddress1; + input.address8 = QIP_testAddress2; + input.address9 = QIP_testAddress3; + input.address10 = QIP_testAddress1; + input.assetName = assetName; + input.price1 = 100; + input.price2 = 200; + input.price3 = 300; + input.saleAmountForPhase1 = 300000; + input.saleAmountForPhase2 = 300000; + input.saleAmountForPhase3 = 400000; + input.percent1 = 10; + input.percent2 = 10; + input.percent3 = 10; + input.percent4 = 10; + input.percent5 = 10; + input.percent6 = 10; + input.percent7 = 10; + input.percent8 = 10; + input.percent9 = 10; + input.percent10 = 5; + + // Test with startEpoch <= current epoch + 1 + input.startEpoch = system.epoch; + increaseEnergy(creator, 1); + QIP::createICO_output output1 = QIP.createICO(creator, input); + EXPECT_EQ(output1.returnCode, QIPLogInfo::QIP_invalidStartEpoch); + + input.startEpoch = system.epoch + 1; + QIP::createICO_output output2 = QIP.createICO(creator, input); + EXPECT_EQ(output2.returnCode, QIPLogInfo::QIP_invalidStartEpoch); +} + +TEST(ContractQIP, createICO_InvalidSaleAmount) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input input; + input.issuer = issuer; + input.address1 = QIP_testAddress1; + input.address2 = QIP_testAddress2; + input.address3 = QIP_testAddress3; + input.address4 = QIP_testAddress1; + input.address5 = QIP_testAddress2; + input.address6 = QIP_testAddress3; + input.address7 = QIP_testAddress1; + input.address8 = QIP_testAddress2; + input.address9 = QIP_testAddress3; + input.address10 = QIP_testAddress1; + input.assetName = assetName; + input.price1 = 100; + input.price2 = 200; + input.price3 = 300; + input.saleAmountForPhase1 = 300000; + input.saleAmountForPhase2 = 300000; + input.saleAmountForPhase3 = 400001; // Total doesn't match + input.percent1 = 10; + input.percent2 = 10; + input.percent3 = 10; + input.percent4 = 10; + input.percent5 = 10; + input.percent6 = 10; + input.percent7 = 10; + input.percent8 = 10; + input.percent9 = 10; + input.percent10 = 5; + input.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output output = QIP.createICO(creator, input); + EXPECT_EQ(output.returnCode, QIPLogInfo::QIP_invalidSaleAmount); +} + +TEST(ContractQIP, createICO_InvalidPrice) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input input; + input.issuer = issuer; + input.address1 = QIP_testAddress1; + input.address2 = QIP_testAddress2; + input.address3 = QIP_testAddress3; + input.address4 = QIP_testAddress1; + input.address5 = QIP_testAddress2; + input.address6 = QIP_testAddress3; + input.address7 = QIP_testAddress1; + input.address8 = QIP_testAddress2; + input.address9 = QIP_testAddress3; + input.address10 = QIP_testAddress1; + input.assetName = assetName; + input.price1 = 0; // Invalid price + input.price2 = 200; + input.price3 = 300; + input.saleAmountForPhase1 = 300000; + input.saleAmountForPhase2 = 300000; + input.saleAmountForPhase3 = 400000; + input.percent1 = 10; + input.percent2 = 10; + input.percent3 = 10; + input.percent4 = 10; + input.percent5 = 10; + input.percent6 = 10; + input.percent7 = 10; + input.percent8 = 10; + input.percent9 = 10; + input.percent10 = 5; + input.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output output = QIP.createICO(creator, input); + EXPECT_EQ(output.returnCode, QIPLogInfo::QIP_invalidPrice); +} + +TEST(ContractQIP, createICO_InvalidPercent) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input input; + input.issuer = issuer; + input.address1 = QIP_testAddress1; + input.address2 = QIP_testAddress2; + input.address3 = QIP_testAddress3; + input.address4 = QIP_testAddress1; + input.address5 = QIP_testAddress2; + input.address6 = QIP_testAddress3; + input.address7 = QIP_testAddress1; + input.address8 = QIP_testAddress2; + input.address9 = QIP_testAddress3; + input.address10 = QIP_testAddress1; + input.assetName = assetName; + input.price1 = 100; + input.price2 = 200; + input.price3 = 300; + input.saleAmountForPhase1 = 300000; + input.saleAmountForPhase2 = 300000; + input.saleAmountForPhase3 = 400000; + input.percent1 = 10; + input.percent2 = 10; + input.percent3 = 10; + input.percent4 = 10; + input.percent5 = 10; + input.percent6 = 10; + input.percent7 = 10; + input.percent8 = 10; + input.percent9 = 5; + input.percent10 = 1; // Total is 96, should be 95 + input.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output output = QIP.createICO(creator, input); + EXPECT_EQ(output.returnCode, QIPLogInfo::QIP_invalidPercent); +} + +TEST(ContractQIP, buyToken_Phase1) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Advance to start epoch + ++system.epoch; + ++system.epoch; + + id buyer = QIP_testBuyer; + uint64 buyAmount = 10000; + uint64 price = createInput.price1; + sint64 requiredReward = buyAmount * price; + + increaseEnergy(buyer, requiredReward); + increaseEnergy(QIP_testAddress1, 1); + increaseEnergy(QIP_testAddress2, 1); + increaseEnergy(QIP_testAddress3, 1); + + // Record balances before purchase for all addresses + sint64 balanceBefore1 = getBalance(QIP_testAddress1); + sint64 balanceBefore2 = getBalance(QIP_testAddress2); + sint64 balanceBefore3 = getBalance(QIP_testAddress3); + sint64 contractBalanceBefore = getBalance(QIP_CONTRACT_ID); + + QIP::buyToken_output buyOutput = QIP.buyToken(buyer, 0, buyAmount, requiredReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_success); + + // Verify buyer received the shares + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer, buyer, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), buyAmount); + + // Check remaining amounts + QIP::getICOInfo_output icoInfo = QIP.getICOInfo(0); + EXPECT_EQ(icoInfo.remainingAmountForPhase1, createInput.saleAmountForPhase1 - buyAmount); + EXPECT_EQ(icoInfo.remainingAmountForPhase2, createInput.saleAmountForPhase2); + EXPECT_EQ(icoInfo.remainingAmountForPhase3, createInput.saleAmountForPhase3); + + // Calculate expected distributions for all 10 addresses + sint64 totalPayment = buyAmount * price; + uint64 expectedDist1 = div(totalPayment * createInput.percent1 * 1ULL, 100ULL); + uint64 expectedDist2 = div(totalPayment * createInput.percent2 * 1ULL, 100ULL); + uint64 expectedDist3 = div(totalPayment * createInput.percent3 * 1ULL, 100ULL); + uint64 expectedDist4 = div(totalPayment * createInput.percent4 * 1ULL, 100ULL); + uint64 expectedDist5 = div(totalPayment * createInput.percent5 * 1ULL, 100ULL); + uint64 expectedDist6 = div(totalPayment * createInput.percent6 * 1ULL, 100ULL); + uint64 expectedDist7 = div(totalPayment * createInput.percent7 * 1ULL, 100ULL); + uint64 expectedDist8 = div(totalPayment * createInput.percent8 * 1ULL, 100ULL); + uint64 expectedDist9 = div(totalPayment * createInput.percent9 * 1ULL, 100ULL); + uint64 expectedDist10 = div(totalPayment * createInput.percent10 * 1ULL, 100ULL); + + // Calculate total distributed to addresses (should be 95% of total payment) + sint64 totalDistributedToAddresses = expectedDist1 + expectedDist2 + expectedDist3 + expectedDist4 + expectedDist5 + + expectedDist6 + expectedDist7 + expectedDist8 + expectedDist9 + expectedDist10; + + // Calculate expected dividend amount (remaining 5% divided by 676) + sint64 remainingForDividends = totalPayment - totalDistributedToAddresses; + uint64 expectedDividendAmount = div(remainingForDividends * 1ULL, 676ULL) * 676; + + // Verify all addresses received correct amounts + // Note: addresses 1, 4, 7, 10 map to QIP_testAddress1 + // addresses 2, 5, 8 map to QIP_testAddress2 + // addresses 3, 6, 9 map to QIP_testAddress3 + sint64 expectedForAddress1 = expectedDist1 + expectedDist4 + expectedDist7 + expectedDist10; + sint64 expectedForAddress2 = expectedDist2 + expectedDist5 + expectedDist8; + sint64 expectedForAddress3 = expectedDist3 + expectedDist6 + expectedDist9; + + EXPECT_EQ(getBalance(QIP_testAddress1), balanceBefore1 + expectedForAddress1); + EXPECT_EQ(getBalance(QIP_testAddress2), balanceBefore2 + expectedForAddress2); + EXPECT_EQ(getBalance(QIP_testAddress3), balanceBefore3 + expectedForAddress3); + + // Verify contract balance decreased by total payment (minus any refund to buyer) + sint64 contractBalanceAfter = getBalance(QIP_CONTRACT_ID); + sint64 contractBalanceChange = contractBalanceAfter - contractBalanceBefore; + // Contract should have received the payment and distributed it, so balance should increase by fee minus distributions + // But since we're transferring from contract to addresses, the contract balance should decrease + // Actually, the contract receives the invocation reward, then transfers to addresses + // So the contract balance should be: initial + requiredReward - totalDistributedToAddresses - expectedDividendAmount + sint64 expectedContractBalanceChange = requiredReward - totalDistributedToAddresses - expectedDividendAmount; + EXPECT_EQ(contractBalanceChange, expectedContractBalanceChange); +} + +TEST(ContractQIP, buyToken_Phase2) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Advance to Phase 2 (startEpoch + 1) + ++system.epoch; + ++system.epoch; + ++system.epoch; // Now at startEpoch + 1 + + id buyer = QIP_testBuyer; + uint64 buyAmount = 10000; + uint64 price = createInput.price2; + sint64 requiredReward = buyAmount * price; + + increaseEnergy(buyer, requiredReward); + increaseEnergy(QIP_testAddress1, 1); + increaseEnergy(QIP_testAddress2, 1); + increaseEnergy(QIP_testAddress3, 1); + + // Record balances before purchase + sint64 balanceBefore1 = getBalance(QIP_testAddress1); + sint64 balanceBefore2 = getBalance(QIP_testAddress2); + sint64 balanceBefore3 = getBalance(QIP_testAddress3); + sint64 contractBalanceBefore = getBalance(QIP_CONTRACT_ID); + + QIP::buyToken_output buyOutput = QIP.buyToken(buyer, 0, buyAmount, requiredReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_success); + + // Verify buyer received the shares + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer, buyer, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), buyAmount); + + // Check remaining amounts + QIP::getICOInfo_output icoInfo = QIP.getICOInfo(0); + EXPECT_EQ(icoInfo.remainingAmountForPhase1, createInput.saleAmountForPhase1); + EXPECT_EQ(icoInfo.remainingAmountForPhase2, createInput.saleAmountForPhase2 - buyAmount); + EXPECT_EQ(icoInfo.remainingAmountForPhase3, createInput.saleAmountForPhase3); + + // Verify fee distribution for all addresses + sint64 totalPayment = buyAmount * price; + uint64 expectedDist1 = div(totalPayment * createInput.percent1 * 1ULL, 100ULL); + uint64 expectedDist2 = div(totalPayment * createInput.percent2 * 1ULL, 100ULL); + uint64 expectedDist3 = div(totalPayment * createInput.percent3 * 1ULL, 100ULL); + uint64 expectedDist4 = div(totalPayment * createInput.percent4 * 1ULL, 100ULL); + uint64 expectedDist5 = div(totalPayment * createInput.percent5 * 1ULL, 100ULL); + uint64 expectedDist6 = div(totalPayment * createInput.percent6 * 1ULL, 100ULL); + uint64 expectedDist7 = div(totalPayment * createInput.percent7 * 1ULL, 100ULL); + uint64 expectedDist8 = div(totalPayment * createInput.percent8 * 1ULL, 100ULL); + uint64 expectedDist9 = div(totalPayment * createInput.percent9 * 1ULL, 100ULL); + uint64 expectedDist10 = div(totalPayment * createInput.percent10 * 1ULL, 100ULL); + + sint64 totalDistributedToAddresses = expectedDist1 + expectedDist2 + expectedDist3 + expectedDist4 + expectedDist5 + + expectedDist6 + expectedDist7 + expectedDist8 + expectedDist9 + expectedDist10; + sint64 remainingForDividends = totalPayment - totalDistributedToAddresses; + uint64 expectedDividendAmount = div(remainingForDividends * 1ULL, 676ULL) * 676; + + sint64 expectedForAddress1 = expectedDist1 + expectedDist4 + expectedDist7 + expectedDist10; + sint64 expectedForAddress2 = expectedDist2 + expectedDist5 + expectedDist8; + sint64 expectedForAddress3 = expectedDist3 + expectedDist6 + expectedDist9; + + EXPECT_EQ(getBalance(QIP_testAddress1), balanceBefore1 + expectedForAddress1); + EXPECT_EQ(getBalance(QIP_testAddress2), balanceBefore2 + expectedForAddress2); + EXPECT_EQ(getBalance(QIP_testAddress3), balanceBefore3 + expectedForAddress3); + + sint64 contractBalanceAfter = getBalance(QIP_CONTRACT_ID); + sint64 contractBalanceChange = contractBalanceAfter - contractBalanceBefore; + sint64 expectedContractBalanceChange = requiredReward - totalDistributedToAddresses - expectedDividendAmount; + EXPECT_EQ(contractBalanceChange, expectedContractBalanceChange); +} + +TEST(ContractQIP, buyToken_Phase3) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Advance to Phase 3 (startEpoch + 2) + ++system.epoch; + ++system.epoch; + ++system.epoch; // Now at startEpoch + 1 + ++system.epoch; // Now at startEpoch + 2 + + id buyer = QIP_testBuyer; + uint64 buyAmount = 10000; + uint64 price = createInput.price3; + sint64 requiredReward = buyAmount * price; + + increaseEnergy(buyer, requiredReward); + increaseEnergy(QIP_testAddress1, 1); + increaseEnergy(QIP_testAddress2, 1); + increaseEnergy(QIP_testAddress3, 1); + + // Record balances before purchase + sint64 balanceBefore1 = getBalance(QIP_testAddress1); + sint64 balanceBefore2 = getBalance(QIP_testAddress2); + sint64 balanceBefore3 = getBalance(QIP_testAddress3); + sint64 contractBalanceBefore = getBalance(QIP_CONTRACT_ID); + + QIP::buyToken_output buyOutput = QIP.buyToken(buyer, 0, buyAmount, requiredReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_success); + + // Verify buyer received the shares + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer, buyer, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), buyAmount); + + // Check remaining amounts + QIP::getICOInfo_output icoInfo = QIP.getICOInfo(0); + EXPECT_EQ(icoInfo.remainingAmountForPhase1, createInput.saleAmountForPhase1); + EXPECT_EQ(icoInfo.remainingAmountForPhase2, createInput.saleAmountForPhase2); + EXPECT_EQ(icoInfo.remainingAmountForPhase3, createInput.saleAmountForPhase3 - buyAmount); + + // Verify fee distribution for all addresses + sint64 totalPayment = buyAmount * price; + uint64 expectedDist1 = div(totalPayment * createInput.percent1 * 1ULL, 100ULL); + uint64 expectedDist2 = div(totalPayment * createInput.percent2 * 1ULL, 100ULL); + uint64 expectedDist3 = div(totalPayment * createInput.percent3 * 1ULL, 100ULL); + uint64 expectedDist4 = div(totalPayment * createInput.percent4 * 1ULL, 100ULL); + uint64 expectedDist5 = div(totalPayment * createInput.percent5 * 1ULL, 100ULL); + uint64 expectedDist6 = div(totalPayment * createInput.percent6 * 1ULL, 100ULL); + uint64 expectedDist7 = div(totalPayment * createInput.percent7 * 1ULL, 100ULL); + uint64 expectedDist8 = div(totalPayment * createInput.percent8 * 1ULL, 100ULL); + uint64 expectedDist9 = div(totalPayment * createInput.percent9 * 1ULL, 100ULL); + uint64 expectedDist10 = div(totalPayment * createInput.percent10 * 1ULL, 100ULL); + + sint64 totalDistributedToAddresses = expectedDist1 + expectedDist2 + expectedDist3 + expectedDist4 + expectedDist5 + + expectedDist6 + expectedDist7 + expectedDist8 + expectedDist9 + expectedDist10; + sint64 remainingForDividends = totalPayment - totalDistributedToAddresses; + uint64 expectedDividendAmount = div(remainingForDividends * 1ULL, 676ULL) * 676; + + sint64 expectedForAddress1 = expectedDist1 + expectedDist4 + expectedDist7 + expectedDist10; + sint64 expectedForAddress2 = expectedDist2 + expectedDist5 + expectedDist8; + sint64 expectedForAddress3 = expectedDist3 + expectedDist6 + expectedDist9; + + EXPECT_EQ(getBalance(QIP_testAddress1), balanceBefore1 + expectedForAddress1); + EXPECT_EQ(getBalance(QIP_testAddress2), balanceBefore2 + expectedForAddress2); + EXPECT_EQ(getBalance(QIP_testAddress3), balanceBefore3 + expectedForAddress3); + + sint64 contractBalanceAfter = getBalance(QIP_CONTRACT_ID); + sint64 contractBalanceChange = contractBalanceAfter - contractBalanceBefore; + sint64 expectedContractBalanceChange = requiredReward - totalDistributedToAddresses - expectedDividendAmount; + EXPECT_EQ(contractBalanceChange, expectedContractBalanceChange); +} + +TEST(ContractQIP, buyToken_InvalidEpoch) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Try to buy before start epoch + id buyer = QIP_testBuyer; + uint64 buyAmount = 10000; + uint64 price = createInput.price1; + sint64 requiredReward = buyAmount * price; + + increaseEnergy(buyer, requiredReward); + QIP::buyToken_output buyOutput = QIP.buyToken(buyer, 0, buyAmount, requiredReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_invalidEpoch); +} + +TEST(ContractQIP, buyToken_InvalidAmount) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Advance to start epoch + ++system.epoch; + ++system.epoch; + + id buyer = QIP_testBuyer; + uint64 buyAmount = 300001; // More than remaining + uint64 price = createInput.price1; + sint64 requiredReward = buyAmount * price; + + increaseEnergy(buyer, requiredReward); + QIP::buyToken_output buyOutput = QIP.buyToken(buyer, 0, buyAmount, requiredReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_invalidAmount); +} + +TEST(ContractQIP, buyToken_ICONotFound) +{ + ContractTestingQIP QIP; + + id buyer = QIP_testBuyer; + uint64 buyAmount = 10000; + sint64 requiredReward = buyAmount * 100; + + increaseEnergy(buyer, requiredReward); + QIP::buyToken_output buyOutput = QIP.buyToken(buyer, 999, buyAmount, requiredReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_ICONotFound); +} + +TEST(ContractQIP, buyToken_InsufficientInvocationReward) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Advance to start epoch + ++system.epoch; + ++system.epoch; + + id buyer = QIP_testBuyer; + uint64 buyAmount = 10000; + uint64 price = createInput.price1; + sint64 requiredReward = buyAmount * price; + sint64 insufficientReward = requiredReward - 1; + + increaseEnergy(buyer, insufficientReward); + QIP::buyToken_output buyOutput = QIP.buyToken(buyer, 0, buyAmount, insufficientReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_insufficientInvocationReward); +} + +TEST(ContractQIP, END_EPOCH_Phase1Rollover) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Check initial state + QIP::getICOInfo_output icoInfo = QIP.getICOInfo(0); + uint64 initialPhase1 = icoInfo.remainingAmountForPhase1; + uint64 initialPhase2 = icoInfo.remainingAmountForPhase2; + + // Advance to startEpoch (Phase 1 ends) + ++system.epoch; // epoch = startEpoch - 1 + ++system.epoch; // epoch = startEpoch + + // End epoch should rollover Phase 1 remaining to Phase 2 + QIP.endEpoch(); + + // Check that Phase 1 remaining was set to 0 + icoInfo = QIP.getICOInfo(0); + EXPECT_EQ(icoInfo.remainingAmountForPhase1, 0); + EXPECT_EQ(icoInfo.remainingAmountForPhase2, initialPhase2 + initialPhase1); +} + +TEST(ContractQIP, END_EPOCH_Phase2Rollover) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Check initial state + QIP::getICOInfo_output icoInfo = QIP.getICOInfo(0); + uint64 initialPhase2 = icoInfo.remainingAmountForPhase2; + uint64 initialPhase3 = icoInfo.remainingAmountForPhase3; + + // Advance to startEpoch + 1 (Phase 2 ends) + ++system.epoch; // epoch = startEpoch - 1 + ++system.epoch; // epoch = startEpoch + ++system.epoch; // epoch = startEpoch + 1 + + // End epoch should rollover Phase 2 remaining to Phase 3 + QIP.endEpoch(); + + // Check that Phase 2 remaining was set to 0 + icoInfo = QIP.getICOInfo(0); + EXPECT_EQ(icoInfo.remainingAmountForPhase2, 0); + EXPECT_EQ(icoInfo.remainingAmountForPhase3, initialPhase3 + initialPhase2); +} + +TEST(ContractQIP, END_EPOCH_Phase3ReturnToCreator) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Check initial state - verify shares are in contract + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, QIP_CONTRACT_ID, QIP_CONTRACT_ID, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), totalShares); + QIP::getICOInfo_output icoInfo = QIP.getICOInfo(0); + uint64 remainingPhase3 = icoInfo.remainingAmountForPhase3; + sint64 creatorSharesBefore = numberOfPossessedShares(assetName, issuer, creator, creator, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX); + + // Advance to startEpoch + 2 (Phase 3 ends) + ++system.epoch; // epoch = startEpoch - 1 + ++system.epoch; // epoch = startEpoch + ++system.epoch; // epoch = startEpoch + 1 + ++system.epoch; // epoch = startEpoch + 2 + + // End epoch should return Phase 3 remaining to creator and remove ICO + QIP.endEpoch(); + + // Verify shares were returned to creator + sint64 creatorSharesAfter = numberOfPossessedShares(assetName, issuer, creator, creator, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX); + EXPECT_EQ(creatorSharesAfter, creatorSharesBefore + remainingPhase3); + + // Verify ICO was removed + QIPChecker* state = QIP.getState(); + EXPECT_EQ(state->getNumberOfICO(), 0); + + // Verify contract no longer has the returned shares + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, QIP_CONTRACT_ID, QIP_CONTRACT_ID, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), totalShares - remainingPhase3); +} + +TEST(ContractQIP, TransferShareManagementRights) +{ + ContractTestingQIP QIP; + + id issuer = QIP_testIssuer; + uint64 assetName = assetNameFromString("ICOASS"); + sint64 totalShares = 1000000; + + increaseEnergy(issuer, QIP_ISSUE_ASSET_FEE); + EXPECT_EQ(QIP.issueAsset(issuer, assetName, totalShares), totalShares); + + id creator = QIP_testBuyer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + increaseEnergy(issuer, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferAsset(issuer, creator, assetName, issuer, totalShares), totalShares); + + // Transfer management rights to QIP contract + Asset asset; + asset.assetName = assetName; + asset.issuer = issuer; + increaseEnergy(creator, QIP_TRANSFER_ASSET_FEE); + EXPECT_EQ(QIP.transferShareManagementRightsQX(creator, asset, totalShares, QIP_CONTRACT_INDEX, QIP_TRANSFER_ASSET_FEE), totalShares); + + // Transfer shares to QIP contract + QIP::createICO_input createInput; + createInput.issuer = issuer; + createInput.address1 = QIP_testAddress1; + createInput.address2 = QIP_testAddress2; + createInput.address3 = QIP_testAddress3; + createInput.address4 = QIP_testAddress1; + createInput.address5 = QIP_testAddress2; + createInput.address6 = QIP_testAddress3; + createInput.address7 = QIP_testAddress1; + createInput.address8 = QIP_testAddress2; + createInput.address9 = QIP_testAddress3; + createInput.address10 = QIP_testAddress1; + createInput.assetName = assetName; + createInput.price1 = 100; + createInput.price2 = 200; + createInput.price3 = 300; + createInput.saleAmountForPhase1 = 300000; + createInput.saleAmountForPhase2 = 300000; + createInput.saleAmountForPhase3 = 400000; + createInput.percent1 = 10; + createInput.percent2 = 10; + createInput.percent3 = 10; + createInput.percent4 = 10; + createInput.percent5 = 10; + createInput.percent6 = 10; + createInput.percent7 = 10; + createInput.percent8 = 10; + createInput.percent9 = 10; + createInput.percent10 = 5; + createInput.startEpoch = system.epoch + 2; + + increaseEnergy(creator, 1); + QIP::createICO_output createOutput = QIP.createICO(creator, createInput); + EXPECT_EQ(createOutput.returnCode, QIPLogInfo::QIP_success); + + // Verify shares are in QIP contract + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, QIP_CONTRACT_ID, QIP_CONTRACT_ID, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), totalShares); + + system.epoch += 2; + // buy token + uint64 buyAmount = 100000; + uint64 price = createInput.price1; + sint64 requiredReward = buyAmount * price; + + increaseEnergy(creator, requiredReward); + increaseEnergy(QIP_testAddress1, 1); + increaseEnergy(QIP_testAddress2, 1); + increaseEnergy(QIP_testAddress3, 1); + + // Record balances before purchase + sint64 balanceBefore1 = getBalance(QIP_testAddress1); + sint64 balanceBefore2 = getBalance(QIP_testAddress2); + sint64 balanceBefore3 = getBalance(QIP_testAddress3); + sint64 contractBalanceBefore = getBalance(QIP_CONTRACT_ID); + + QIP::buyToken_output buyOutput = QIP.buyToken(creator, 0, buyAmount, requiredReward); + EXPECT_EQ(buyOutput.returnCode, QIPLogInfo::QIP_success); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, creator, creator, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), buyAmount); + + // Verify fee distribution for all addresses + sint64 totalPayment = buyAmount * price; + uint64 expectedDist1 = div(totalPayment * createInput.percent1 * 1ULL, 100ULL); + uint64 expectedDist2 = div(totalPayment * createInput.percent2 * 1ULL, 100ULL); + uint64 expectedDist3 = div(totalPayment * createInput.percent3 * 1ULL, 100ULL); + uint64 expectedDist4 = div(totalPayment * createInput.percent4 * 1ULL, 100ULL); + uint64 expectedDist5 = div(totalPayment * createInput.percent5 * 1ULL, 100ULL); + uint64 expectedDist6 = div(totalPayment * createInput.percent6 * 1ULL, 100ULL); + uint64 expectedDist7 = div(totalPayment * createInput.percent7 * 1ULL, 100ULL); + uint64 expectedDist8 = div(totalPayment * createInput.percent8 * 1ULL, 100ULL); + uint64 expectedDist9 = div(totalPayment * createInput.percent9 * 1ULL, 100ULL); + uint64 expectedDist10 = div(totalPayment * createInput.percent10 * 1ULL, 100ULL); + + sint64 totalDistributedToAddresses = expectedDist1 + expectedDist2 + expectedDist3 + expectedDist4 + expectedDist5 + + expectedDist6 + expectedDist7 + expectedDist8 + expectedDist9 + expectedDist10; + sint64 remainingForDividends = totalPayment - totalDistributedToAddresses; + uint64 expectedDividendAmount = div(remainingForDividends * 1ULL, 676ULL) * 676; + + sint64 expectedForAddress1 = expectedDist1 + expectedDist4 + expectedDist7 + expectedDist10; + sint64 expectedForAddress2 = expectedDist2 + expectedDist5 + expectedDist8; + sint64 expectedForAddress3 = expectedDist3 + expectedDist6 + expectedDist9; + + EXPECT_EQ(getBalance(QIP_testAddress1), balanceBefore1 + expectedForAddress1); + EXPECT_EQ(getBalance(QIP_testAddress2), balanceBefore2 + expectedForAddress2); + EXPECT_EQ(getBalance(QIP_testAddress3), balanceBefore3 + expectedForAddress3); + + sint64 contractBalanceAfter = getBalance(QIP_CONTRACT_ID); + sint64 contractBalanceChange = contractBalanceAfter - contractBalanceBefore; + sint64 expectedContractBalanceChange = requiredReward - totalDistributedToAddresses - expectedDividendAmount; + EXPECT_EQ(contractBalanceChange, expectedContractBalanceChange); + + // Transfer management rights + sint64 transferAmount = 100000; + + increaseEnergy(creator, QIP_TRANSFER_RIGHTS_FEE); + sint64 transferred = QIP.transferShareManagementRights(creator, asset, transferAmount, QX_CONTRACT_INDEX, QIP_TRANSFER_RIGHTS_FEE); + EXPECT_EQ(transferred, transferAmount); + + // Verify shares were transferred + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, QIP_CONTRACT_ID, QIP_CONTRACT_ID, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), totalShares - transferAmount); +} + diff --git a/test/contract_qraffle.cpp b/test/contract_qraffle.cpp new file mode 100644 index 000000000..afbf3e503 --- /dev/null +++ b/test/contract_qraffle.cpp @@ -0,0 +1,1576 @@ +#define NO_UEFI + +#include +#include + +#include "contract_testing.h" + +static std::mt19937_64 rand64; + +static unsigned long long random(unsigned long long minValue, unsigned long long maxValue) +{ + if(minValue > maxValue) + { + return 0; + } + return minValue + rand64() % (maxValue - minValue); +} + +static id getUser(unsigned long long i) +{ + return id(i, i / 2 + 4, i + 10, i * 3 + 8); +} + +static std::vector getRandomUsers(unsigned int totalUsers, unsigned int maxNum) +{ + std::map userMap; + unsigned long long userCount = random(0, maxNum); + std::vector users; + users.reserve(userCount); + for (unsigned int i = 0; i < userCount; ++i) + { + unsigned long long userIdx = random(0, totalUsers - 1); + id user = getUser(userIdx); + if (userMap.contains(user)) + { + continue; + } + userMap[user] = true; + users.push_back(user); + } + return users; +} + +class QRaffleChecker : public QRAFFLE +{ +public: + void registerChecker(const id& user, uint32 expectedRegisters, bool isRegistered) + { + if (isRegistered) + { + EXPECT_EQ(registers.contains(user), 1); + } + else + { + EXPECT_EQ(registers.contains(user), 0); + } + EXPECT_EQ(numberOfRegisters, expectedRegisters); + } + + void unregisterChecker(const id& user, uint32 expectedRegisters) + { + EXPECT_EQ(registers.contains(user), 0); + EXPECT_EQ(numberOfRegisters, expectedRegisters); + } + + void entryAmountChecker(const id& user, uint64 expectedAmount, uint32 expectedSubmitted) + { + uint64 amount = 0; + if (quRaffleEntryAmount.contains(user)) + { + quRaffleEntryAmount.get(user, amount); + EXPECT_EQ(amount, expectedAmount); + } + EXPECT_EQ(numberOfEntryAmountSubmitted, expectedSubmitted); + } + + void proposalChecker(uint32 index, const Asset& expectedToken, uint64 expectedEntryAmount) + { + EXPECT_EQ(proposals.get(index).token.assetName, expectedToken.assetName); + EXPECT_EQ(proposals.get(index).token.issuer, expectedToken.issuer); + EXPECT_EQ(proposals.get(index).entryAmount, expectedEntryAmount); + EXPECT_EQ(numberOfProposals, index + 1); + } + + void voteChecker(uint32 proposalIndex, uint32 expectedYes, uint32 expectedNo) + { + EXPECT_EQ(proposals.get(proposalIndex).nYes, expectedYes); + EXPECT_EQ(proposals.get(proposalIndex).nNo, expectedNo); + } + + void quRaffleMemberChecker(const id& user, uint32 expectedMembers) + { + bool found = false; + for (uint32 i = 0; i < numberOfQuRaffleMembers; i++) + { + if (quRaffleMembers.get(i) == user) + { + found = true; + break; + } + } + EXPECT_EQ(found, 1); + EXPECT_EQ(numberOfQuRaffleMembers, expectedMembers); + } + + void tokenRaffleMemberChecker(uint32 raffleIndex, const id& user, uint32 expectedMembers) + { + tokenRaffleMembers.get(raffleIndex, tmpTokenRaffleMembers); + bool found = false; + for (uint32 i = 0; i < numberOfTokenRaffleMembers.get(raffleIndex); i++) + { + if (tmpTokenRaffleMembers.get(i) == user) + { + found = true; + break; + } + } + EXPECT_EQ(found, 1); + EXPECT_EQ(numberOfTokenRaffleMembers.get(raffleIndex), expectedMembers); + } + + void analyticsChecker(uint64 expectedBurn, uint64 expectedCharity, uint64 expectedShareholder, + uint64 expectedRegister, uint64 expectedFee, uint64 expectedWinner, + uint64 expectedLargestWinner, uint32 expectedRegisters, uint32 expectedProposals, + uint32 expectedQuMembers, uint32 expectedActiveTokenRaffle, + uint32 expectedEndedTokenRaffle, uint32 expectedEntrySubmitted) + { + EXPECT_EQ(totalBurnAmount, expectedBurn); + EXPECT_EQ(totalCharityAmount, expectedCharity); + EXPECT_EQ(totalShareholderAmount, expectedShareholder); + EXPECT_EQ(totalRegisterAmount, expectedRegister); + EXPECT_EQ(totalFeeAmount, expectedFee); + EXPECT_EQ(totalWinnerAmount, expectedWinner); + EXPECT_EQ(largestWinnerAmount, expectedLargestWinner); + EXPECT_EQ(numberOfRegisters, expectedRegisters); + EXPECT_EQ(numberOfProposals, expectedProposals); + EXPECT_EQ(numberOfQuRaffleMembers, expectedQuMembers); + EXPECT_EQ(numberOfActiveTokenRaffle, expectedActiveTokenRaffle); + EXPECT_EQ(numberOfEndedTokenRaffle, expectedEndedTokenRaffle); + EXPECT_EQ(numberOfEntryAmountSubmitted, expectedEntrySubmitted); + } + + void activeTokenRaffleChecker(uint32 index, const Asset& expectedToken, uint64 expectedEntryAmount) + { + EXPECT_EQ(activeTokenRaffle.get(index).token.assetName, expectedToken.assetName); + EXPECT_EQ(activeTokenRaffle.get(index).token.issuer, expectedToken.issuer); + EXPECT_EQ(activeTokenRaffle.get(index).entryAmount, expectedEntryAmount); + } + + void endedTokenRaffleChecker(uint32 index, const id& expectedWinner, const Asset& expectedToken, + uint64 expectedEntryAmount, uint32 expectedMembers, uint32 expectedWinnerIndex, uint32 expectedEpoch) + { + EXPECT_EQ(tokenRaffle.get(index).epochWinner, expectedWinner); + EXPECT_EQ(tokenRaffle.get(index).token.assetName, expectedToken.assetName); + EXPECT_EQ(tokenRaffle.get(index).token.issuer, expectedToken.issuer); + EXPECT_EQ(tokenRaffle.get(index).entryAmount, expectedEntryAmount); + EXPECT_EQ(tokenRaffle.get(index).numberOfMembers, expectedMembers); + EXPECT_EQ(tokenRaffle.get(index).winnerIndex, expectedWinnerIndex); + EXPECT_EQ(tokenRaffle.get(index).epoch, expectedEpoch); + } + + void quRaffleWinnerChecker(uint16 epoch, const id& expectedWinner, uint64 expectedReceived, + uint64 expectedEntryAmount, uint32 expectedMembers, uint32 expectedWinnerIndex) + { + EXPECT_EQ(QuRaffles.get(epoch).epochWinner, expectedWinner); + EXPECT_EQ(QuRaffles.get(epoch).receivedAmount, expectedReceived); + EXPECT_EQ(QuRaffles.get(epoch).entryAmount, expectedEntryAmount); + EXPECT_EQ(QuRaffles.get(epoch).numberOfMembers, expectedMembers); + EXPECT_EQ(QuRaffles.get(epoch).winnerIndex, expectedWinnerIndex); + } + + uint64 getQuRaffleEntryAmount() + { + return qREAmount; + } + + uint32 getNumberOfActiveTokenRaffle() + { + return numberOfActiveTokenRaffle; + } + + uint32 getNumberOfEndedTokenRaffle() + { + return numberOfEndedTokenRaffle; + } + + uint64 getEpochQXMRRevenue() + { + return epochQXMRRevenue; + } + + uint32 getNumberOfRegisters() + { + return numberOfRegisters; + } + + id getQXMRIssuer() + { + return QXMRIssuer; + } +}; + +class ContractTestingQraffle : protected ContractTesting +{ +public: + ContractTestingQraffle() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QRAFFLE); + callSystemProcedure(QRAFFLE_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + } + + QRaffleChecker* getState() + { + return (QRaffleChecker*)contractStates[QRAFFLE_CONTRACT_INDEX]; + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QRAFFLE_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + QRAFFLE::registerInSystem_output registerInSystem(const id& user, uint64 amount, bit useQXMR) + { + QRAFFLE::registerInSystem_input input; + QRAFFLE::registerInSystem_output output; + + input.useQXMR = useQXMR; + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 1, input, output, user, amount); + return output; + } + + QRAFFLE::logoutInSystem_output logoutInSystem(const id& user) + { + QRAFFLE::logoutInSystem_input input; + QRAFFLE::logoutInSystem_output output; + + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 2, input, output, user, 0); + return output; + } + + QRAFFLE::submitEntryAmount_output submitEntryAmount(const id& user, uint64 amount) + { + QRAFFLE::submitEntryAmount_input input; + QRAFFLE::submitEntryAmount_output output; + + input.amount = amount; + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 3, input, output, user, 0); + return output; + } + + QRAFFLE::submitProposal_output submitProposal(const id& user, const Asset& token, uint64 entryAmount) + { + QRAFFLE::submitProposal_input input; + QRAFFLE::submitProposal_output output; + + input.tokenIssuer = token.issuer; + input.tokenName = token.assetName; + input.entryAmount = entryAmount; + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 4, input, output, user, 0); + return output; + } + + QRAFFLE::voteInProposal_output voteInProposal(const id& user, uint32 proposalIndex, bit yes) + { + QRAFFLE::voteInProposal_input input; + QRAFFLE::voteInProposal_output output; + + input.indexOfProposal = proposalIndex; + input.yes = yes; + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 5, input, output, user, 0); + return output; + } + + QRAFFLE::depositInQuRaffle_output depositInQuRaffle(const id& user, uint64 amount) + { + QRAFFLE::depositInQuRaffle_input input; + QRAFFLE::depositInQuRaffle_output output; + + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 6, input, output, user, amount); + return output; + } + + QRAFFLE::depositInTokenRaffle_output depositInTokenRaffle(const id& user, uint32 raffleIndex, uint64 amount) + { + QRAFFLE::depositInTokenRaffle_input input; + QRAFFLE::depositInTokenRaffle_output output; + + input.indexOfTokenRaffle = raffleIndex; + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 7, input, output, user, amount); + return output; + } + + QRAFFLE::getRegisters_output getRegisters(uint32 offset, uint32 limit) + { + QRAFFLE::getRegisters_input input; + QRAFFLE::getRegisters_output output; + + input.offset = offset; + input.limit = limit; + callFunction(QRAFFLE_CONTRACT_INDEX, 1, input, output); + return output; + } + + QRAFFLE::getAnalytics_output getAnalytics() + { + QRAFFLE::getAnalytics_input input; + QRAFFLE::getAnalytics_output output; + + callFunction(QRAFFLE_CONTRACT_INDEX, 2, input, output); + return output; + } + + QRAFFLE::getActiveProposal_output getActiveProposal(uint32 proposalIndex) + { + QRAFFLE::getActiveProposal_input input; + QRAFFLE::getActiveProposal_output output; + + input.indexOfProposal = proposalIndex; + callFunction(QRAFFLE_CONTRACT_INDEX, 3, input, output); + return output; + } + + QRAFFLE::getEndedTokenRaffle_output getEndedTokenRaffle(uint32 raffleIndex) + { + QRAFFLE::getEndedTokenRaffle_input input; + QRAFFLE::getEndedTokenRaffle_output output; + + input.indexOfRaffle = raffleIndex; + callFunction(QRAFFLE_CONTRACT_INDEX, 4, input, output); + return output; + } + + QRAFFLE::getEndedQuRaffle_output getEndedQuRaffle(uint16 epoch) + { + QRAFFLE::getEndedQuRaffle_input input; + QRAFFLE::getEndedQuRaffle_output output; + + input.epoch = epoch; + callFunction(QRAFFLE_CONTRACT_INDEX, 5, input, output); + return output; + } + + QRAFFLE::getActiveTokenRaffle_output getActiveTokenRaffle(uint32 raffleIndex) + { + QRAFFLE::getActiveTokenRaffle_input input; + QRAFFLE::getActiveTokenRaffle_output output; + + input.indexOfTokenRaffle = raffleIndex; + callFunction(QRAFFLE_CONTRACT_INDEX, 6, input, output); + return output; + } + + QRAFFLE::getEpochRaffleIndexes_output getEpochRaffleIndexes(uint16 epoch) + { + QRAFFLE::getEpochRaffleIndexes_input input; + QRAFFLE::getEpochRaffleIndexes_output output; + + input.epoch = epoch; + callFunction(QRAFFLE_CONTRACT_INDEX, 7, input, output); + return output; + } + + QRAFFLE::getQuRaffleEntryAmountPerUser_output getQuRaffleEntryAmountPerUser(const id& user) + { + QRAFFLE::getQuRaffleEntryAmountPerUser_input input; + QRAFFLE::getQuRaffleEntryAmountPerUser_output output; + + input.user = user; + callFunction(QRAFFLE_CONTRACT_INDEX, 8, input, output); + return output; + } + + QRAFFLE::getQuRaffleEntryAverageAmount_output getQuRaffleEntryAverageAmount() + { + QRAFFLE::getQuRaffleEntryAverageAmount_input input; + QRAFFLE::getQuRaffleEntryAverageAmount_output output; + + callFunction(QRAFFLE_CONTRACT_INDEX, 9, input, output); + return output; + } + + sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares, uint64 unitOfMeasurement, sint8 numberOfDecimalPlaces) + { + QX::IssueAsset_input input{ assetName, numberOfShares, unitOfMeasurement, numberOfDecimalPlaces }; + QX::IssueAsset_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, 1000000000ULL); + return output.issuedNumberOfShares; + } + + sint64 transferShareOwnershipAndPossession(const id& issuer, uint64 assetName, const id& currentOwnerAndPossesor, sint64 numberOfShares, const id& newOwnerAndPossesor) + { + QX::TransferShareOwnershipAndPossession_input input; + QX::TransferShareOwnershipAndPossession_output output; + + input.assetName = assetName; + input.issuer = issuer; + input.newOwnerAndPossessor = newOwnerAndPossesor; + input.numberOfShares = numberOfShares; + + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, currentOwnerAndPossesor, 100); + return output.transferredNumberOfShares; + } + + sint64 TransferShareManagementRights(const id& issuer, uint64 assetName, uint32 newManagingContractIndex, sint64 numberOfShares, const id& currentOwner) + { + QX::TransferShareManagementRights_input input; + QX::TransferShareManagementRights_output output; + + input.asset.assetName = assetName; + input.asset.issuer = issuer; + input.newManagingContractIndex = newManagingContractIndex; + input.numberOfShares = numberOfShares; + + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, currentOwner, 0); + + return output.transferredNumberOfShares; + } + + sint64 TransferShareManagementRightsQraffle(const id& issuer, uint64 assetName, uint32 newManagingContractIndex, sint64 numberOfShares, const id& currentOwner) + { + QRAFFLE::TransferShareManagementRights_input input; + QRAFFLE::TransferShareManagementRights_output output; + + input.tokenName = assetName; + input.tokenIssuer = issuer; + input.newManagingContractIndex = newManagingContractIndex; + input.numberOfShares = numberOfShares; + + invokeUserProcedure(QRAFFLE_CONTRACT_INDEX, 8, input, output, currentOwner, QRAFFLE_TRANSFER_SHARE_FEE); + return output.transferredNumberOfShares; + } +}; + +TEST(ContractQraffle, RegisterInSystem) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Test successful registration + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + auto result = qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->registerChecker(user, ++registerCount, true); + } + + // // Test insufficient funds + id poorUser = getUser(9999); + increaseEnergy(poorUser, QRAFFLE_REGISTER_AMOUNT - 1); + auto result = qraffle.registerInSystem(poorUser, QRAFFLE_REGISTER_AMOUNT - 1, 0); + EXPECT_EQ(result.returnCode, QRAFFLE_INSUFFICIENT_FUND); + qraffle.getState()->registerChecker(poorUser, registerCount, false); + + // Test already registered + increaseEnergy(users[0], QRAFFLE_REGISTER_AMOUNT); + result = qraffle.registerInSystem(users[0], QRAFFLE_REGISTER_AMOUNT, 0); + EXPECT_EQ(result.returnCode, QRAFFLE_ALREADY_REGISTERED); + qraffle.getState()->registerChecker(users[0], registerCount, true); +} + +TEST(ContractQraffle, LogoutInSystem) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + } + + // Test successful logout + for (const auto& user : users) + { + auto result = qraffle.logoutInSystem(user); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(getBalance(user), QRAFFLE_REGISTER_AMOUNT - QRAFFLE_LOGOUT_FEE); + qraffle.getState()->unregisterChecker(user, --registerCount); + } + + // Test unregistered user logout + qraffle.getState()->unregisterChecker(users[0], registerCount); + auto result = qraffle.logoutInSystem(users[0]); + EXPECT_EQ(result.returnCode, QRAFFLE_UNREGISTERED); +} + +TEST(ContractQraffle, SubmitEntryAmount) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + uint32 entrySubmittedCount = 0; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + } + + // Test successful entry amount submission + for (const auto& user : users) + { + uint64 amount = random(1000000, 1000000000); + auto result = qraffle.submitEntryAmount(user, amount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->entryAmountChecker(user, amount, ++entrySubmittedCount); + } + + // Test unregistered user + id unregisteredUser = getUser(9999); + increaseEnergy(unregisteredUser, QRAFFLE_REGISTER_AMOUNT); + auto result = qraffle.submitEntryAmount(unregisteredUser, 1000000); + EXPECT_EQ(result.returnCode, QRAFFLE_UNREGISTERED); + + // Test update entry amount + uint64 newAmount = random(1000000, 1000000000); + result = qraffle.submitEntryAmount(users[0], newAmount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->entryAmountChecker(users[0], newAmount, entrySubmittedCount); +} + +TEST(ContractQraffle, SubmitProposal) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + uint32 proposalCount = 0; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + } + + // Issue some test assets + id issuer = getUser(2000); + increaseEnergy(issuer, 1000000000ULL); + uint64 assetName1 = assetNameFromString("TEST1"); + uint64 assetName2 = assetNameFromString("TEST2"); + qraffle.issueAsset(issuer, assetName1, 1000000, 0, 0); + qraffle.issueAsset(issuer, assetName2, 2000000, 0, 0); + + Asset token1, token2; + token1.assetName = assetName1; + token1.issuer = issuer; + token2.assetName = assetName2; + token2.issuer = issuer; + + // Test successful proposal submission + for (const auto& user : users) + { + uint64 entryAmount = random(1000000, 1000000000); + Asset token = (random(0, 2) == 0) ? token1 : token2; + + if (proposalCount == QRAFFLE_MAX_PROPOSAL_EPOCH - 1) + { + break; + } + increaseEnergy(user, 1000); + auto result = qraffle.submitProposal(user, token, entryAmount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->proposalChecker(proposalCount, token, entryAmount); + proposalCount++; + } + + // Test unregistered user + id unregisteredUser = getUser(1999); + increaseEnergy(unregisteredUser, QRAFFLE_REGISTER_AMOUNT); + auto result = qraffle.submitProposal(unregisteredUser, token1, 1000000); + EXPECT_EQ(result.returnCode, QRAFFLE_UNREGISTERED); +} + +TEST(ContractQraffle, VoteInProposal) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + uint32 proposalCount = 0; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + } + + // Create a proposal + id issuer = getUser(2000); + increaseEnergy(issuer, 1000000000ULL); + uint64 assetName = assetNameFromString("VOTETS"); + qraffle.issueAsset(issuer, assetName, 1000000, 0, 0); + + Asset token; + token.assetName = assetName; + token.issuer = issuer; + + qraffle.submitProposal(users[0], token, 1000000); + proposalCount++; + + uint32 yesVotes = 0, noVotes = 0; + + // Test voting + for (const auto& user : users) + { + bit vote = (bit)random(0, 2); + auto result = qraffle.voteInProposal(user, 0, vote); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + + if (vote) + yesVotes++; + else + noVotes++; + + qraffle.getState()->voteChecker(0, yesVotes, noVotes); + } + + // Test duplicate vote (should change vote) + bit newVote = (bit)random(0, 2); + auto result = qraffle.voteInProposal(users[0], 0, newVote); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + + if (newVote) + { + yesVotes++; + noVotes--; + } + else + { + noVotes++; + yesVotes--; + } + + qraffle.getState()->voteChecker(0, yesVotes, noVotes); + + // Test unregistered user + id unregisteredUser = getUser(9999); + increaseEnergy(unregisteredUser, 1000000000ULL); + result = qraffle.voteInProposal(unregisteredUser, 0, 1); + EXPECT_EQ(result.returnCode, QRAFFLE_UNREGISTERED); + + // Test invalid proposal index + result = qraffle.voteInProposal(users[0], 9999, 1); + EXPECT_EQ(result.returnCode, QRAFFLE_INVALID_PROPOSAL); +} + +TEST(ContractQraffle, depositInQuRaffle) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + uint32 memberCount = 0; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + } + + // Test successful deposit + for (const auto& user : users) + { + increaseEnergy(user, qraffle.getState()->getQuRaffleEntryAmount()); + auto result = qraffle.depositInQuRaffle(user, qraffle.getState()->getQuRaffleEntryAmount()); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->quRaffleMemberChecker(user, ++memberCount); + } + + // Test insufficient funds + id poorUser = getUser(9999); + increaseEnergy(poorUser, qraffle.getState()->getQuRaffleEntryAmount() - 1); + auto result = qraffle.depositInQuRaffle(poorUser, qraffle.getState()->getQuRaffleEntryAmount() - 1); + EXPECT_EQ(result.returnCode, QRAFFLE_INSUFFICIENT_FUND); + + // Test already registered + increaseEnergy(users[0], qraffle.getState()->getQuRaffleEntryAmount()); + result = qraffle.depositInQuRaffle(users[0], qraffle.getState()->getQuRaffleEntryAmount()); + EXPECT_EQ(result.returnCode, QRAFFLE_ALREADY_REGISTERED); +} + +TEST(ContractQraffle, DepositInTokenRaffle) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + } + + // Create a proposal and vote for it + id issuer = getUser(2000); + increaseEnergy(issuer, 2000000000ULL); + uint64 assetName = assetNameFromString("TOKENRF"); + qraffle.issueAsset(issuer, assetName, 1000000000000, 0, 0); + + Asset token; + token.assetName = assetName; + token.issuer = issuer; + + qraffle.submitProposal(users[0], token, 1000000); + + // Vote yes for the proposal + for (const auto& user : users) + { + qraffle.voteInProposal(user, 0, 1); + } + + // End epoch to activate token raffle + qraffle.endEpoch(); + + // Test active token raffle + auto activeRaffle = qraffle.getActiveTokenRaffle(0); + EXPECT_EQ(activeRaffle.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(activeRaffle.tokenName, assetName); + EXPECT_EQ(activeRaffle.tokenIssuer, issuer); + EXPECT_EQ(activeRaffle.entryAmount, 1000000); + + // Test successful token raffle deposit + uint32 memberCount = 0; + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_TRANSFER_SHARE_FEE); + EXPECT_EQ(qraffle.transferShareOwnershipAndPossession(issuer, assetName, issuer, 1000000, user), 1000000); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, user, user, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 1000000); + + EXPECT_EQ(qraffle.TransferShareManagementRights(issuer, assetName, QRAFFLE_CONTRACT_INDEX, 1000000, user), 1000000); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, user, user, QRAFFLE_CONTRACT_INDEX, QRAFFLE_CONTRACT_INDEX), 1000000); + + auto result = qraffle.depositInTokenRaffle(user, 0, QRAFFLE_TRANSFER_SHARE_FEE); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + memberCount++; + qraffle.getState()->tokenRaffleMemberChecker(0, user, memberCount); + } + + // Test insufficient funds + id poorUser = getUser(9999); + increaseEnergy(poorUser, QRAFFLE_TRANSFER_SHARE_FEE - 1); + auto result = qraffle.depositInTokenRaffle(poorUser, 0, QRAFFLE_TRANSFER_SHARE_FEE - 1); + EXPECT_EQ(result.returnCode, QRAFFLE_INSUFFICIENT_FUND); + + // Test insufficient Token + id poorUser2 = getUser(8888); + increaseEnergy(poorUser2, QRAFFLE_TRANSFER_SHARE_FEE); + qraffle.transferShareOwnershipAndPossession(issuer, assetName, issuer, 999999, poorUser2); + result = qraffle.depositInTokenRaffle(poorUser2, 0, QRAFFLE_TRANSFER_SHARE_FEE); + EXPECT_EQ(result.returnCode, QRAFFLE_FAILED_TO_DEPOSIT); + + // Test invalid token raffle index + increaseEnergy(users[0], QRAFFLE_TRANSFER_SHARE_FEE); + result = qraffle.depositInTokenRaffle(users[0], 999, QRAFFLE_TRANSFER_SHARE_FEE); + EXPECT_EQ(result.returnCode, QRAFFLE_INVALID_TOKEN_RAFFLE); +} + +TEST(ContractQraffle, TransferShareManagementRights) +{ + ContractTestingQraffle qraffle; + + id issuer = getUser(1000); + increaseEnergy(issuer, 2000000000ULL); + uint64 assetName = assetNameFromString("TOKENRF"); + qraffle.issueAsset(issuer, assetName, 1000000000000, 0, 0); + + id user1 = getUser(1001); + increaseEnergy(user1, 1000000000ULL); + qraffle.transferShareOwnershipAndPossession(issuer, assetName, issuer, 1000000, user1); + EXPECT_EQ(qraffle.TransferShareManagementRights(issuer, assetName, QRAFFLE_CONTRACT_INDEX, 1000000, user1), 1000000); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, user1, user1, QRAFFLE_CONTRACT_INDEX, QRAFFLE_CONTRACT_INDEX), 1000000); + + increaseEnergy(user1, 1000000000ULL); + qraffle.TransferShareManagementRightsQraffle(issuer, assetName, QX_CONTRACT_INDEX, 1000000, user1); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, user1, user1, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 1000000); +} + +TEST(ContractQraffle, GetFunctions) +{ + ContractTestingQraffle qraffle; + system.epoch = 0; + + // Setup: Create test users and register them + auto users = getRandomUsers(1000, 1000); // Use smaller set for more predictable testing + uint32 registerCount = 5; + uint32 proposalCount = 0; + uint32 entrySubmittedCount = 0; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + auto result = qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + registerCount++; + } + + // Submit entry amounts for some users + for (size_t i = 0; i < users.size() / 2; ++i) + { + uint64 amount = random(1000000, 1000000000); + auto result = qraffle.submitEntryAmount(users[i], amount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + entrySubmittedCount++; + } + + // Create some proposals + id issuer = getUser(2000); + increaseEnergy(issuer, 1000000000ULL); + uint64 assetName1 = assetNameFromString("TEST1"); + uint64 assetName2 = assetNameFromString("TEST2"); + qraffle.issueAsset(issuer, assetName1, 1000000000, 0, 0); + qraffle.issueAsset(issuer, assetName2, 2000000000, 0, 0); + + Asset token1, token2; + token1.assetName = assetName1; + token1.issuer = issuer; + token2.assetName = assetName2; + token2.issuer = issuer; + + // Submit proposals + for (size_t i = 0; i < std::min(users.size(), (size_t)5); ++i) + { + uint64 entryAmount = random(1000000, 1000000000); + Asset token = (i % 2 == 0) ? token1 : token2; + + increaseEnergy(users[i], 1000); + auto result = qraffle.submitProposal(users[i], token, entryAmount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + proposalCount++; + } + + // Vote on proposals + for (const auto& user : users) + { + for (uint32 i = 0; i < proposalCount; ++i) + { + bit vote = (bit)(i % 2); + auto result = qraffle.voteInProposal(user, i, vote); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + } + } + + // Deposit in QuRaffle + uint32 memberCount = 0; + for (size_t i = 0; i < users.size() / 3; ++i) + { + increaseEnergy(users[i], qraffle.getState()->getQuRaffleEntryAmount()); + auto result = qraffle.depositInQuRaffle(users[i], qraffle.getState()->getQuRaffleEntryAmount()); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + memberCount++; + } + + // Test 1: getActiveProposal function + { + // Test with valid proposal indices + for (uint32 i = 0; i < proposalCount; ++i) + { + auto proposal = qraffle.getActiveProposal(i); + EXPECT_EQ(proposal.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(proposal.tokenName, (i % 2 == 0) ? assetName1 : assetName2); + EXPECT_EQ(proposal.tokenIssuer, issuer); + EXPECT_GT(proposal.entryAmount, 0); + EXPECT_GE(proposal.nYes, 0u); + EXPECT_GE(proposal.nNo, 0u); + } + + // Test with invalid proposal index (beyond available proposals) + auto invalidProposal = qraffle.getActiveProposal(proposalCount + 10); + EXPECT_EQ(invalidProposal.returnCode, QRAFFLE_INVALID_PROPOSAL); + + // Test with very large proposal index + auto largeIndexProposal = qraffle.getActiveProposal(UINT32_MAX); + EXPECT_EQ(largeIndexProposal.returnCode, QRAFFLE_INVALID_PROPOSAL); + } + + + // End epoch to create some ended raffles + qraffle.endEpoch(); + + // ===== DETAILED TEST CASES FOR EACH GETTER FUNCTION ===== + + // Test 2: getRegisters function + { + // Test with valid offset and limit + auto registers = qraffle.getRegisters(0, 10); + EXPECT_EQ(registers.returnCode, QRAFFLE_SUCCESS); + + // Test with offset beyond available registers + auto registers2 = qraffle.getRegisters(registerCount + 10, 5); + EXPECT_EQ(registers2.returnCode, QRAFFLE_INVALID_OFFSET_OR_LIMIT); + + // Test with limit exceeding maximum (1024) + auto registers3 = qraffle.getRegisters(0, 1025); + EXPECT_EQ(registers3.returnCode, QRAFFLE_INVALID_OFFSET_OR_LIMIT); + + // Test with offset + limit exceeding total registers + auto registers4 = qraffle.getRegisters(registerCount - 5, 10); + EXPECT_EQ(registers4.returnCode, QRAFFLE_INVALID_OFFSET_OR_LIMIT); + + // Test with zero limit + auto registers5 = qraffle.getRegisters(0, 0); + EXPECT_EQ(registers5.returnCode, QRAFFLE_SUCCESS); + } + + // Test 3: getAnalytics function + { + auto analytics = qraffle.getAnalytics(); + EXPECT_EQ(analytics.returnCode, QRAFFLE_SUCCESS); + + // Validate all analytics fields + EXPECT_GE(analytics.totalBurnAmount, 0); + EXPECT_GE(analytics.totalCharityAmount, 0); + EXPECT_GE(analytics.totalShareholderAmount, 0); + EXPECT_GE(analytics.totalRegisterAmount, 0); + EXPECT_GE(analytics.totalFeeAmount, 0); + EXPECT_GE(analytics.totalWinnerAmount, 0); + EXPECT_GE(analytics.largestWinnerAmount, 0); + EXPECT_EQ(analytics.numberOfRegisters, registerCount); + EXPECT_EQ(analytics.numberOfProposals, 0); + EXPECT_EQ(analytics.numberOfQuRaffleMembers, 0); + EXPECT_GE(analytics.numberOfActiveTokenRaffle, 0u); + EXPECT_GE(analytics.numberOfEndedTokenRaffle, 0u); + EXPECT_EQ(analytics.numberOfEntryAmountSubmitted, 0u); + + // Cross-validate with internal state + qraffle.getState()->analyticsChecker(analytics.totalBurnAmount, analytics.totalCharityAmount, + analytics.totalShareholderAmount, analytics.totalRegisterAmount, + analytics.totalFeeAmount, analytics.totalWinnerAmount, + analytics.largestWinnerAmount, analytics.numberOfRegisters, + analytics.numberOfProposals, analytics.numberOfQuRaffleMembers, + analytics.numberOfActiveTokenRaffle, analytics.numberOfEndedTokenRaffle, + analytics.numberOfEntryAmountSubmitted); + + // Direct-validate with calculated values + // Calculate expected values based on the test setup + uint64 expectedTotalBurnAmount = 0; + uint64 expectedTotalCharityAmount = 0; + uint64 expectedTotalShareholderAmount = 0; + uint64 expectedTotalRegisterAmount = 0; + uint64 expectedTotalFeeAmount = 0; + uint64 expectedTotalWinnerAmount = 0; + uint64 expectedLargestWinnerAmount = 0; + + // Calculate expected values from QuRaffle (if any members participated) + if (memberCount > 0) { + uint64 qREAmount = 10000000; // initial entry amount + uint64 totalQuRaffleAmount = qREAmount * memberCount; + + expectedTotalBurnAmount += (totalQuRaffleAmount * QRAFFLE_BURN_FEE) / 100; + expectedTotalCharityAmount += (totalQuRaffleAmount * QRAFFLE_CHARITY_FEE) / 100; + expectedTotalShareholderAmount += ((totalQuRaffleAmount * QRAFFLE_SHRAEHOLDER_FEE) / 100) / 676 * 676; + expectedTotalRegisterAmount += ((totalQuRaffleAmount * QRAFFLE_REGISTER_FEE) / 100) / registerCount * registerCount; + expectedTotalFeeAmount += (totalQuRaffleAmount * QRAFFLE_FEE) / 100; + + // Winner amount calculation (after all fees) + uint64 winnerAmount = totalQuRaffleAmount - expectedTotalBurnAmount - expectedTotalCharityAmount + - expectedTotalShareholderAmount - expectedTotalRegisterAmount - expectedTotalFeeAmount; + expectedTotalWinnerAmount += winnerAmount; + expectedLargestWinnerAmount = winnerAmount; // First winner sets the largest + } + + // Validate calculated values + EXPECT_EQ(analytics.totalBurnAmount, expectedTotalBurnAmount); + EXPECT_EQ(analytics.totalCharityAmount, expectedTotalCharityAmount); + EXPECT_EQ(analytics.totalShareholderAmount, expectedTotalShareholderAmount); + EXPECT_EQ(analytics.totalRegisterAmount, expectedTotalRegisterAmount); + EXPECT_EQ(analytics.totalFeeAmount, expectedTotalFeeAmount); + EXPECT_EQ(analytics.totalWinnerAmount, expectedTotalWinnerAmount); + EXPECT_EQ(analytics.largestWinnerAmount, expectedLargestWinnerAmount); + + // Validate counters + EXPECT_EQ(analytics.numberOfRegisters, registerCount); + EXPECT_EQ(analytics.numberOfProposals, 0); // Proposals are cleared after epoch end + EXPECT_EQ(analytics.numberOfQuRaffleMembers, 0); // Members are cleared after epoch end + EXPECT_EQ(analytics.numberOfActiveTokenRaffle, qraffle.getState()->getNumberOfActiveTokenRaffle()); + EXPECT_EQ(analytics.numberOfEndedTokenRaffle, qraffle.getState()->getNumberOfEndedTokenRaffle()); + EXPECT_EQ(analytics.numberOfEntryAmountSubmitted, 0); // Entry amounts are cleared after epoch end + + } + + // Test 4: getEndedTokenRaffle function + { + // Test with valid raffle indices (if any ended raffles exist) + for (uint32 i = 0; i < qraffle.getState()->getNumberOfEndedTokenRaffle(); ++i) + { + auto endedRaffle = qraffle.getEndedTokenRaffle(i); + EXPECT_EQ(endedRaffle.returnCode, QRAFFLE_SUCCESS); + EXPECT_NE(endedRaffle.epochWinner, id(0, 0, 0, 0)); // Winner should be set + EXPECT_GT(endedRaffle.entryAmount, 0); + EXPECT_GT(endedRaffle.numberOfMembers, 0u); + EXPECT_GE(endedRaffle.epoch, 0u); + } + + // Test with invalid raffle index (beyond available ended raffles) + auto invalidEndedRaffle = qraffle.getEndedTokenRaffle(qraffle.getState()->getNumberOfEndedTokenRaffle() + 10); + EXPECT_EQ(invalidEndedRaffle.returnCode, QRAFFLE_INVALID_TOKEN_RAFFLE); + + // Test with very large raffle index + auto largeIndexEndedRaffle = qraffle.getEndedTokenRaffle(UINT32_MAX); + EXPECT_EQ(largeIndexEndedRaffle.returnCode, QRAFFLE_INVALID_TOKEN_RAFFLE); + } + + // Test 5: getEpochRaffleIndexes function + { + // Test with current epoch (0) + auto raffleIndexes = qraffle.getEpochRaffleIndexes(0); + EXPECT_EQ(raffleIndexes.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(raffleIndexes.StartIndex, 0); + EXPECT_EQ(raffleIndexes.EndIndex, qraffle.getState()->getNumberOfActiveTokenRaffle()); + + // Test with future epoch + auto futureRaffleIndexes = qraffle.getEpochRaffleIndexes(1); + EXPECT_EQ(futureRaffleIndexes.returnCode, QRAFFLE_INVALID_EPOCH); + + // Test with past epoch (if any exist) + if (qraffle.getState()->getNumberOfEndedTokenRaffle() > 0) + { + auto pastRaffleIndexes = qraffle.getEpochRaffleIndexes(0); // Should work for epoch 0 + EXPECT_EQ(pastRaffleIndexes.returnCode, QRAFFLE_SUCCESS); + } + } + + // Test 6: getEndedQuRaffle function + { + // Test with current epoch (0) + auto endedQuRaffle = qraffle.getEndedQuRaffle(0); + EXPECT_EQ(endedQuRaffle.returnCode, QRAFFLE_SUCCESS); + EXPECT_NE(endedQuRaffle.epochWinner, id(0, 0, 0, 0)); // Winner should be set + EXPECT_GT(endedQuRaffle.receivedAmount, 0); + EXPECT_EQ(endedQuRaffle.entryAmount, 10000000); + EXPECT_EQ(endedQuRaffle.numberOfMembers, memberCount); + + // Test with future epoch + auto futureQuRaffle = qraffle.getEndedQuRaffle(1); + EXPECT_EQ(futureQuRaffle.returnCode, QRAFFLE_SUCCESS); + + // Test with very large epoch number + auto largeEpochQuRaffle = qraffle.getEndedQuRaffle(UINT16_MAX); + EXPECT_EQ(largeEpochQuRaffle.returnCode, QRAFFLE_SUCCESS); + } + + // Test 7: getActiveTokenRaffle function + { + // Test with valid raffle indices (if any active raffles exist) + for (uint32 i = 0; i < qraffle.getState()->getNumberOfActiveTokenRaffle(); ++i) + { + auto activeRaffle = qraffle.getActiveTokenRaffle(i); + EXPECT_EQ(activeRaffle.returnCode, QRAFFLE_SUCCESS); + EXPECT_GT(activeRaffle.tokenName, 0); + EXPECT_NE(activeRaffle.tokenIssuer, id(0, 0, 0, 0)); + EXPECT_GT(activeRaffle.entryAmount, 0); + } + + // Test with invalid raffle index (beyond available active raffles) + auto invalidActiveRaffle = qraffle.getActiveTokenRaffle(qraffle.getState()->getNumberOfActiveTokenRaffle() + 10); + EXPECT_EQ(invalidActiveRaffle.returnCode, QRAFFLE_INVALID_TOKEN_RAFFLE); + + // Test with very large raffle index + auto largeIndexActiveRaffle = qraffle.getActiveTokenRaffle(UINT32_MAX); + EXPECT_EQ(largeIndexActiveRaffle.returnCode, QRAFFLE_INVALID_TOKEN_RAFFLE); + } +} + +TEST(ContractQraffle, EndEpoch) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + } + + // Submit entry amounts + for (const auto& user : users) + { + uint64 amount = random(1000000, 1000000000); + qraffle.submitEntryAmount(user, amount); + } + + // Create proposals and vote for them + id issuer = getUser(2000); + increaseEnergy(issuer, 3000000000ULL); + uint64 assetName1 = assetNameFromString("TOKEN1"); + uint64 assetName2 = assetNameFromString("TOKEN2"); + qraffle.issueAsset(issuer, assetName1, 1000000000, 0, 0); + qraffle.issueAsset(issuer, assetName2, 2000000000, 0, 0); + + Asset token1, token2; + token1.assetName = assetName1; + token1.issuer = issuer; + token2.assetName = assetName2; + token2.issuer = issuer; + + qraffle.submitProposal(users[0], token1, 1000000); + qraffle.submitProposal(users[1], token2, 2000000); + + // Vote yes for both proposals + for (const auto& user : users) + { + qraffle.voteInProposal(user, 0, 1); + qraffle.voteInProposal(user, 1, 1); + } + + // Deposit in QuRaffle + for (const auto& user : users) + { + increaseEnergy(user, qraffle.getState()->getQuRaffleEntryAmount()); + qraffle.depositInQuRaffle(user, qraffle.getState()->getQuRaffleEntryAmount()); + } + + // Deposit in token raffles + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_TRANSFER_SHARE_FEE + 1000000); + EXPECT_EQ(qraffle.transferShareOwnershipAndPossession(issuer, assetName1, issuer, 1000000, user), 1000000); + EXPECT_EQ(qraffle.transferShareOwnershipAndPossession(issuer, assetName2, issuer, 2000000, user), 2000000); + } + + // End epoch + qraffle.endEpoch(); + + qraffle.getState()->activeTokenRaffleChecker(0, token1, 1000000); + qraffle.getState()->activeTokenRaffleChecker(1, token2, 2000000); + + // Deposit in token raffles + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_TRANSFER_SHARE_FEE); + EXPECT_EQ(qraffle.TransferShareManagementRights(issuer, assetName1, QRAFFLE_CONTRACT_INDEX, 1000000, user), 1000000); + EXPECT_EQ(qraffle.TransferShareManagementRights(issuer, assetName2, QRAFFLE_CONTRACT_INDEX, 2000000, user), 2000000); + + qraffle.depositInTokenRaffle(user, 0, QRAFFLE_TRANSFER_SHARE_FEE); + qraffle.depositInTokenRaffle(user, 1, QRAFFLE_TRANSFER_SHARE_FEE); + } + + // Check that QuRaffle was processed + auto quRaffle = qraffle.getEndedQuRaffle(0); + EXPECT_EQ(quRaffle.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->quRaffleWinnerChecker(0, quRaffle.epochWinner, quRaffle.receivedAmount, + quRaffle.entryAmount, quRaffle.numberOfMembers, quRaffle.winnerIndex); + + qraffle.endEpoch(); + // Check that token raffles were processed + auto tokenRaffle1 = qraffle.getEndedTokenRaffle(0); + EXPECT_EQ(tokenRaffle1.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->endedTokenRaffleChecker(0, tokenRaffle1.epochWinner, token1, + tokenRaffle1.entryAmount, tokenRaffle1.numberOfMembers, + tokenRaffle1.winnerIndex, tokenRaffle1.epoch); + + auto tokenRaffle2 = qraffle.getEndedTokenRaffle(1); + EXPECT_EQ(tokenRaffle2.returnCode, QRAFFLE_SUCCESS); + qraffle.getState()->endedTokenRaffleChecker(1, tokenRaffle2.epochWinner, token2, + tokenRaffle2.entryAmount, tokenRaffle2.numberOfMembers, + tokenRaffle2.winnerIndex, tokenRaffle2.epoch); + + // Check analytics after epoch + auto analytics = qraffle.getAnalytics(); + EXPECT_EQ(analytics.returnCode, QRAFFLE_SUCCESS); + EXPECT_GT(analytics.totalBurnAmount, 0); + EXPECT_GT(analytics.totalCharityAmount, 0); + EXPECT_GT(analytics.totalShareholderAmount, 0); + EXPECT_GT(analytics.totalWinnerAmount, 0); +} + +TEST(ContractQraffle, RegisterInSystemWithQXMR) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Issue QXMR tokens to users + id qxmrIssuer = qraffle.getState()->getQXMRIssuer(); + increaseEnergy(qxmrIssuer, 2000000000ULL); + qraffle.issueAsset(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, 10000000000000, 0, 0); + + // Test successful registration with QXMR tokens + for (const auto& user : users) + { + increaseEnergy(user, 1000); + // Transfer QXMR tokens to user + qraffle.transferShareOwnershipAndPossession(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, QRAFFLE_QXMR_REGISTER_AMOUNT, user); + qraffle.TransferShareManagementRights(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, QRAFFLE_CONTRACT_INDEX, QRAFFLE_QXMR_REGISTER_AMOUNT, user); + + // Register using QXMR tokens + auto result = qraffle.registerInSystem(user, 0, 1); // useQXMR = 1 + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + registerCount++; + qraffle.getState()->registerChecker(user, registerCount, true); + } + + // Test insufficient QXMR tokens + id poorUser = getUser(9999); + increaseEnergy(poorUser, 1000); + qraffle.transferShareOwnershipAndPossession(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, QRAFFLE_QXMR_REGISTER_AMOUNT - 1, poorUser); + qraffle.TransferShareManagementRights(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, QRAFFLE_CONTRACT_INDEX, QRAFFLE_QXMR_REGISTER_AMOUNT - 1, poorUser); + auto result = qraffle.registerInSystem(poorUser, 0, 1); + EXPECT_EQ(result.returnCode, QRAFFLE_INSUFFICIENT_QXMR); + qraffle.getState()->registerChecker(poorUser, registerCount, false); + + // Test already registered with QXMR + qraffle.transferShareOwnershipAndPossession(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, QRAFFLE_QXMR_REGISTER_AMOUNT, users[0]); + qraffle.TransferShareManagementRights(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, QRAFFLE_CONTRACT_INDEX, QRAFFLE_QXMR_REGISTER_AMOUNT, users[0]); + result = qraffle.registerInSystem(users[0], 0, 1); + EXPECT_EQ(result.returnCode, QRAFFLE_ALREADY_REGISTERED); + qraffle.getState()->registerChecker(users[0], registerCount, true); +} + +TEST(ContractQraffle, LogoutInSystemWithQXMR) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Issue QXMR tokens + id qxmrIssuer = qraffle.getState()->getQXMRIssuer(); + increaseEnergy(qxmrIssuer, 2000000000ULL); + qraffle.issueAsset(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, 10000000000000, 0, 0); + + // Register users with QXMR tokens first + for (const auto& user : users) + { + increaseEnergy(user, 1000); + qraffle.transferShareOwnershipAndPossession(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, QRAFFLE_QXMR_REGISTER_AMOUNT, user); + qraffle.TransferShareManagementRights(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, QRAFFLE_CONTRACT_INDEX, QRAFFLE_QXMR_REGISTER_AMOUNT, user); + qraffle.registerInSystem(user, 0, 1); + registerCount++; + } + + // Test successful logout with QXMR tokens + for (const auto& user : users) + { + increaseEnergy(user, 1000); + auto result = qraffle.logoutInSystem(user); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + + // Check that user received QXMR refund + uint64 expectedRefund = QRAFFLE_QXMR_REGISTER_AMOUNT - QRAFFLE_QXMR_LOGOUT_FEE; + EXPECT_EQ(numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, user, user, QRAFFLE_CONTRACT_INDEX, QRAFFLE_CONTRACT_INDEX), expectedRefund); + + registerCount--; + qraffle.getState()->unregisterChecker(user, registerCount); + } + + // Test unregistered user logout with QXMR + increaseEnergy(users[0], 1000); + auto result = qraffle.logoutInSystem(users[0]); + EXPECT_EQ(result.returnCode, QRAFFLE_UNREGISTERED); +} + +TEST(ContractQraffle, MixedRegistrationAndLogout) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Issue QXMR tokens + id qxmrIssuer = qraffle.getState()->getQXMRIssuer(); + increaseEnergy(qxmrIssuer, 2000000000ULL); + qraffle.issueAsset(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, 10000000000000, 0, 0); + + // Register some users with qubic, some with QXMR + for (size_t i = 0; i < users.size(); ++i) + { + if (i % 2 == 0) + { + // Register with qubic + increaseEnergy(users[i], QRAFFLE_REGISTER_AMOUNT); + auto result = qraffle.registerInSystem(users[i], QRAFFLE_REGISTER_AMOUNT, 0); // useQXMR = 0 + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + } + else + { + // Register with QXMR + increaseEnergy(users[i], 1000); + qraffle.transferShareOwnershipAndPossession(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, QRAFFLE_QXMR_REGISTER_AMOUNT, users[i]); + qraffle.TransferShareManagementRights(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, QRAFFLE_CONTRACT_INDEX, QRAFFLE_QXMR_REGISTER_AMOUNT, users[i]); + auto result = qraffle.registerInSystem(users[i], 0, 1); // useQXMR = 1 + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + } + registerCount++; + } + + // Logout some users with qubic, some with QXMR + for (size_t i = 0; i < users.size(); ++i) + { + if (i % 2 == 0) + { + // Logout with qubic + auto result = qraffle.logoutInSystem(users[i]); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(getBalance(users[i]), QRAFFLE_REGISTER_AMOUNT - QRAFFLE_LOGOUT_FEE); + } + else + { + // Logout with QXMR + auto result = qraffle.logoutInSystem(users[i]); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + + uint64 expectedRefund = QRAFFLE_QXMR_REGISTER_AMOUNT - QRAFFLE_QXMR_LOGOUT_FEE; + EXPECT_EQ(numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, users[i], users[i], QRAFFLE_CONTRACT_INDEX, QRAFFLE_CONTRACT_INDEX), expectedRefund); + } + registerCount--; + } + + // Verify final state + EXPECT_EQ(qraffle.getState()->getNumberOfRegisters(), registerCount); +} + +TEST(ContractQraffle, QXMRInvalidTokenType) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Issue QXMR tokens + id qxmrIssuer = qraffle.getState()->getQXMRIssuer(); + increaseEnergy(qxmrIssuer, 2000000000ULL); + qraffle.issueAsset(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, 10000000000000, 0, 0); + + // Register user with qubic (token type 1) + increaseEnergy(users[0], QRAFFLE_REGISTER_AMOUNT); + qraffle.registerInSystem(users[0], QRAFFLE_REGISTER_AMOUNT, 0); + registerCount++; + + // Try to logout with QXMR when registered with qubic + auto result = qraffle.logoutInSystem(users[0]); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + + // Register user with QXMR (token type 2) + increaseEnergy(users[1], 1000); + qraffle.transferShareOwnershipAndPossession(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, QRAFFLE_QXMR_REGISTER_AMOUNT, users[1]); + qraffle.TransferShareManagementRights(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, QRAFFLE_CONTRACT_INDEX, QRAFFLE_QXMR_REGISTER_AMOUNT, users[1]); + qraffle.registerInSystem(users[1], 0, 1); + registerCount++; + + // Try to logout + result = qraffle.logoutInSystem(users[1]); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + + registerCount--; +} + +TEST(ContractQraffle, QXMRRevenueDistribution) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + + // Issue QXMR tokens + id qxmrIssuer = qraffle.getState()->getQXMRIssuer(); + increaseEnergy(qxmrIssuer, 2000000000ULL); + qraffle.issueAsset(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, 10000000000000, 0, 0); + + // Register some users with QXMR to generate QXMR revenue + for (const auto& user : users) + { + increaseEnergy(user, 1000); + qraffle.transferShareOwnershipAndPossession(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, qxmrIssuer, QRAFFLE_QXMR_REGISTER_AMOUNT, user); + qraffle.TransferShareManagementRights(qxmrIssuer, QRAFFLE_QXMR_ASSET_NAME, QRAFFLE_CONTRACT_INDEX, QRAFFLE_QXMR_REGISTER_AMOUNT, user); + qraffle.registerInSystem(user, 0, 1); + registerCount++; + } + + uint64 expectedQXMRRevenue = 0; + // Logout some users to generate QXMR revenue + for (size_t i = 0; i < users.size(); ++i) + { + auto result = qraffle.logoutInSystem(users[i]); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + expectedQXMRRevenue += QRAFFLE_QXMR_LOGOUT_FEE; + registerCount--; + } + + // Check that QXMR revenue was recorded + EXPECT_EQ(qraffle.getState()->getEpochQXMRRevenue(), expectedQXMRRevenue); + + // Test QXMR revenue distribution during epoch end + increaseEnergy(users[0], QRAFFLE_DEFAULT_QRAFFLE_AMOUNT); + qraffle.depositInQuRaffle(users[0], QRAFFLE_DEFAULT_QRAFFLE_AMOUNT); + + qraffle.endEpoch(); + EXPECT_EQ(qraffle.getState()->getEpochQXMRRevenue(), expectedQXMRRevenue - div(expectedQXMRRevenue, 676ull) * 676); +} + +TEST(ContractQraffle, GetQuRaffleEntryAmountPerUser) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + uint32 entrySubmittedCount = 0; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + auto result = qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + registerCount++; + } + + // Test 1: Query entry amount for users who haven't submitted any + for (const auto& user : users) + { + auto result = qraffle.getQuRaffleEntryAmountPerUser(user); + EXPECT_EQ(result.returnCode, QRAFFLE_USER_NOT_FOUND); + EXPECT_EQ(result.entryAmount, 0); + } + + // Submit entry amounts for some users + std::vector submittedAmounts; + for (size_t i = 0; i < users.size() / 2; ++i) + { + uint64 amount = random(1000000, 1000000000); + auto result = qraffle.submitEntryAmount(users[i], amount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + submittedAmounts.push_back(amount); + entrySubmittedCount++; + } + + // Test 2: Query entry amount for users who have submitted amounts + for (size_t i = 0; i < submittedAmounts.size(); ++i) + { + auto result = qraffle.getQuRaffleEntryAmountPerUser(users[i]); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(result.entryAmount, submittedAmounts[i]); + } + + // Test 3: Query entry amount for users who haven't submitted amounts + for (size_t i = submittedAmounts.size(); i < users.size(); ++i) + { + auto result = qraffle.getQuRaffleEntryAmountPerUser(users[i]); + EXPECT_EQ(result.returnCode, QRAFFLE_USER_NOT_FOUND); + EXPECT_EQ(result.entryAmount, 0); + } + + // Test 4: Update entry amount and verify + uint64 newAmount = random(1000000, 1000000000); + auto result = qraffle.submitEntryAmount(users[0], newAmount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + + auto updatedResult = qraffle.getQuRaffleEntryAmountPerUser(users[0]); + EXPECT_EQ(updatedResult.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(updatedResult.entryAmount, newAmount); + + // Test 5: Query for non-existent user + id nonExistentUser = getUser(99999); + auto nonExistentResult = qraffle.getQuRaffleEntryAmountPerUser(nonExistentUser); + EXPECT_EQ(nonExistentResult.returnCode, QRAFFLE_USER_NOT_FOUND); + EXPECT_EQ(nonExistentResult.entryAmount, 0); + + // Test 6: Query for unregistered user + id unregisteredUser = getUser(88888); + increaseEnergy(unregisteredUser, QRAFFLE_REGISTER_AMOUNT); + auto unregisteredResult = qraffle.getQuRaffleEntryAmountPerUser(unregisteredUser); + EXPECT_EQ(unregisteredResult.returnCode, QRAFFLE_USER_NOT_FOUND); + EXPECT_EQ(unregisteredResult.entryAmount, 0); +} + +TEST(ContractQraffle, GetQuRaffleEntryAverageAmount) +{ + ContractTestingQraffle qraffle; + + auto users = getRandomUsers(1000, 1000); + uint32 registerCount = 5; + uint32 entrySubmittedCount = 0; + + // Register users first + for (const auto& user : users) + { + increaseEnergy(user, QRAFFLE_REGISTER_AMOUNT); + auto result = qraffle.registerInSystem(user, QRAFFLE_REGISTER_AMOUNT, 0); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + registerCount++; + } + + // Test 1: Query average when no users have submitted entry amounts + auto result = qraffle.getQuRaffleEntryAverageAmount(); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(result.entryAverageAmount, 0); + + // Submit entry amounts for some users + std::vector submittedAmounts; + uint64 totalAmount = 0; + for (size_t i = 0; i < users.size() / 2; ++i) + { + uint64 amount = random(1000000, 1000000000); + increaseEnergy(users[i], amount); + auto result = qraffle.submitEntryAmount(users[i], amount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + submittedAmounts.push_back(amount); + totalAmount += amount; + entrySubmittedCount++; + } + + // Test 2: Query average with submitted amounts + auto averageResult = qraffle.getQuRaffleEntryAverageAmount(); + EXPECT_EQ(averageResult.returnCode, QRAFFLE_SUCCESS); + + // Calculate expected average + uint64 expectedAverage = 0; + if (submittedAmounts.size() > 0) + { + expectedAverage = totalAmount / submittedAmounts.size(); + } + EXPECT_EQ(averageResult.entryAverageAmount, expectedAverage); + + // Test 3: Add more users and verify average updates + std::vector additionalAmounts; + uint64 additionalTotal = 0; + for (size_t i = users.size() / 2; i < users.size(); ++i) + { + uint64 amount = random(1000000, 1000000000); + auto result = qraffle.submitEntryAmount(users[i], amount); + EXPECT_EQ(result.returnCode, QRAFFLE_SUCCESS); + additionalAmounts.push_back(amount); + additionalTotal += amount; + entrySubmittedCount++; + } + + // Calculate new expected average + uint64 newTotalAmount = totalAmount + additionalTotal; + uint64 newExpectedAverage = newTotalAmount / (submittedAmounts.size() + additionalAmounts.size()); + + auto updatedAverageResult = qraffle.getQuRaffleEntryAverageAmount(); + EXPECT_EQ(updatedAverageResult.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(updatedAverageResult.entryAverageAmount, newExpectedAverage); + + // Test 4: Update existing user's entry amount and verify average + uint64 updatedAmount = random(1000000, 1000000000); + auto updateResult = qraffle.submitEntryAmount(users[0], updatedAmount); + EXPECT_EQ(updateResult.returnCode, QRAFFLE_SUCCESS); + + // Recalculate expected average with updated amount + uint64 recalculatedTotal = newTotalAmount - submittedAmounts[0] + updatedAmount; + uint64 recalculatedAverage = recalculatedTotal / (submittedAmounts.size() + additionalAmounts.size()); + + auto recalculatedAverageResult = qraffle.getQuRaffleEntryAverageAmount(); + EXPECT_EQ(recalculatedAverageResult.returnCode, QRAFFLE_SUCCESS); + EXPECT_EQ(recalculatedAverageResult.entryAverageAmount, recalculatedAverage); +} \ No newline at end of file diff --git a/test/contract_qrwa.cpp b/test/contract_qrwa.cpp new file mode 100644 index 000000000..374075951 --- /dev/null +++ b/test/contract_qrwa.cpp @@ -0,0 +1,1750 @@ +#define NO_UEFI + +#include "contract_testing.h" +#include "test_util.h" + +#define ENABLE_BALANCE_DEBUG 0 + +// Pseudo IDs (for testing only) + +// QMINE_ISSUER is is also the ADMIN_ADDRESS +static const id QMINE_ISSUER = ID( + _Q, _M, _I, _N, _E, _Q, _Q, _X, _Y, _B, _E, _G, _B, _H, _N, _S, + _U, _P, _O, _U, _Y, _D, _I, _Q, _K, _Z, _P, _C, _B, _P, _Q, _I, + _I, _H, _U, _U, _Z, _M, _C, _P, _L, _B, _P, _C, _C, _A, _I, _A, + _R, _V, _Z, _B, _T, _Y, _K, _G +); +static const id ADMIN_ADDRESS = QMINE_ISSUER; + +// temporary holder for the initial 150M QMINE supply +static const id TREASURY_HOLDER = id::randomValue(); + +// Addresses for governance-set fees +static const id FEE_ADDR_E = id::randomValue(); // Electricity fees address +static const id FEE_ADDR_M = id::randomValue(); // Maintenance fees address +static const id FEE_ADDR_R = id::randomValue(); // Reinvestment fees address + +// pseudo test address for QMINE developer +static const id QMINE_DEV_ADDR_TEST = ID( + _Z, _O, _X, _X, _I, _D, _C, _Z, _I, _M, _G, _C, _E, _C, _C, _F, + _A, _X, _D, _D, _C, _M, _B, _B, _X, _C, _D, _A, _Q, _J, _I, _H, + _G, _O, _O, _A, _T, _A, _F, _P, _S, _B, _F, _I, _O, _F, _O, _Y, + _E, _C, _F, _K, _U, _F, _P, _B +); + +// Test accounts for holders and users +static const id HOLDER_A = id::randomValue(); +static const id HOLDER_B = id::randomValue(); +static const id HOLDER_C = id::randomValue(); +static const id USER_D = id::randomValue(); // no-share user +static const id DESTINATION_ADDR = id::randomValue(); // dest for asset releases + +// Test QMINE Asset (using the random issuer for testing only) +static const Asset QMINE_ASSET = { QMINE_ISSUER, 297666170193ULL }; + +// Fees for dependent contracts +static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ull; +static constexpr uint64 QX_TRANSFER_FEE = 100ull; // Fee for transfering assets back to QX +static constexpr uint64 QX_MGT_TRANSFER_FEE = 0ull; // Fee for QX::TransferShareManagementRights +static constexpr sint64 QUTIL_STM1_FEE = 10LL; // QUTIL SendToManyV1 fee (QUTIL_STM1_INVOCATION_FEE) + + +enum qRWAFunctionIds +{ + QRWA_FUNC_GET_GOV_PARAMS = 1, + QRWA_FUNC_GET_GOV_POLL = 2, + QRWA_FUNC_GET_ASSET_RELEASE_POLL = 3, + QRWA_FUNC_GET_TREASURY_BALANCE = 4, + QRWA_FUNC_GET_DIVIDEND_BALANCES = 5, + QRWA_FUNC_GET_TOTAL_DISTRIBUTED = 6 +}; + +enum qRWAProcedureIds +{ + QRWA_PROC_DONATE_TO_TREASURY = 3, + QRWA_PROC_VOTE_GOV_PARAMS = 4, + QRWA_PROC_CREATE_ASSET_RELEASE_POLL = 5, + QRWA_PROC_VOTE_ASSET_RELEASE = 6, + QRWA_PROC_DEPOSIT_GENERAL_ASSET = 7, + QRWA_PROC_REVOKE_ASSET = 8, +}; + +enum QxProcedureIds +{ + QX_PROC_ISSUE_ASSET = 1, + QX_PROC_TRANSFER_SHARES = 2, + QX_PROC_TRANSFER_MANAGEMENT = 9 +}; + +enum QutilProcedureIds +{ + QUTIL_PROC_SEND_TO_MANY_V1 = 1 +}; + + +class ContractTestingQRWA : protected ContractTesting +{ + // Grant access to protected/private members for setup + friend struct QRWA; + +public: + ContractTestingQRWA() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QRWA); + callSystemProcedure(QRWA_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QUTIL); + callSystemProcedure(QUTIL_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QSWAP); + callSystemProcedure(QSWAP_CONTRACT_INDEX, INITIALIZE); + + // Custom Initialization for qRWA State + // (Overrides defaults from INITIALIZE() for testing purposes) + QRWA* state = getState(); + + // Fee addresses + // Note: We want to check these Fee Addresses separately, + // we use different addresses instead of same address as the Admin Address + state->mCurrentGovParams.electricityAddress = FEE_ADDR_E; + state->mCurrentGovParams.maintenanceAddress = FEE_ADDR_M; + state->mCurrentGovParams.reinvestmentAddress = FEE_ADDR_R; + } + + QRWA* getState() + { + return (QRWA*)contractStates[QRWA_CONTRACT_INDEX]; + } + + void beginEpoch(bool expectSuccess = true) + { + callSystemProcedure(QRWA_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QRWA_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + void endTick(bool expectSuccess = true) + { + callSystemProcedure(QRWA_CONTRACT_INDEX, END_TICK, expectSuccess); + } + + // manually reset the last payout time for testing. + void resetPayoutTime() + { + getState()->mLastPayoutTime = { 0, 0, 0, 0, 0, 0, 0 }; + } + + // QX/QUTIL Contract Wrappers + + void issueAsset(const id& issuer, uint64 assetName, sint64 shares) + { + QX::IssueAsset_input input{ assetName, shares, 0, 0 }; + QX::IssueAsset_output output; + increaseEnergy(issuer, QX_ISSUE_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, QX_PROC_ISSUE_ASSET, input, output, issuer, QX_ISSUE_ASSET_FEE); + } + + // Transfers asset ownership and possession on QX. + void transferAsset(const id& from, const id& to, const Asset& asset, sint64 shares) + { + QX::TransferShareOwnershipAndPossession_input input{ asset.issuer, to, asset.assetName, shares }; + QX::TransferShareOwnershipAndPossession_output output; + increaseEnergy(from, QX_TRANSFER_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, QX_PROC_TRANSFER_SHARES, input, output, from, QX_TRANSFER_FEE); + } + + // Transfers management rights of an asset to another contract + void transferManagementRights(const id& from, const Asset& asset, sint64 shares, uint32 toContract) + { + QX::TransferShareManagementRights_input input{ asset, shares, toContract }; + QX::TransferShareManagementRights_output output; + increaseEnergy(from, QX_MGT_TRANSFER_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, QX_PROC_TRANSFER_MANAGEMENT, input, output, from, QX_MGT_TRANSFER_FEE); + } + + // Simulates a dividend payout from QLI pool using QUTIL::SendToManyV1. + void sendToMany(const id& from, const id& to, sint64 amount) + { + QUTIL::SendToManyV1_input input = {}; + input.dst0 = to; + input.amt0 = amount; + QUTIL::SendToManyV1_output output; + increaseEnergy(from, amount + QUTIL_STM1_FEE); + invokeUserProcedure(QUTIL_CONTRACT_INDEX, QUTIL_PROC_SEND_TO_MANY_V1, input, output, from, amount + QUTIL_STM1_FEE); + } + + // QRWA Procedure Wrappers + + uint64 donateToTreasury(const id& from, uint64 amount) + { + QRWA::DonateToTreasury_input input{ amount }; + QRWA::DonateToTreasury_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_DONATE_TO_TREASURY, input, output, from, 0); + return output.status; + } + + uint64 voteGovParams(const id& from, const QRWA::QRWAGovParams& params) + { + QRWA::VoteGovParams_input input{ params }; + QRWA::VoteGovParams_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_VOTE_GOV_PARAMS, input, output, from, 0); + return output.status; + } + + QRWA::CreateAssetReleasePoll_output createAssetReleasePoll(const id& from, const QRWA::CreateAssetReleasePoll_input& input) + { + QRWA::CreateAssetReleasePoll_output output; + memset(&output, 0, sizeof(output)); + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_CREATE_ASSET_RELEASE_POLL, input, output, from, 0); + return output; + } + + uint64 voteAssetRelease(const id& from, uint64 pollId, uint64 option) + { + QRWA::VoteAssetRelease_input input{ pollId, option }; + QRWA::VoteAssetRelease_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_VOTE_ASSET_RELEASE, input, output, from, 0); + return output.status; + } + + uint64 depositGeneralAsset(const id& from, const Asset& asset, uint64 amount) + { + QRWA::DepositGeneralAsset_input input{ asset, amount }; + QRWA::DepositGeneralAsset_output output; + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_DEPOSIT_GENERAL_ASSET, input, output, from, 0); + return output.status; + } + + QRWA::RevokeAssetManagementRights_output revokeAssetManagementRights(const id& from, const Asset& asset, sint64 numberOfShares) + { + QRWA::RevokeAssetManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + + QRWA::RevokeAssetManagementRights_output output; + memset(&output, 0, sizeof(output)); + + invokeUserProcedure(QRWA_CONTRACT_INDEX, QRWA_PROC_REVOKE_ASSET, input, output, from, QRWA_RELEASE_MANAGEMENT_FEE); + return output; + } + + // QRWA Wrappers + + QRWA::QRWAGovParams getGovParams() + { + QRWA::GetGovParams_input input; + QRWA::GetGovParams_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_GOV_PARAMS, input, output); + return output.params; + } + + QRWA::GetGovPoll_output getGovPoll(uint64 pollId) + { + QRWA::GetGovPoll_input input{ pollId }; + QRWA::GetGovPoll_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_GOV_POLL, input, output); + return output; + } + + QRWA::GetAssetReleasePoll_output getAssetReleasePoll(uint64 pollId) + { + QRWA::GetAssetReleasePoll_input input{ pollId }; + QRWA::GetAssetReleasePoll_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_ASSET_RELEASE_POLL, input, output); + return output; + } + + uint64 getTreasuryBalance() + { + QRWA::GetTreasuryBalance_input input; + QRWA::GetTreasuryBalance_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_TREASURY_BALANCE, input, output); + return output.balance; + } + + QRWA::GetDividendBalances_output getDividendBalances() + { + QRWA::GetDividendBalances_input input; + QRWA::GetDividendBalances_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_DIVIDEND_BALANCES, input, output); + return output; + } + + QRWA::GetTotalDistributed_output getTotalDistributed() + { + QRWA::GetTotalDistributed_input input; + QRWA::GetTotalDistributed_output output; + callFunction(QRWA_CONTRACT_INDEX, QRWA_FUNC_GET_TOTAL_DISTRIBUTED, input, output); + return output; + } + +}; + + +TEST(ContractQRWA, Initialization) +{ + ContractTestingQRWA qrwa; + + // Check gov params (set in test constructor) + auto params = qrwa.getGovParams(); + EXPECT_EQ(params.mAdminAddress, ADMIN_ADDRESS); + EXPECT_EQ(params.qmineDevAddress, QMINE_DEV_ADDR_TEST); + EXPECT_EQ(params.electricityAddress, FEE_ADDR_E); + EXPECT_EQ(params.maintenanceAddress, FEE_ADDR_M); + EXPECT_EQ(params.reinvestmentAddress, FEE_ADDR_R); + EXPECT_EQ(params.electricityPercent, 350); + EXPECT_EQ(params.maintenancePercent, 50); + EXPECT_EQ(params.reinvestmentPercent, 100); + + // Check pools and balances via public functions + EXPECT_EQ(qrwa.getTreasuryBalance(), 0); + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + EXPECT_EQ(divBalances.revenuePoolB, 0); + EXPECT_EQ(divBalances.qmineDividendPool, 0); + EXPECT_EQ(divBalances.qrwaDividendPool, 0); + + auto distTotals = qrwa.getTotalDistributed(); + EXPECT_EQ(distTotals.totalQmineDistributed, 0); + EXPECT_EQ(distTotals.totalQRWADistributed, 0); +} + + +TEST(ContractQRWA, RevenueAccounting_POST_INCOMING_TRANSFER) +{ + ContractTestingQRWA qrwa; + + // Pool A from SC QUTIL + // We simulate this by calling QUTIL's SendToMany + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 1000000); + // We cannot test pool B as the test environment does not support standard transfer + // as noted in contract_testex.cpp + EXPECT_EQ(divBalances.revenuePoolB, 0); +} + +TEST(ContractQRWA, Governance_VoteGovParams_And_EndEpochCount) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 400000); // 40% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 400000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 600000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // 30% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 300000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 200000); // 20% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 200000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 100000); + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + qrwa.beginEpoch(); + // Quorum should be 2/3 of 900,000 = 600,000 + + // Not a holder + EXPECT_EQ(qrwa.voteGovParams(USER_D, {}), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Invalid params (Admin NULL_ID) + QRWA::QRWAGovParams invalidParams = qrwa.getGovParams(); + invalidParams.mAdminAddress = NULL_ID; + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, invalidParams), QRWA_STATUS_FAILURE_INVALID_INPUT); + + // Create new poll and vote for it + QRWA::QRWAGovParams paramsA = qrwa.getGovParams(); + paramsA.electricityPercent = 100; // Change one param + + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, paramsA), QRWA_STATUS_SUCCESS); // Poll 0 + EXPECT_EQ(qrwa.voteGovParams(HOLDER_B, paramsA), QRWA_STATUS_SUCCESS); // Vote for Poll 0 + + // Change vote + QRWA::QRWAGovParams paramsB = qrwa.getGovParams(); + paramsB.maintenancePercent = 100; // Change another param + + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, paramsB), QRWA_STATUS_SUCCESS); // Poll 1 + + // Mid-epoch sale + qrwa.transferAsset(HOLDER_B, USER_D, QMINE_ASSET, 150000); // B's balance is now 150k + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 150000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 150000); + + + // Accountant at END_EPOCH + qrwa.endEpoch(); + + // Check results: + // Poll 0 (ParamsA): HOLDER_B voted. Begin=300k, End=150k. VotingPower = 150k. + // Poll 1 (ParamsB): HOLDER_A voted. Begin=400k, End=400k. VotingPower = 400k. + // Total power = 900k. Quorum = 600k. + + // Poll 0 (ParamsA) failed. + auto poll0 = qrwa.getGovPoll(0); + EXPECT_EQ(poll0.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll0.proposal.score, 150000); + EXPECT_EQ(poll0.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + + // Poll 1 (ParamsB) failed. + auto poll1 = qrwa.getGovPoll(1); + EXPECT_EQ(poll1.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll1.proposal.score, 400000); + EXPECT_EQ(poll1.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + + // Params should be unchanged (still 50 from init) + EXPECT_EQ(qrwa.getGovParams().maintenancePercent, 50); + + // New Epoch: Test successful vote + qrwa.beginEpoch(); // New snapshot total: A(400k) + B(150k) + C(200k) = 750k. Quorum = 500k. + + // All holders vote for ParamsB + EXPECT_EQ(qrwa.voteGovParams(HOLDER_A, paramsB), QRWA_STATUS_SUCCESS); // Creates Poll 2 + EXPECT_EQ(qrwa.voteGovParams(HOLDER_B, paramsB), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(HOLDER_C, paramsB), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Check results: + // Poll 2 (ParamsB): A(400k) + B(150k) + C(200k) = 750k vote power. + // Vote passes. + auto poll2 = qrwa.getGovPoll(2); + EXPECT_EQ(poll2.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll2.proposal.score, 750000); + EXPECT_EQ(poll2.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); + + // Verify params were updated + EXPECT_EQ(qrwa.getGovParams().maintenancePercent, 100); +} + +TEST(ContractQRWA, Governance_AssetReleasePolls) +{ + ContractTestingQRWA qrwa; + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(USER_D, 1000000); + increaseEnergy(DESTINATION_ADDR, 1000000); + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000 + 1000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1001000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 700000); // 70% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 700000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 301000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // 30% + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000); + // QMINE_ISSUER (ADMIN_ADDRESS) now holds 1000 + + // Give SC 1000 QMINE for its treasury + qrwa.transferManagementRights(QMINE_ISSUER, QMINE_ASSET, 1000, QRWA_CONTRACT_INDEX); + EXPECT_EQ(qrwa.donateToTreasury(QMINE_ISSUER, 1000), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.getTreasuryBalance(), 1000); + + qrwa.beginEpoch(); + + // Not Admin + QRWA::CreateAssetReleasePoll_input pollInput = {}; + pollInput.proposalName = id::randomValue(); + pollInput.asset = QMINE_ASSET; + pollInput.amount = 100; + pollInput.destination = DESTINATION_ADDR; + + auto pollOut = qrwa.createAssetReleasePoll(HOLDER_A, pollInput); // HOLDER_A is not admin + EXPECT_EQ(pollOut.status, QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Admin creates poll + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + EXPECT_EQ(pollOut.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(pollOut.proposalId, 0); + + // Not a holder + EXPECT_EQ(qrwa.voteAssetRelease(USER_D, 0, 1), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Holders vote + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, 0, 1), QRWA_STATUS_SUCCESS); // 700k YES + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_B, 0, 0), QRWA_STATUS_SUCCESS); // 300k NO + + // Add revenue to Pool A so the contract can pay the release fee + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + EXPECT_EQ(qrwa.getDividendBalances().revenuePoolA, 1000000); + + // Count at end epoch (Pass) + qrwa.endEpoch(); + + auto poll = qrwa.getAssetReleasePoll(0); + EXPECT_EQ(poll.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); // Should pass now + EXPECT_EQ(poll.proposal.votesYes, 700000); + EXPECT_EQ(poll.proposal.votesNo, 300000); + + // Verify balances + EXPECT_EQ(qrwa.getTreasuryBalance(), 900); // 1000 - 100 + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 100); // Should be 100 now + + // Count at end epoch (Fail Vote) + qrwa.beginEpoch(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); // Poll 1 + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, 1, 0), QRWA_STATUS_SUCCESS); // 700k NO + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_B, 1, 1), QRWA_STATUS_SUCCESS); // 300k YES + qrwa.endEpoch(); + + poll = qrwa.getAssetReleasePoll(1); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + EXPECT_EQ(qrwa.getTreasuryBalance(), 900); // Unchanged + + // Count at end epoch (Fail Execution - Insufficient) + qrwa.beginEpoch(); + pollInput.amount = 1000; // Try to release 1000 (only 900 left) + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); // Poll 2 + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, 2, 1), QRWA_STATUS_SUCCESS); // 700k YES + qrwa.endEpoch(); + + poll = qrwa.getAssetReleasePoll(2); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION); + EXPECT_EQ(qrwa.getTreasuryBalance(), 900); // Unchanged +} + +TEST(ContractQRWA, Governance_AssetRelease_FailAndRevoke) +{ + ContractTestingQRWA qrwa; + + const sint64 initialEnergy = 1000000000; + increaseEnergy(HOLDER_A, initialEnergy); + increaseEnergy(HOLDER_B, initialEnergy); + increaseEnergy(ADMIN_ADDRESS, initialEnergy + QX_ISSUE_ASSET_FEE); + increaseEnergy(DESTINATION_ADDR, initialEnergy); + + const sint64 treasuryAmount = 1000; + const sint64 voterShares = 1000000; + const sint64 releaseAmount = 500; + + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, voterShares + treasuryAmount); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 700000); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); + + // Give qRWA management rights over the treasury shares + qrwa.transferManagementRights(QMINE_ISSUER, QMINE_ASSET, treasuryAmount, QRWA_CONTRACT_INDEX); + + // Verify management rights were transferred + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QRWA_CONTRACT_INDEX }, + { QMINE_ISSUER, QRWA_CONTRACT_INDEX }), treasuryAmount); + + // Donate the shares to the treasury + EXPECT_EQ(qrwa.donateToTreasury(QMINE_ISSUER, treasuryAmount), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.getTreasuryBalance(), treasuryAmount); + + // Verify Revenue Pool A (for fees) is empty. + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + + qrwa.beginEpoch(); + // Total voting power = 1,000,000 (HOLDER_A + HOLDER_B) + // Quorum = (1,000,000 * 2 / 3) + 1 = 666,667 + + QRWA::CreateAssetReleasePoll_input pollInput = {}; + pollInput.proposalName = id::randomValue(); + pollInput.asset = QMINE_ASSET; + pollInput.amount = releaseAmount; + pollInput.destination = DESTINATION_ADDR; + + // create poll + auto pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + EXPECT_EQ(pollOut.status, QRWA_STATUS_SUCCESS); + uint64 pollId = pollOut.proposalId; + + // HOLDER_A votes YES, passing the poll (700k > 666k quorum) + EXPECT_EQ(qrwa.voteAssetRelease(HOLDER_A, pollId, 1), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Check poll status + // It should have passed the vote but failed execution (due to lack of 100 QUs fee for QX management transfer) + auto poll = qrwa.getAssetReleasePoll(pollId); + EXPECT_EQ(poll.proposal.status, QRWA_POLL_STATUS_PASSED_FAILED_EXECUTION); + EXPECT_EQ(poll.proposal.votesYes, 700000); + + // Check SC asset state + // Asserts the INTERNAL counter is now decreased + EXPECT_EQ(qrwa.getTreasuryBalance(), treasuryAmount - releaseAmount); // 1000 - 500 = 500 + + // the SC balance is decreased + sint64 scOwnedBalance = numberOfShares(QMINE_ASSET, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }); + EXPECT_EQ(scOwnedBalance, treasuryAmount - releaseAmount); // 1000 - 500 = 500 + + // DESTINATION_ADDR should now owns the shares, but they are MANAGED by qRWA + sint64 destManagedByQrwa = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQrwa, releaseAmount); // 500 shares are stuck + + // DESTINATION_ADDR should have 0 shares managed by QX + sint64 destManagedByQx = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQx, 0); + + // Test Revoke + qrwa.beginEpoch(); + + // Fund DESTINATION_ADDR with the fee for the revoke procedure + increaseEnergy(DESTINATION_ADDR, QRWA_RELEASE_MANAGEMENT_FEE); + sint64 destBalanceBeforeRevoke = getBalance(DESTINATION_ADDR); + + // DESTINATION_ADDR calls revokeAssetManagementRights + auto revokeOut = qrwa.revokeAssetManagementRights(DESTINATION_ADDR, QMINE_ASSET, releaseAmount); + + // check the outcome + EXPECT_EQ(revokeOut.status, QRWA_STATUS_SUCCESS); + EXPECT_EQ(revokeOut.transferredNumberOfShares, releaseAmount); + + // check final on-chain asset state + // DESTINATION_ADDR should be no longer have shares managed by qRWA + destManagedByQrwa = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }, + { DESTINATION_ADDR, QRWA_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQrwa, 0); + + // DESTINATION_ADDR's shares should now be managed by QX + destManagedByQx = numberOfShares(QMINE_ASSET, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }, + { DESTINATION_ADDR, QX_CONTRACT_INDEX }); + EXPECT_EQ(destManagedByQx, releaseAmount); + + // check if the fee was paid by the user + sint64 destBalanceAfterRevoke = getBalance(DESTINATION_ADDR); + EXPECT_EQ(destBalanceAfterRevoke, destBalanceBeforeRevoke - QRWA_RELEASE_MANAGEMENT_FEE); + + // Critical check: + // Verify that the fee sent to the SC was NOT permanently added to Pool B. + // The POST_INCOMING_TRANSFER adds 100 QU to Pool B. + // The procedure executes, spends 100 QU to QX, and logic must subtract 100 from Pool B. + // Net result for Pool B must be 0. + auto finalDivBalances = qrwa.getDividendBalances(); + EXPECT_EQ(finalDivBalances.revenuePoolB, 0); +} + +TEST(ContractQRWA, Treasury_Donation) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE to the temporary treasury holder + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 150000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 150000000); + + qrwa.transferAsset(QMINE_ISSUER, TREASURY_HOLDER, QMINE_ASSET, 150000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { TREASURY_HOLDER, QX_CONTRACT_INDEX }, + { TREASURY_HOLDER, QX_CONTRACT_INDEX }), 150000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 0); + + increaseEnergy(TREASURY_HOLDER, 1000000); + + // Fail (No Management Rights) + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, 1000), QRWA_STATUS_FAILURE_INSUFFICIENT_BALANCE); + + // Success (With Management Rights) + // Give SC management rights + qrwa.transferManagementRights(TREASURY_HOLDER, QMINE_ASSET, 150000000, QRWA_CONTRACT_INDEX); + + // Verify rights + sint64 managedBalance = numberOfShares(QMINE_ASSET, + { TREASURY_HOLDER, QRWA_CONTRACT_INDEX }, + { TREASURY_HOLDER, QRWA_CONTRACT_INDEX }); + EXPECT_EQ(managedBalance, 150000000); + + // Donate + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, 150000000), QRWA_STATUS_SUCCESS); + + // Verify treasury balance in SC + EXPECT_EQ(qrwa.getTreasuryBalance(), 150000000); + + // Verify SC now owns the shares + sint64 scOwnedBalance = numberOfShares(QMINE_ASSET, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }, + { id(QRWA_CONTRACT_INDEX, 0, 0, 0), QRWA_CONTRACT_INDEX }); + EXPECT_EQ(scOwnedBalance, 150000000); +} + +TEST(ContractQRWA, Payout_FullDistribution) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000000); + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 200000); // Holder A + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 200000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 800000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // Holder B + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 500000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 100000); // Holder C + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 100000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 400000); + + qrwa.beginEpoch(); + // mTotalQmineBeginEpoch = 1,000,000 + + // Mid-epoch transfers + qrwa.transferAsset(HOLDER_A, USER_D, QMINE_ASSET, 50000); // Holder A ends with 150k + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 50000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 150000); + + qrwa.transferAsset(HOLDER_C, USER_D, QMINE_ASSET, 100000); // Holder C ends with 0 + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 150000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 0); + + // Deposit revenue + // Pool A (from SC) + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + // Pool B (from User) - Untestable. We will proceed using only Pool A. + + qrwa.endEpoch(); + + // Set time to payout day + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 7; // A Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + + // Use helper to reset payout time + qrwa.resetPayoutTime(); // Reset time to allow payout + + // Call END_TICK to trigger DistributeRewards + qrwa.endTick(); + + // Verification + // Fees: Pool A = 1M + // Elec (35%) = 350,000 + // Maint (5%) = 50,000 + // Reinv (10%) = 100,000 + // Total Fees = 500,000 + EXPECT_EQ(getBalance(FEE_ADDR_E), 350000); + EXPECT_EQ(getBalance(FEE_ADDR_M), 50000); + EXPECT_EQ(getBalance(FEE_ADDR_R), 100000); + + // Distribution Pool + // Y_revenue = 1,000,000 - 500,000 = 500,000 + // totalDistribution = 500,000 (Y) + 0 (B) = 500,000 + // mQmineDividendPool = 500k * 90% = 450,000 + // mQRWADividendPool = 500k * 10% = 50,000 + + // qRWA Payout (50,000 QUs) + uint64 qrwaPerShare = 50000 / NUMBER_OF_COMPUTORS; // 73 + auto distTotals = qrwa.getTotalDistributed(); + EXPECT_EQ(distTotals.totalQRWADistributed, qrwaPerShare * NUMBER_OF_COMPUTORS); // 73 * 676 = 49328 + + // QMINE Payout (450,000 QUs) + // mPayoutTotalQmineBegin = 1,000,000 + + // Eligible Balances: + // H1: min(200k, 150k) = 150,000 + // H2: min(300k, 300k) = 300,000 + // H3: min(100k, 0) = 0 + // Issuer: min(400k, 400k) = 400,000 + // Total Eligible = 850,000 + + // Payouts: + // H1 Payout: (150,000 * 450,000) / 1,000,000 = 67,500 + // H2 Payout: (300,000 * 450,000) / 1,000,000 = 135,000 + // H3 Payout: 0 + // Issuer Payout: (400,000 * 450,000) / 1,000,000 = 180,000 + // Total Eligible Paid = 67,500 + 135,000 + 180,000 = 382,500 + // QMINE_DEV Payout (Remainder) = 450,000 - 382,500 = 67,500 + + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 67500); + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 135000); + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 0); + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 67500); + + // Re-check balances + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 135000); + + // Check pools are empty (or contain only dust from integer division) + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + EXPECT_EQ(divBalances.revenuePoolB, 0); + EXPECT_EQ(divBalances.qmineDividendPool, 0); + EXPECT_EQ(divBalances.qrwaDividendPool, 50000 - (qrwaPerShare * NUMBER_OF_COMPUTORS)); // Dust +} + +TEST(ContractQRWA, Payout_SnapshotLogic) +{ + ContractTestingQRWA qrwa; + + // Give energy to all participants + increaseEnergy(QMINE_ISSUER, 1000000000); + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + // Issue 3500 QMINE + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 3500); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, { QMINE_ISSUER, QX_CONTRACT_INDEX }), 3500); + + // Epoch 1 Setup: Distribute initial shares + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 1000); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 1000); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 1000); + // QMINE_ISSUER keeps 500 + + qrwa.beginEpoch(); + // Snapshot (Begin Epoch 1): + // Total: 3500 (A, B, C, Issuer) + // A: 1000 + // B: 1000 + // C: 1000 + // D: 0 + // Issuer: 500 + + // Epoch 1 Mid-Epoch Transfers + qrwa.transferAsset(HOLDER_A, USER_D, QMINE_ASSET, 500); // A: 500, D: 500 + qrwa.transferAsset(HOLDER_B, USER_D, QMINE_ASSET, 1000); // B: 0, D: 1500 + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 500); // C: 1500, Issuer: 0 + + // Deposit 1M QUs into Pool A + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + qrwa.endEpoch(); + // Payout Snapshots (Epoch 1): + // mPayoutTotalQmineBegin: 3500 + // Eligible: + // A: min(1000, 500) = 500 + // B: min(1000, 0) = 0 + // C: min(1000, 1500) = 1000 + // D: (not in begin map) = 0 + // Issuer: min(500, 0) = 0 + // Total Eligible: 1500 + + // Payout Calculation (Epoch 1): + // Pool A: 1,000,000 -> Fees (50%) = 500,000 -> Y_revenue = 500,000 + // mQmineDividendPool (90%): 450,000 + // mQRWADividendPool (10%): 50,000 + + // Payouts: + // A: (500 * 450,000) / 3,500 = 64,285 + // B: 0 + // C: (1000 * 450,000) / 3,500 = 128,571 + // D: 0 + // Issuer: 0 + // totalEligiblePaid = 192,856 + // movedSharesPayout (QMINE_DEV) = 450,000 - 192,856 = 257,144 + + // Trigger Payout + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 14; // Next Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + qrwa.resetPayoutTime(); + qrwa.endTick(); + + // Verify Payout 1 + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 64285); + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 0); + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 128571); + EXPECT_EQ(getBalance(USER_D), 1000000 + 0); + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 257144); + + // Check C's balance again + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 128571); + + + // Epoch 2 + qrwa.beginEpoch(); + // Snapshot (Begin Epoch 2): + // Total: 3500 + // A: 500, B: 0, C: 1500, D: 1500, Issuer: 0 + + // Epoch 2 Mid-Epoch Transfers + qrwa.transferAsset(USER_D, HOLDER_A, QMINE_ASSET, 500); // A: 1000, D: 1000 + qrwa.transferAsset(HOLDER_C, HOLDER_B, QMINE_ASSET, 1000); // C: 500, B: 1000 + + // Deposit 1M QUs into Pool A + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + qrwa.endEpoch(); + // Snapshot (End Epoch 2): + // A: 1000, B: 1000, C: 500, D: 1000, Issuer: 0 + // + // Payout Snapshots (Epoch 2): + // mPayoutTotalQmineBegin: 3500 + // Eligible: + // A: min(500, 1000) = 500 + // B: min(0, 1000) = 0 + // C: min(1500, 500) = 500 + // D: min(1500, 1000) = 1000 + // Total Eligible: 2000 + + // Payout Calculation (Epoch 2): + // Pool A: 1,000,000 -> Fees (50%) = 500,000 -> Y_revenue = 500,000 + // mQmineDividendPool (90%): 450,000 + // Payouts: + // A: (500 * 450,000) / 3,500 = 64,285 + // B: 0 + // C: (500 * 450,000) / 3,500 = 64,285 + // D: (1000 * 450,000) / 3,500 = 128,571 + // totalEligiblePaid = 257,141 + // movedSharesPayout (QMINE_DEV) = 450,000 - 257,141 = 192,859 + + // Trigger Payout 2 + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 21; // Next Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + qrwa.resetPayoutTime(); + qrwa.endTick(); + + // Verify Payout 2 (Cumulative) + // A: Base + payout1 + payout2 + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 64285 + 64285); + // B: Base + payout1 + payout2 + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 0 + 0); + // C: Base + payout1 + payout2 + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 128571 + 64285); + // D: Base + payout1 + payout2 + EXPECT_EQ(getBalance(USER_D), 1000000 + 0 + 128571); + // QMINE dev: payout1 + payout2 + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 257144 + 192859); +} + +TEST(ContractQRWA, Payout_FullDistribution2) +{ + ContractTestingQRWA qrwa; + + // Issue QMINE, distribute, and run BEGIN_EPOCH + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, 1000000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 1000000); + + increaseEnergy(HOLDER_A, 1000000); + increaseEnergy(HOLDER_B, 1000000); + increaseEnergy(HOLDER_C, 1000000); + increaseEnergy(USER_D, 1000000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, 200000); // Holder A + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 200000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 800000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_B, QMINE_ASSET, 300000); // Holder B + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_B, QX_CONTRACT_INDEX }, + { HOLDER_B, QX_CONTRACT_INDEX }), 300000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 500000); + + qrwa.transferAsset(QMINE_ISSUER, HOLDER_C, QMINE_ASSET, 100000); // Holder C + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 100000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { QMINE_ISSUER, QX_CONTRACT_INDEX }, + { QMINE_ISSUER, QX_CONTRACT_INDEX }), 400000); + + qrwa.beginEpoch(); + // mTotalQmineBeginEpoch = 1,000,000 (A:200k, B:300k, C:100k, Issuer:400k) + + // Mid-epoch transfers + qrwa.transferAsset(HOLDER_A, USER_D, QMINE_ASSET, 50000); // Holder A ends with 150k + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 50000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_A, QX_CONTRACT_INDEX }, + { HOLDER_A, QX_CONTRACT_INDEX }), 150000); + + qrwa.transferAsset(HOLDER_C, USER_D, QMINE_ASSET, 100000); // Holder C ends with 0 + EXPECT_EQ(numberOfShares(QMINE_ASSET, { USER_D, QX_CONTRACT_INDEX }, + { USER_D, QX_CONTRACT_INDEX }), 150000); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { HOLDER_C, QX_CONTRACT_INDEX }, + { HOLDER_C, QX_CONTRACT_INDEX }), 0); + + // Deposit revenue + // Pool A (from SC) + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 3000000); // Increased revenue + + // Pool B (from User): Untestable. We will proceed using only Pool A. + + qrwa.endEpoch(); + + // Set time to payout day + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 7; // A Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + + // Use helper to reset payout time + qrwa.resetPayoutTime(); // Reset time to allow payout + + // Call END_TICK to trigger DistributeRewards + qrwa.endTick(); + + // Verification + // Fees: Pool A = 3M + // Elec (35%) = 1,050,000 + // Maint (5%) = 150,000 + // Reinv (10%) = 300,000 + // Total Fees = 1,500,000 + EXPECT_EQ(getBalance(FEE_ADDR_E), 1050000); + EXPECT_EQ(getBalance(FEE_ADDR_M), 150000); + EXPECT_EQ(getBalance(FEE_ADDR_R), 300000); + + // Distribution Pool + // Y_revenue = 3,000,000 - 1,500,000 = 1,500,000 + // totalDistribution = 1,500,000 (Y) + 0 (B) = 1,500,000 + // mQmineDividendPool = 1.5M * 90% = 1,350,000 + // mQRWADividendPool = 1.5M * 10% = 150,000 + + // qRWA Payout (150,000 QUs) + uint64 qrwaPerShare = 150000 / NUMBER_OF_COMPUTORS; // 150000 / 676 = 221 + auto distTotals = qrwa.getTotalDistributed(); + EXPECT_EQ(distTotals.totalQRWADistributed, qrwaPerShare * NUMBER_OF_COMPUTORS); // 221 * 676 = 149416 + + // QMINE Payout (1,350,000 QUs) + // mPayoutTotalQmineBegin = 1,000,000 (A:200k, B:300k, C:100k, Issuer:400k) + + // Eligible: + // H1: min(200k, 150k) = 150,000 + // H2: min(300k, 300k) = 300,000 + // H3: min(100k, 0) = 0 + // Issuer: min(400k, 400k) = 400,000 + // Total Eligible = 850,000 + + // Payouts: + // H1 Payout: (150,000 * 1,350,000) / 1,000,000 = 202,500 + // H2 Payout: (300,000 * 1,350,000) / 1,000,000 = 405,000 + // H3 Payout: 0 + // Issuer Payout: (400,000 * 1,350,000) / 1,000,000 = 540,000 + // Total Eligible Paid = 202,500 + 405,000 + 540,000 = 1,147,500 + // QMINE dev Payout (Remainder) = 1,350,000 - 1,147,500 = 202,500 + + EXPECT_EQ(getBalance(HOLDER_A), 1000000 + 202500); + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 405000); + EXPECT_EQ(getBalance(HOLDER_C), 1000000 + 0); + EXPECT_EQ(getBalance(QMINE_DEV_ADDR_TEST), 202500); + + // Re-check B's balance + EXPECT_EQ(getBalance(HOLDER_B), 1000000 + 405000); + + + // Check pools are empty (or contain only dust from integer division) + auto divBalances = qrwa.getDividendBalances(); + EXPECT_EQ(divBalances.revenuePoolA, 0); + EXPECT_EQ(divBalances.revenuePoolB, 0); + EXPECT_EQ(divBalances.qmineDividendPool, 0); // QMINE dev gets the remainder + EXPECT_EQ(divBalances.qrwaDividendPool, 150000 - (qrwaPerShare * NUMBER_OF_COMPUTORS)); // Dust (584) +} + +TEST(ContractQRWA, FullScenario_DividendsAndGovernance) +{ + ContractTestingQRWA qrwa; + + /* --- SETUP --- */ + + etalonTick.year = 25; // 2025 + etalonTick.month = 11; // November + etalonTick.day = 7; // 7th (Friday) + etalonTick.hour = 12; + etalonTick.minute = 1; + etalonTick.second = 0; + etalonTick.millisecond = 0; + + // Helper to handle month rollovers for this test + auto advanceTime7Days = [&]() + { + etalonTick.day += 7; + // Simple logic for Nov/Dec 2025 + if (etalonTick.month == 11 && etalonTick.day > 30) { + etalonTick.day -= 30; + etalonTick.month++; + } + else if (etalonTick.month == 12 && etalonTick.day > 31) { + etalonTick.day -= 31; + etalonTick.month = 1; + etalonTick.year++; + } + }; + + // Constants + const sint64 TOTAL_SUPPLY = 1000000000000LL; // 1,000,000,000,000 = 1 Trillion + const sint64 TREASURY_INIT = 150000000000LL; // 150 Billion + const sint64 SHAREHOLDERS_TOTAL = 850000000000LL; // 850 Billion + const sint64 SHAREHOLDER_AMT = SHAREHOLDERS_TOTAL / 5; // 170 Billion each + const sint64 REVENUE_AMT = 10000000LL; // 10 Million QUs per epoch revenue + + // Known Pool Amounts derived from REVENUE_AMT and 50% total fees + // Revenue 10M -> Fees 5M -> Net 5M + const sint64 QMINE_POOL_AMT = 4500000LL; // 90% of 5M + const sint64 QRWA_POOL_AMT_BASE = 500000LL; // 10% of 5M + + const sint64 QRWA_TOTAL_SHARES = 676LL; + + // Track dust for qRWA pool to calculate accurate rates per epoch + sint64 currentQrwaDust = 0; + sint64 currentQXReleaseFee = 0; + + auto getQrwaRateForEpoch = [&](sint64 poolAmount) -> sint64 { + sint64 totalPool = poolAmount + currentQrwaDust; + sint64 rate = totalPool / QRWA_TOTAL_SHARES; + currentQrwaDust = totalPool % QRWA_TOTAL_SHARES; // Update dust for next epoch + return rate; + }; + + // Entities + const id S1 = id::randomValue(); // Hybrid: Holds QMINE + qRWA shares + const id S2 = id::randomValue(); // Control QMINE: Holds only QMINE + const id S3 = id::randomValue(); // QMINE only + const id S4 = id::randomValue(); // QMINE only + const id S5 = id::randomValue(); // QMINE only + const id Q1 = id::randomValue(); // Control qRWA: Holds only qRWA shares + const id Q2 = id::randomValue(); // qRWA only + + // Energy Funding + increaseEnergy(QMINE_ISSUER, QX_ISSUE_ASSET_FEE * 2 + 100000000); + increaseEnergy(TREASURY_HOLDER, 100000000); + increaseEnergy(S1, 100000000); + increaseEnergy(S2, 100000000); + increaseEnergy(S3, 100000000); + increaseEnergy(S4, 100000000); + increaseEnergy(S5, 100000000); + increaseEnergy(Q1, 100000000); + increaseEnergy(Q2, 100000000); + increaseEnergy(DESTINATION_ADDR, 1000000); + increaseEnergy(ADMIN_ADDRESS, 1000000); + + // Issue QMINE + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, TOTAL_SUPPLY); + + // Distribute to Treasury Holder + qrwa.transferAsset(QMINE_ISSUER, TREASURY_HOLDER, QMINE_ASSET, TREASURY_INIT); + + // Distribute to 5 Shareholders (170B each) + qrwa.transferAsset(QMINE_ISSUER, S1, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S2, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S3, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S4, QMINE_ASSET, SHAREHOLDER_AMT); + qrwa.transferAsset(QMINE_ISSUER, S5, QMINE_ASSET, SHAREHOLDER_AMT); + + // Issue and Distribute qrwa Contract Shares + std::vector> qrwaShares{ + {S1, 200}, + {Q1, 200}, + {Q2, 276} + }; + issueContractShares(QRWA_CONTRACT_INDEX, qrwaShares); + + // Snapshot balances + std::map prevBalances; + auto snapshotBalances = [&]() { + prevBalances[S1] = getBalance(S1); + prevBalances[S2] = getBalance(S2); + prevBalances[S3] = getBalance(S3); + prevBalances[S4] = getBalance(S4); + prevBalances[S5] = getBalance(S5); + prevBalances[Q1] = getBalance(Q1); + prevBalances[Q2] = getBalance(Q2); + prevBalances[DESTINATION_ADDR] = getBalance(DESTINATION_ADDR); + }; + snapshotBalances(); + + // Helper to calculate exact QMINE payout matching contract logic + // Payout = (EligibleBalance * DividendPool) / PayoutBase + auto calculateQminePayout = [&](sint64 balance, sint64 payoutBase, sint64 poolAmount) -> sint64 { + if (payoutBase == 0) return 0; + // Contract uses: div((uint128)balance * pool, totalEligible) + // We mimic that integer math here + uint128 res = (uint128)balance * (uint128)poolAmount; + res = res / (uint128)payoutBase; + return (sint64)res.low; + }; + + // Helper that uses the calculated rate for the current epoch + auto calculateQrwaPayout = [&](sint64 shares, sint64 currentRate) -> sint64 { + return shares * currentRate; + }; + +#if ENABLE_BALANCE_DEBUG + auto print_balances = [&]() + { + std::cout << "\n--- Current Balances ---" << std::endl; + std::cout << "S1: " << getBalance(S1) << std::endl; + std::cout << "S2: " << getBalance(S2) << std::endl; + std::cout << "S3: " << getBalance(S3) << std::endl; + std::cout << "S4: " << getBalance(S4) << std::endl; + std::cout << "S5: " << getBalance(S5) << std::endl; + std::cout << "Q1: " << getBalance(Q1) << std::endl; + std::cout << "Q2: " << getBalance(Q2) << std::endl; + std::cout << "Dest: " << getBalance(DESTINATION_ADDR) << std::endl; + std::cout << "Treasury: " << getBalance(TREASURY_HOLDER) << std::endl; + std::cout << "Dev: " << getBalance(QMINE_DEV_ADDR_TEST) << std::endl; + std::cout << "------------------------\n" << std::endl; + }; + + std::cout << "PRE-EPOCH 1\n"; + print_balances(); +#endif + // epoch 1 + qrwa.beginEpoch(); + + //Shareholders Exchange + qrwa.transferAsset(S1, S2, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S3, S4, QMINE_ASSET, 5000000000LL); + + // Treasury Donation + qrwa.transferManagementRights(TREASURY_HOLDER, QMINE_ASSET, 10, QRWA_CONTRACT_INDEX); + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, 10), QRWA_STATUS_SUCCESS); + + //Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + qrwa.endEpoch(); + + // Checks Ep 1 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << "END-EPOCH 1\n"; + print_balances(); +#endif + + // Contract holds 10 shares. Base = Total Supply - 10 + sint64 payoutBaseEp1 = TOTAL_SUPPLY - 10; + sint64 qrwaRateEp1 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); // Standard pool for Ep 1 + + sint64 divS1 = calculateQminePayout(160000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS2 = calculateQminePayout(170000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS3 = calculateQminePayout(165000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS4 = calculateQminePayout(170000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + sint64 divS5 = calculateQminePayout(170000000000LL, payoutBaseEp1, QMINE_POOL_AMT); + + sint64 divQS1 = calculateQrwaPayout(200, qrwaRateEp1); + sint64 divQQ1 = calculateQrwaPayout(200, qrwaRateEp1); + sint64 divQQ2 = calculateQrwaPayout(276, qrwaRateEp1); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << "PRE-EPOCH 2\n"; + print_balances(); +#endif + + // epoch 2 + qrwa.beginEpoch(); + + // Treasury Donation (Remaining) + sint64 treasuryRemaining = TREASURY_INIT - 10; + qrwa.transferManagementRights(TREASURY_HOLDER, QMINE_ASSET, treasuryRemaining, QRWA_CONTRACT_INDEX); + EXPECT_EQ(qrwa.donateToTreasury(TREASURY_HOLDER, treasuryRemaining), QRWA_STATUS_SUCCESS); + + // Exchange + qrwa.transferAsset(S1, S2, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S2, S3, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S3, S4, QMINE_ASSET, 10000000000LL); + qrwa.transferAsset(S4, S5, QMINE_ASSET, 10000000000LL); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Release Poll + QRWA::CreateAssetReleasePoll_input pollInput; + pollInput.proposalName = id::randomValue(); + pollInput.asset = QMINE_ASSET; + pollInput.amount = 1000; + pollInput.destination = DESTINATION_ADDR; + + auto pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp2 = pollOut.proposalId; + + EXPECT_EQ(qrwa.voteAssetRelease(S1, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S2, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S3, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S4, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S5, pollIdEp2, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(Q1, pollIdEp2, 1), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + qrwa.endEpoch(); + + // Checks Ep 2 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << "END-EPOCH 2\n"; + print_balances(); +#endif + + auto pollResultEp2 = qrwa.getAssetReleasePoll(pollIdEp2); + EXPECT_EQ(pollResultEp2.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 1000); + + // Calculate Pools based on Revenue - 100 QU Fee + sint64 netRevenueEp2 = REVENUE_AMT - 100; + sint64 feeAmtEp2 = (netRevenueEp2 * 500) / 1000; // 50% fees + sint64 distributableEp2 = netRevenueEp2 - feeAmtEp2; + sint64 qminePoolEp2 = (distributableEp2 * 900) / 1000; + sint64 qrwaPoolEp2 = distributableEp2 - qminePoolEp2; + + // Correct Base: TOTAL_SUPPLY - 10 (Shares held by SC at START of epoch) + sint64 payoutBaseEp2 = TOTAL_SUPPLY - 10; + + sint64 qrwaRateEp2 = getQrwaRateForEpoch(qrwaPoolEp2); + + divS1 = calculateQminePayout(150000000000LL, payoutBaseEp2, qminePoolEp2); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp2, qminePoolEp2); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp2, qminePoolEp2); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp2, qminePoolEp2); + divS5 = calculateQminePayout(170000000000LL, payoutBaseEp2, qminePoolEp2); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp2); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp2); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp2); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 3\n"; + print_balances(); +#endif + + // epoch 3 + qrwa.beginEpoch(); + + // Exchange + qrwa.transferAsset(S1, S2, QMINE_ASSET, 5000000000LL); + qrwa.transferAsset(S2, S3, QMINE_ASSET, 5000000000LL); + qrwa.transferAsset(S3, S4, QMINE_ASSET, 5000000000LL); + qrwa.transferAsset(S4, S5, QMINE_ASSET, 5000000000LL); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Release Poll + pollInput.amount = 500; + pollInput.proposalName = id::randomValue(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp3 = pollOut.proposalId; + + EXPECT_EQ(qrwa.voteAssetRelease(S1, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S2, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S3, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S4, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S5, pollIdEp3, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(Q1, pollIdEp3, 1), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + // Gov Vote + QRWA::QRWAGovParams newParams = qrwa.getGovParams(); + newParams.electricityPercent = 300; + newParams.maintenancePercent = 100; + + newParams.mAdminAddress = ADMIN_ADDRESS; + newParams.qmineDevAddress = QMINE_DEV_ADDR_TEST; + newParams.electricityAddress = FEE_ADDR_E; + newParams.maintenanceAddress = FEE_ADDR_M; + newParams.reinvestmentAddress = FEE_ADDR_R; + + EXPECT_EQ(qrwa.voteGovParams(S1, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S2, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S3, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S4, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(S5, newParams), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteGovParams(Q1, newParams), QRWA_STATUS_FAILURE_NOT_AUTHORIZED); + + qrwa.endEpoch(); + + // Checks Ep 3 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 3\n"; + print_balances(); +#endif + + auto pollResultEp3 = qrwa.getAssetReleasePoll(pollIdEp3); + EXPECT_EQ(pollResultEp3.proposal.status, QRWA_POLL_STATUS_PASSED_EXECUTED); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 1000 + 500); + + auto activeParams = qrwa.getGovParams(); + EXPECT_EQ(activeParams.electricityPercent, 300); + EXPECT_EQ(activeParams.maintenancePercent, 100); + + // Calculate Pools based on Revenue - 100 QU Fee + sint64 netRevenueEp3 = REVENUE_AMT - 100; + sint64 feeAmtEp3 = (netRevenueEp3 * 500) / 1000; // 50% fees still (params update next epoch) + sint64 distributableEp3 = netRevenueEp3 - feeAmtEp3; + sint64 qminePoolEp3 = (distributableEp3 * 900) / 1000; + sint64 qrwaPoolEp3 = distributableEp3 - qminePoolEp3; + + // Contract released 1000 + 500. Balance = 150B - 1500. + sint64 payoutBaseEp3 = TOTAL_SUPPLY - (TREASURY_INIT - 1500); + + sint64 qrwaRateEp3 = getQrwaRateForEpoch(qrwaPoolEp3); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp3, qminePoolEp3); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp3, qminePoolEp3); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp3, qminePoolEp3); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp3, qminePoolEp3); + divS5 = calculateQminePayout(180000000000LL, payoutBaseEp3, qminePoolEp3); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp3); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp3); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp3); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 4\n"; + print_balances(); +#endif + + // epoch 4 (no transfers) + qrwa.beginEpoch(); + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + qrwa.endEpoch(); + + // Checks Ep 4 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 4\n"; + print_balances(); +#endif + + // Payout base remains same as previous epoch (no new releases) + sint64 payoutBaseEp4 = payoutBaseEp3; + // Revenue is full 10M (no releases) + sint64 qminePoolEp4 = QMINE_POOL_AMT; + + sint64 qrwaRateEp4 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp4); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp4); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp4); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 5\n"; + print_balances(); +#endif + + // epoch 5 + qrwa.beginEpoch(); + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Release Poll + pollInput.amount = 100; + pollInput.proposalName = id::randomValue(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp5 = pollOut.proposalId; + + // Vote NO (3/5 Majority) + EXPECT_EQ(qrwa.voteAssetRelease(S1, pollIdEp5, 0), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S2, pollIdEp5, 0), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S3, pollIdEp5, 0), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S4, pollIdEp5, 1), QRWA_STATUS_SUCCESS); + EXPECT_EQ(qrwa.voteAssetRelease(S5, pollIdEp5, 1), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Checks Ep 5 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 5\n"; + print_balances(); +#endif + + auto pollResultEp5 = qrwa.getAssetReleasePoll(pollIdEp5); + EXPECT_EQ(pollResultEp5.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + EXPECT_EQ(numberOfShares(QMINE_ASSET, { DESTINATION_ADDR, QX_CONTRACT_INDEX }), 1500); // Unchanged + + // Failed vote = No release = No fee = Full Revenue. Base unchanged. + sint64 qrwaRateEp5 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp5); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp5); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp5); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 6\n"; + print_balances(); +#endif + + // epoch 6 + qrwa.beginEpoch(); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Create Gov Proposal + QRWA::QRWAGovParams failParams = qrwa.getGovParams(); + failParams.reinvestmentPercent = 200; + + // Only S1 votes (< 20% supply). Quorum fail + EXPECT_EQ(qrwa.voteGovParams(S1, failParams), QRWA_STATUS_SUCCESS); + + qrwa.endEpoch(); + + // Checks Ep 6 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 6\n"; + print_balances(); +#endif + + auto paramsEp6 = qrwa.getGovParams(); + EXPECT_EQ(paramsEp6.reinvestmentPercent, 100); + EXPECT_NE(paramsEp6.reinvestmentPercent, 200); + + sint64 qrwaRateEp6 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp6); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp6); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp6); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); + + snapshotBalances(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " PRE-EPOCH 7\n"; + print_balances(); +#endif + + // epoch 7 + qrwa.beginEpoch(); + + // Revenue + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), REVENUE_AMT); + + // Create poll, no votes + pollInput.amount = 100; + pollInput.proposalName = id::randomValue(); + pollOut = qrwa.createAssetReleasePoll(ADMIN_ADDRESS, pollInput); + uint64 pollIdEp7 = pollOut.proposalId; + + qrwa.endEpoch(); + + // Checks Ep 7 + advanceTime7Days(); + qrwa.resetPayoutTime(); + qrwa.endTick(); + +#if ENABLE_BALANCE_DEBUG + std::cout << " END-EPOCH 7\n"; + print_balances(); +#endif + + auto pollResultEp7 = qrwa.getAssetReleasePoll(pollIdEp7); + EXPECT_EQ(pollResultEp7.proposal.status, QRWA_POLL_STATUS_FAILED_VOTE); + + sint64 qrwaRateEp7 = getQrwaRateForEpoch(QRWA_POOL_AMT_BASE); + + divS1 = calculateQminePayout(145000000000LL, payoutBaseEp4, qminePoolEp4); + divS2 = calculateQminePayout(180000000000LL, payoutBaseEp4, qminePoolEp4); + divS3 = calculateQminePayout(165000000000LL, payoutBaseEp4, qminePoolEp4); + divS4 = calculateQminePayout(175000000000LL, payoutBaseEp4, qminePoolEp4); + divS5 = calculateQminePayout(185000000000LL, payoutBaseEp4, qminePoolEp4); + + divQS1 = calculateQrwaPayout(200, qrwaRateEp7); + divQQ1 = calculateQrwaPayout(200, qrwaRateEp7); + divQQ2 = calculateQrwaPayout(276, qrwaRateEp7); + + EXPECT_EQ(getBalance(S1), prevBalances[S1] + divS1 + divQS1); + EXPECT_EQ(getBalance(S2), prevBalances[S2] + divS2); + EXPECT_EQ(getBalance(S3), prevBalances[S3] + divS3); + EXPECT_EQ(getBalance(S4), prevBalances[S4] + divS4); + EXPECT_EQ(getBalance(S5), prevBalances[S5] + divS5); + EXPECT_EQ(getBalance(Q1), prevBalances[Q1] + divQQ1); + EXPECT_EQ(getBalance(Q2), prevBalances[Q2] + divQQ2); +} + +TEST(ContractQRWA, Payout_MultiContractManagement) +{ + ContractTestingQRWA qrwa; + + const sint64 totalShares = 1000000; + const sint64 qxManagedShares = 700000; + const sint64 qswapManagedShares = 300000; // 30% moved to QSWAP management + + // Issue QMINE and give to HOLDER_A + // Initially, all 1M shares are managed by QX (default for transfers via QX) + increaseEnergy(QMINE_ISSUER, 1000000000); + increaseEnergy(HOLDER_A, 1000000); // For fees + + qrwa.issueAsset(QMINE_ISSUER, QMINE_ASSET.assetName, totalShares); + qrwa.transferAsset(QMINE_ISSUER, HOLDER_A, QMINE_ASSET, totalShares); + + // Verify initial state managed by QX + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), totalShares); + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QSWAP_CONTRACT_INDEX, QSWAP_CONTRACT_INDEX), 0); + + // Transfer management rights of 300k shares to QSWAP + // The user (HOLDER_A) remains the Possessor. + qrwa.transferManagementRights(HOLDER_A, QMINE_ASSET, qswapManagedShares, QSWAP_CONTRACT_INDEX); + + // Verify the split in management rights + // 700k should remain under QX + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), qxManagedShares); + // 300k should now be under QSWAP + EXPECT_EQ(numberOfPossessedShares(QMINE_ASSET.assetName, QMINE_ASSET.issuer, HOLDER_A, HOLDER_A, QSWAP_CONTRACT_INDEX, QSWAP_CONTRACT_INDEX), qswapManagedShares); + + qrwa.beginEpoch(); + + // Generate Revenue + // pool A revenue: 1,000,000 QUs + // fees (50%): 500,000 + // net revenue: 500,000 + // QMINE pool (90%): 450,000 + qrwa.sendToMany(ADMIN_ADDRESS, id(QRWA_CONTRACT_INDEX, 0, 0, 0), 1000000); + + qrwa.endEpoch(); + + // trigger Payout + etalonTick.year = 25; etalonTick.month = 11; etalonTick.day = 7; // Friday + etalonTick.hour = 12; etalonTick.minute = 1; etalonTick.second = 0; + qrwa.resetPayoutTime(); + + // snapshot balances for check + sint64 balanceBefore = getBalance(HOLDER_A); + + qrwa.endTick(); + + // Calculate Expected Payout + // Payout = (UserTotalShares * PoolAmount) / TotalSupply + // UserTotalShares = 1,000,000 (regardless of manager) + // PoolAmount = 450,000 + // TotalSupply = 1,000,000 + // Expected = 450,000 + sint64 expectedPayout = (totalShares * 450000) / totalShares; + + sint64 balanceAfter = getBalance(HOLDER_A); + + // If qRWA only counted QX shares, the payout would be (700k/1M * 450k) = 315,000. + // If qRWA counts ALL shares, the payout is 450,000. + EXPECT_EQ(balanceAfter - balanceBefore, expectedPayout); + EXPECT_EQ(balanceAfter - balanceBefore, 450000); +} diff --git a/test/contract_qswap.cpp b/test/contract_qswap.cpp index 4daffc022..1fe164eab 100644 --- a/test/contract_qswap.cpp +++ b/test/contract_qswap.cpp @@ -12,23 +12,24 @@ static const id QSWAP_CONTRACT_ID(QSWAP_CONTRACT_INDEX, 0, 0, 0); //constexpr uint32 SWAP_FEE_IDX = 1; constexpr uint32 GET_POOL_BASIC_STATE_IDX = 2; -constexpr uint32 GET_LIQUDITY_OF_IDX = 3; +constexpr uint32 GET_LIQUIDITY_OF_IDX = 3; constexpr uint32 QUOTE_EXACT_QU_INPUT_IDX = 4; constexpr uint32 QUOTE_EXACT_QU_OUTPUT_IDX = 5; constexpr uint32 QUOTE_EXACT_ASSET_INPUT_IDX = 6; constexpr uint32 QUOTE_EXACT_ASSET_OUTPUT_IDX = 7; -constexpr uint32 TEAM_INFO_IDX = 8; +constexpr uint32 INVEST_REWARDS_INFO_IDX = 8; // constexpr uint32 ISSUE_ASSET_IDX = 1; constexpr uint32 TRANSFER_SHARE_OWNERSHIP_AND_POSSESSION_IDX = 2; constexpr uint32 CREATE_POOL_IDX = 3; -constexpr uint32 ADD_LIQUDITY_IDX = 4; -constexpr uint32 REMOVE_LIQUDITY_IDX = 5; +constexpr uint32 ADD_LIQUIDITY_IDX = 4; +constexpr uint32 REMOVE_LIQUIDITY_IDX = 5; constexpr uint32 SWAP_EXACT_QU_FOR_ASSET_IDX = 6; constexpr uint32 SWAP_QU_FOR_EXACT_ASSET_IDX = 7; constexpr uint32 SWAP_EXACT_ASSET_FOR_QU_IDX = 8; constexpr uint32 SWAP_ASSET_FOR_EXACT_QU_IDX = 9; -constexpr uint32 SET_TEAM_INFO_IDX = 10; +constexpr uint32 SET_INVEST_REWARDS_INFO_IDX = 10; +constexpr uint32 TRANSFER_SHARE_MANAGEMENT_RIGHTS_IDX = 11; class QswapChecker : public QSWAP @@ -62,39 +63,45 @@ class ContractTestingQswap : protected ContractTesting return load(filename, sizeof(QSWAP), contractStates[QSWAP_CONTRACT_INDEX]) == sizeof(QSWAP); } - QSWAP::TeamInfo_output teamInfo(){ - QSWAP::TeamInfo_input input{}; - QSWAP::TeamInfo_output output; - callFunction(QSWAP_CONTRACT_INDEX, TEAM_INFO_IDX, input, output); + QSWAP::InvestRewardsInfo_output investRewardsInfo() + { + QSWAP::InvestRewardsInfo_input input{}; + QSWAP::InvestRewardsInfo_output output; + callFunction(QSWAP_CONTRACT_INDEX, INVEST_REWARDS_INFO_IDX, input, output); return output; } - bool setTeamId(const id& issuer, QSWAP::SetTeamInfo_input input){ - QSWAP::CreatePool_output output; - invokeUserProcedure(QSWAP_CONTRACT_INDEX, SET_TEAM_INFO_IDX, input, output, issuer, 0); + bool setInvestRewardsInfo(const id& issuer, QSWAP::SetInvestRewardsInfo_input input) + { + QSWAP::SetInvestRewardsInfo_output output; + invokeUserProcedure(QSWAP_CONTRACT_INDEX, SET_INVEST_REWARDS_INFO_IDX, input, output, issuer, 0); return output.success; } - sint64 issueAsset(const id& issuer, QSWAP::IssueAsset_input input){ + sint64 issueAsset(const id& issuer, QSWAP::IssueAsset_input input) + { QSWAP::IssueAsset_output output; invokeUserProcedure(QSWAP_CONTRACT_INDEX, ISSUE_ASSET_IDX, input, output, issuer, QSWAP_ISSUE_ASSET_FEE); return output.issuedNumberOfShares; } - sint64 transferAsset(const id& issuer, QSWAP::TransferShareOwnershipAndPossession_input input){ + sint64 transferAsset(const id& issuer, QSWAP::TransferShareOwnershipAndPossession_input input) + { QSWAP::TransferShareOwnershipAndPossession_output output; invokeUserProcedure(QSWAP_CONTRACT_INDEX, TRANSFER_SHARE_OWNERSHIP_AND_POSSESSION_IDX, input, output, issuer, QSWAP_TRANSFER_ASSET_FEE); return output.transferredAmount; } - bool createPool(const id& issuer, uint64 assetName){ + bool createPool(const id& issuer, uint64 assetName) + { QSWAP::CreatePool_input input{issuer, assetName}; QSWAP::CreatePool_output output; invokeUserProcedure(QSWAP_CONTRACT_INDEX, CREATE_POOL_IDX, input, output, issuer, QSWAP_CREATE_POOL_FEE); return output.success; } - QSWAP::GetPoolBasicState_output getPoolBasicState(const id& issuer, uint64 assetName){ + QSWAP::GetPoolBasicState_output getPoolBasicState(const id& issuer, uint64 assetName) + { QSWAP::GetPoolBasicState_input input{issuer, assetName}; QSWAP::GetPoolBasicState_output output; @@ -102,11 +109,12 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::AddLiqudity_output addLiqudity(const id& issuer, QSWAP::AddLiqudity_input input, uint64 inputValue){ - QSWAP::AddLiqudity_output output; + QSWAP::AddLiquidity_output addLiquidity(const id& issuer, QSWAP::AddLiquidity_input input, uint64 inputValue) + { + QSWAP::AddLiquidity_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, - ADD_LIQUDITY_IDX, + ADD_LIQUIDITY_IDX, input, output, issuer, @@ -115,11 +123,12 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::RemoveLiqudity_output removeLiqudity(const id& issuer, QSWAP::RemoveLiqudity_input input, uint64 inputValue){ - QSWAP::RemoveLiqudity_output output; + QSWAP::RemoveLiquidity_output removeLiquidity(const id& issuer, QSWAP::RemoveLiquidity_input input, uint64 inputValue) + { + QSWAP::RemoveLiquidity_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, - REMOVE_LIQUDITY_IDX, + REMOVE_LIQUIDITY_IDX, input, output, issuer, @@ -128,13 +137,15 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::GetLiqudityOf_output getLiqudityOf(QSWAP::GetLiqudityOf_input input){ - QSWAP::GetLiqudityOf_output output; - callFunction(QSWAP_CONTRACT_INDEX, GET_LIQUDITY_OF_IDX, input, output); + QSWAP::GetLiquidityOf_output getLiquidityOf(QSWAP::GetLiquidityOf_input input) + { + QSWAP::GetLiquidityOf_output output; + callFunction(QSWAP_CONTRACT_INDEX, GET_LIQUIDITY_OF_IDX, input, output); return output; } - QSWAP::SwapExactQuForAsset_output swapExactQuForAsset( const id& issuer, QSWAP::SwapExactQuForAsset_input input, uint64 inputValue) { + QSWAP::SwapExactQuForAsset_output swapExactQuForAsset( const id& issuer, QSWAP::SwapExactQuForAsset_input input, uint64 inputValue) + { QSWAP::SwapExactQuForAsset_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -148,7 +159,8 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::SwapQuForExactAsset_output swapQuForExactAsset( const id& issuer, QSWAP::SwapQuForExactAsset_input input, uint64 inputValue) { + QSWAP::SwapQuForExactAsset_output swapQuForExactAsset( const id& issuer, QSWAP::SwapQuForExactAsset_input input, uint64 inputValue) + { QSWAP::SwapQuForExactAsset_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -162,7 +174,8 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::SwapExactAssetForQu_output swapExactAssetForQu(const id& issuer, QSWAP::SwapExactAssetForQu_input input, uint64 inputValue) { + QSWAP::SwapExactAssetForQu_output swapExactAssetForQu(const id& issuer, QSWAP::SwapExactAssetForQu_input input, uint64 inputValue) + { QSWAP::SwapExactAssetForQu_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -176,7 +189,8 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::SwapAssetForExactQu_output swapAssetForExactQu(const id& issuer, QSWAP::SwapAssetForExactQu_input input, uint64 inputValue) { + QSWAP::SwapAssetForExactQu_output swapAssetForExactQu(const id& issuer, QSWAP::SwapAssetForExactQu_input input, uint64 inputValue) + { QSWAP::SwapAssetForExactQu_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -190,68 +204,79 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::QuoteExactQuInput_output quoteExactQuInput(QSWAP::QuoteExactQuInput_input input) { + QSWAP::TransferShareManagementRights_output transferShareManagementRights(const id& invocator, QSWAP::TransferShareManagementRights_input input, uint64 inputValue) + { + QSWAP::TransferShareManagementRights_output output; + invokeUserProcedure(QSWAP_CONTRACT_INDEX, TRANSFER_SHARE_MANAGEMENT_RIGHTS_IDX, input, output, invocator, inputValue); + return output; + } + + QSWAP::QuoteExactQuInput_output quoteExactQuInput(QSWAP::QuoteExactQuInput_input input) + { QSWAP::QuoteExactQuInput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_QU_INPUT_IDX, input, output); return output; } - QSWAP::QuoteExactQuOutput_output quoteExactQuOutput(QSWAP::QuoteExactQuOutput_input input) { + QSWAP::QuoteExactQuOutput_output quoteExactQuOutput(QSWAP::QuoteExactQuOutput_input input) + { QSWAP::QuoteExactQuOutput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_QU_OUTPUT_IDX, input, output); return output; } - QSWAP::QuoteExactAssetInput_output quoteExactAssetInput(QSWAP::QuoteExactAssetInput_input input){ + QSWAP::QuoteExactAssetInput_output quoteExactAssetInput(QSWAP::QuoteExactAssetInput_input input) + { QSWAP::QuoteExactAssetInput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_ASSET_INPUT_IDX, input, output); return output; } - QSWAP::QuoteExactAssetOutput_output quoteExactAssetOutput(QSWAP::QuoteExactAssetOutput_input input){ + QSWAP::QuoteExactAssetOutput_output quoteExactAssetOutput(QSWAP::QuoteExactAssetOutput_input input) + { QSWAP::QuoteExactAssetOutput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_ASSET_OUTPUT_IDX, input, output); return output; } }; -TEST(ContractSwap, TeamInfoTest) +TEST(ContractSwap, InvestRewardsInfoTest) { ContractTestingQswap qswap; - { - QSWAP::TeamInfo_output team_info = qswap.teamInfo(); + { + QSWAP::InvestRewardsInfo_output info = qswap.investRewardsInfo(); - auto expectIdentity = (const unsigned char*)"IRUNQTXZRMLDEENHPRZQPSGPCFACORRUJYSBVJPQEHFCEKLLURVDDJVEXNBL"; + auto expectIdentity = (const unsigned char*)"VJGRUFWJCUSNHCQJRWRRYXAUEJFCVHYPXWKTDLYKUACPVVYBGOLVCJSF"; m256i expectPubkey; getPublicKeyFromIdentity(expectIdentity, expectPubkey.m256i_u8); - EXPECT_EQ(team_info.teamId, expectPubkey); - EXPECT_EQ(team_info.teamFee, 20); - } + EXPECT_EQ(info.investRewardsId, expectPubkey); + EXPECT_EQ(info.investRewardsFee, 3); + } { - id newTeamId(6,6,6,6); - QSWAP::SetTeamInfo_input input = {newTeamId}; + id newInvestRewardsId(6,6,6,6); + QSWAP::SetInvestRewardsInfo_input input = {newInvestRewardsId}; id invalidIssuer(1,2,3,4); - increaseEnergy(invalidIssuer, 100); - bool res1 = qswap.setTeamId(invalidIssuer, input); + increaseEnergy(invalidIssuer, 100); + bool res1 = qswap.setInvestRewardsInfo(invalidIssuer, input); // printf("res1: %d\n", res1); EXPECT_FALSE(res1); - auto teamIdentity = (const unsigned char*)"IRUNQTXZRMLDEENHPRZQPSGPCFACORRUJYSBVJPQEHFCEKLLURVDDJVEXNBL"; - m256i teamPubkey; - getPublicKeyFromIdentity(teamIdentity, teamPubkey.m256i_u8); + auto investRewardsIdentity = (const unsigned char*)"VJGRUFWJCUSNHCQJRWRRYXAUEJFCVHYPXWKTDLYKUACPVVYBGOLVCJSF"; + m256i investRewardsPubkey; + getPublicKeyFromIdentity(investRewardsIdentity, investRewardsPubkey.m256i_u8); - increaseEnergy(teamPubkey, 100); - bool res2 = qswap.setTeamId(teamPubkey, input); + increaseEnergy(investRewardsPubkey, 100); + bool res2 = qswap.setInvestRewardsInfo(investRewardsPubkey, input); // printf("res2: %d\n", res2); EXPECT_TRUE(res2); - QSWAP::TeamInfo_output team_info = qswap.teamInfo(); - EXPECT_EQ(team_info.teamId, newTeamId); - // printf("%d\n", team_info.teamId == newTeamId); + QSWAP::InvestRewardsInfo_output info = qswap.investRewardsInfo(); + EXPECT_EQ(info.investRewardsId, newInvestRewardsId); + // printf("%d\n", info.investRewardsId == newInvestRewardsId); } } @@ -263,7 +288,7 @@ TEST(ContractSwap, QuoteTest) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -275,8 +300,8 @@ TEST(ContractSwap, QuoteTest) sint64 inputValue = 30*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 30*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 30*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); QSWAP::QuoteExactQuInput_input qi_input = {issuer, assetName, 1000}; QSWAP::QuoteExactQuInput_output qi_output = qswap.quoteExactQuInput(qi_input); @@ -286,7 +311,7 @@ TEST(ContractSwap, QuoteTest) QSWAP::QuoteExactQuOutput_input qo_input = {issuer, assetName, 1000}; QSWAP::QuoteExactQuOutput_output qo_output = qswap.quoteExactQuOutput(qo_input); // printf("quote exact qu output: %lld\n", qo_output.assetAmountIn); - EXPECT_EQ(qo_output.assetAmountIn, 1037); + EXPECT_EQ(qo_output.assetAmountIn, 1038); QSWAP::QuoteExactAssetInput_input ai_input = {issuer, assetName, 1000}; QSWAP::QuoteExactAssetInput_output ai_output = qswap.quoteExactAssetInput(ai_input); @@ -296,7 +321,7 @@ TEST(ContractSwap, QuoteTest) QSWAP::QuoteExactAssetOutput_input ao_input = {issuer, assetName, 1000}; QSWAP::QuoteExactAssetOutput_output ao_output = qswap.quoteExactAssetOutput(ao_input); // printf("quote exact asset output: %lld\n", ao_output.quAmountIn); - EXPECT_EQ(ao_output.quAmountIn, 1037); + EXPECT_EQ(ao_output.quAmountIn, 1038); } } @@ -306,7 +331,7 @@ TEST(ContractSwap, QuoteTest) 2. issue duplicate asset 3. issue asset with invalid input params, such as numberOfShares: 0 */ -TEST(ContractSwap, IssueAsset) +TEST(ContractSwap, IssueAssetAndTransferShareManagementRights) { ContractTestingQswap qswap; @@ -332,6 +357,15 @@ TEST(ContractSwap, IssueAsset) EXPECT_EQ(qswap.transferAsset(issuer, ts_input), transferAmount); EXPECT_EQ(numberOfPossessedShares(assetName, issuer, newId, newId, QSWAP_CONTRACT_INDEX, QSWAP_CONTRACT_INDEX), transferAmount); // printf("%lld\n", getBalance(QSWAP_CONTRACT_ID)); + increaseEnergy(issuer, 100); + uint64 qswapIdBalance = getBalance(QSWAP_CONTRACT_ID); + uint64 issuerBalance = getBalance(issuer); + QSWAP::TransferShareManagementRights_input tsr_input = {Asset{issuer, assetName}, transferAmount, QX_CONTRACT_INDEX}; + EXPECT_EQ(qswap.transferShareManagementRights(issuer, tsr_input, 100).transferredNumberOfShares, transferAmount); + EXPECT_EQ(getBalance(id(QX_CONTRACT_INDEX, 0, 0, 0)), 100); + EXPECT_EQ(getBalance(QSWAP_CONTRACT_ID), qswapIdBalance); + EXPECT_EQ(getBalance(issuer), issuerBalance - 100); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, issuer, issuer, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), transferAmount); } // 1. not enough energy for asset issue fee @@ -370,7 +404,7 @@ TEST(ContractSwap, SwapExactQuForAsset) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -382,9 +416,9 @@ TEST(ContractSwap, SwapExactQuForAsset) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); } { @@ -406,11 +440,11 @@ TEST(ContractSwap, SwapExactQuForAsset) EXPECT_TRUE(output.assetAmountOut <= 50000); // 49924 if swapFee 0.3% QSWAP::GetPoolBasicState_output psOutput = qswap.getPoolBasicState(issuer, assetName); - // printf("%lld, %lld, %lld\n", psOutput.reservedAssetAmount, psOutput.reservedQuAmount, psOutput.totalLiqudity); - // swapFee is 200_000 * 0.3% = 600, teamFee: 120, protocolFee: 96 - EXPECT_TRUE(psOutput.reservedQuAmount >= 399784); // 399784 = (400_000 - 120 - 96) + // printf("%lld, %lld, %lld\n", psOutput.reservedAssetAmount, psOutput.reservedQuAmount, psOutput.totalLiquidity); + // swapFee is 200_000 * 0.3% = 600, shareholders 27%: 162, QX 5%: 30, invest&rewards 3%: 18, burn 1%: 6 = 216 + EXPECT_TRUE(psOutput.reservedQuAmount >= 399784); // 399784 = (400_000 - 216) EXPECT_TRUE(psOutput.reservedAssetAmount >= 50000 ); // 50076 - EXPECT_EQ(psOutput.totalLiqudity, 141421); // liqudity stay the same + EXPECT_EQ(psOutput.totalLiquidity, 141421); // liquidity stay the same } } @@ -422,7 +456,7 @@ TEST(ContractSwap, SwapQuForExactAsset) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -434,9 +468,9 @@ TEST(ContractSwap, SwapQuForExactAsset) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); } { @@ -468,7 +502,7 @@ TEST(ContractSwap, SwapExactAssetForQu) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -480,9 +514,9 @@ TEST(ContractSwap, SwapExactAssetForQu) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); } { @@ -511,7 +545,7 @@ TEST(ContractSwap, SwapAssetForExactQu) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -523,12 +557,12 @@ TEST(ContractSwap, SwapAssetForExactQu) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); // QSWAP::GetPoolBasicState_output gp_output = qswap.getPoolBasicState(issuer, assetName); - // printf("%lld, %lld, %lld\n", gp_output.reservedQuAmount, gp_output.reservedAssetAmount, gp_output.totalLiqudity); + // printf("%lld, %lld, %lld\n", gp_output.reservedQuAmount, gp_output.reservedAssetAmount, gp_output.totalLiquidity); } { @@ -546,7 +580,7 @@ TEST(ContractSwap, SwapAssetForExactQu) id user(1,2,3,4); sint64 inputValue = 0; sint64 quAmountOut = 100*1000; - sint64 expectAssetAmountIn = 100603; + sint64 expectAssetAmountIn = 100604; QSWAP::QuoteExactQuOutput_input qo_input = {issuer, assetName, quAmountOut}; QSWAP::QuoteExactQuOutput_output qo_output = qswap.quoteExactQuOutput(qo_input); @@ -600,7 +634,7 @@ TEST(ContractSwap, CreatePool) EXPECT_EQ(output.poolExists, true); EXPECT_EQ(output.reservedQuAmount, 0); EXPECT_EQ(output.reservedAssetAmount, 0); - EXPECT_EQ(output.totalLiqudity, 0); + EXPECT_EQ(output.totalLiquidity, 0); } // 2. create duplicate pool @@ -616,7 +650,7 @@ TEST(ContractSwap, CreatePool) } /* -add liqudity 2 times, and then remove +add liquidity 2 times, and then remove */ TEST(ContractSwap, LiqTest1) { @@ -638,13 +672,13 @@ TEST(ContractSwap, LiqTest1) EXPECT_TRUE(qswap.createPool(issuer, assetName)); } - // 1. add liqudity to a initial pool, first time + // 1. add liquidity to a initial pool, first time { sint64 quStakeAmount = 200*1000; sint64 inputValue = quStakeAmount; sint64 assetStakeAmount = 100*1000; increaseEnergy(issuer, quStakeAmount); - QSWAP::AddLiqudity_input addLiqInput = { + QSWAP::AddLiquidity_input addLiqInput = { issuer, assetName, assetStakeAmount, @@ -652,9 +686,9 @@ TEST(ContractSwap, LiqTest1) 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, addLiqInput, inputValue); - // actually, 141421 liqudity add to the pool, but the first 1000 liqudity is retainedd by the pool rather than the staker - EXPECT_EQ(output.userIncreaseLiqudity, 140421); + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, addLiqInput, inputValue); + // actually, 141421 liquidity add to the pool, but the first 1000 liquidity is retainedd by the pool rather than the staker + EXPECT_EQ(output.userIncreaseLiquidity, 140421); EXPECT_EQ(output.quAmount, 200*1000); EXPECT_EQ(output.assetAmount, 100*1000); @@ -662,18 +696,18 @@ TEST(ContractSwap, LiqTest1) EXPECT_EQ(output2.poolExists, true); EXPECT_EQ(output2.reservedQuAmount, 200*1000); EXPECT_EQ(output2.reservedAssetAmount, 100*1000); - EXPECT_EQ(output2.totalLiqudity, 141421); - // printf("pool state: %lld, %lld, %lld\n", output2.reservedQuAmount, output2.reservedAssetAmount, output2.totalLiqudity); + EXPECT_EQ(output2.totalLiquidity, 141421); + // printf("pool state: %lld, %lld, %lld\n", output2.reservedQuAmount, output2.reservedAssetAmount, output2.totalLiquidity); - QSWAP::GetLiqudityOf_input getLiqInput = { + QSWAP::GetLiquidityOf_input getLiqInput = { issuer, assetName, issuer }; - QSWAP::GetLiqudityOf_output getLiqOutput = qswap.getLiqudityOf(getLiqInput); - EXPECT_EQ(getLiqOutput.liqudity, 140421); + QSWAP::GetLiquidityOf_output getLiqOutput = qswap.getLiquidityOf(getLiqInput); + EXPECT_EQ(getLiqOutput.liquidity, 140421); - // 2. add liqudity second time + // 2. add liquidity second time increaseEnergy(issuer, quStakeAmount); addLiqInput = { issuer, @@ -683,15 +717,15 @@ TEST(ContractSwap, LiqTest1) 0 }; - QSWAP::AddLiqudity_output output3 = qswap.addLiqudity(issuer, addLiqInput, inputValue); - EXPECT_EQ(output3.userIncreaseLiqudity, 141421); + QSWAP::AddLiquidity_output output3 = qswap.addLiquidity(issuer, addLiqInput, inputValue); + EXPECT_EQ(output3.userIncreaseLiquidity, 141421); EXPECT_EQ(output3.quAmount, 200*1000); EXPECT_EQ(output3.assetAmount, 100*1000); - getLiqOutput = qswap.getLiqudityOf(getLiqInput); - EXPECT_EQ(getLiqOutput.liqudity, 281842); // 140421 + 141421 + getLiqOutput = qswap.getLiquidityOf(getLiqInput); + EXPECT_EQ(getLiqOutput.liquidity, 281842); // 140421 + 141421 - QSWAP::RemoveLiqudity_input rmLiqInput = { + QSWAP::RemoveLiquidity_input rmLiqInput = { issuer, assetName, 141421, @@ -699,15 +733,15 @@ TEST(ContractSwap, LiqTest1) 100*1000, // should lte 1000*100 }; - // 3. remove liqudity - QSWAP::RemoveLiqudity_output rmLiqOutput = qswap.removeLiqudity(issuer, rmLiqInput, 0); + // 3. remove liquidity + QSWAP::RemoveLiquidity_output rmLiqOutput = qswap.removeLiquidity(issuer, rmLiqInput, 0); // printf("qu: %lld, asset: %lld\n", rmLiqOutput.quAmount, rmLiqOutput.assetAmount); EXPECT_EQ(rmLiqOutput.quAmount, 1000 * 200); EXPECT_EQ(rmLiqOutput.assetAmount, 1000 * 100); - getLiqOutput = qswap.getLiqudityOf(getLiqInput); - // printf("liq: %lld\n", getLiqOutput.liqudity); - EXPECT_EQ(getLiqOutput.liqudity, 140421); // 281842 - 141421 + getLiqOutput = qswap.getLiquidityOf(getLiqInput); + // printf("liq: %lld\n", getLiqOutput.liquidity); + EXPECT_EQ(getLiqOutput.liquidity, 140421); // 281842 - 141421 } } @@ -732,12 +766,12 @@ TEST(ContractSwap, LiqTest2) EXPECT_TRUE(qswap.createPool(issuer, assetName)); } - // add liqudity to invalid pool, + // add liquidity to invalid pool, { // decreaseEnergy(getBalance(issuer)); uint64 quAmount = 1000; increaseEnergy(issuer, quAmount); - QSWAP::AddLiqudity_input addLiqInput = { + QSWAP::AddLiquidity_input addLiqInput = { issuer, invalidAssetName, 1000, @@ -745,16 +779,16 @@ TEST(ContractSwap, LiqTest2) 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, addLiqInput, 1000); - EXPECT_EQ(output.userIncreaseLiqudity, 0); + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, addLiqInput, 1000); + EXPECT_EQ(output.userIncreaseLiquidity, 0); EXPECT_EQ(output.quAmount, 0); EXPECT_EQ(output.assetAmount, 0); } - // add liqudity with asset more than holdings + // add liquidity with asset more than holdings { increaseEnergy(issuer, 1000); - QSWAP::AddLiqudity_input addLiqInput = { + QSWAP::AddLiquidity_input addLiqInput = { issuer, assetName, 1000*1000 + 100, // excced 1000*1000 @@ -762,8 +796,8 @@ TEST(ContractSwap, LiqTest2) 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, addLiqInput, 1000); - EXPECT_EQ(output.userIncreaseLiqudity, 0); + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, addLiqInput, 1000); + EXPECT_EQ(output.userIncreaseLiquidity, 0); EXPECT_EQ(output.quAmount, 0); EXPECT_EQ(output.assetAmount, 0); } diff --git a/test/contract_qutil.cpp b/test/contract_qutil.cpp index 8a6a17296..18e66b7ea 100644 --- a/test/contract_qutil.cpp +++ b/test/contract_qutil.cpp @@ -13,6 +13,9 @@ class ContractTestingQUtil : public ContractTesting { callSystemProcedure(QUTIL_CONTRACT_INDEX, INITIALIZE); INIT_CONTRACT(QX); callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + + // init fees + callSystemProcedure(QUTIL_CONTRACT_INDEX, INITIALIZE, true); } void beginEpoch(bool expectSuccess = true) @@ -100,6 +103,13 @@ class ContractTestingQUtil : public ContractTesting { callFunction(QUTIL_CONTRACT_INDEX, 6, input, output); return output; } + + QUTIL::DistributeQuToShareholders_output distributeQuToShareholders(const id& invocator, const Asset& asset, sint64 amount) { + QUTIL::DistributeQuToShareholders_input input{ asset }; + QUTIL::DistributeQuToShareholders_output output; + invokeUserProcedure(QUTIL_CONTRACT_INDEX, 7, input, output, invocator, amount); + return output; + } }; // Helper function to generate random ID @@ -537,7 +547,9 @@ TEST(QUtilTest, VoterListUpdateAndCompaction) { vote_inputA.address = voterA; vote_inputA.amount = min_amount; vote_inputA.chosen_option = 0; - qutil.vote(voterA, vote_inputA, QUTIL_VOTE_FEE); + EXPECT_TRUE(qutil.vote(voterA, vote_inputA, QUTIL_VOTE_FEE).success); + + EXPECT_EQ(getBalance(voterA), min_amount + QUTIL_VOTE_FEE); QUTIL::Vote_input vote_inputB; vote_inputB.poll_id = poll_id0; @@ -1155,8 +1167,8 @@ TEST(QUtilTest, CreatePoll_InvalidGithubLink) { id creator = generateRandomId(); id poll_name = generateRandomId(); uint64_t min_amount = 1000; - // Invalid GitHub link (does not start with "https://github.com/qubic") - Array invalid_github_link = stringToArray("https://github.com/invalidorg/proposal/abc"); + // Invalid GitHub link (does not start with "https://github.com/") + Array invalid_github_link = stringToArray("https://gitlab.com/invalidlink/proposal/abc"); uint64_t poll_type = QUTIL_POLL_TYPE_QUBIC; QUTIL::CreatePoll_input input; @@ -1575,3 +1587,165 @@ TEST(QUtilTest, MultipleVoters_ShareTransfers_EligibilityTest) EXPECT_EQ(result.voter_count.get(1), 2); EXPECT_EQ(result.voter_count.get(2), 3); } + +TEST(QUtilTest, DistributeQuToShareholders) +{ + ContractTestingQUtil qutil; + + id distributor = generateRandomId(); + id issuer = generateRandomId(); + std::vector shareholder(10); + for (auto& v : shareholder) + { + v = generateRandomId(); + } + + increaseEnergy(issuer, 4000000000); // for issuance and transfers + increaseEnergy(distributor, 10000000000); + + // Issue 3 asset + unsigned long long assetNameA = assetNameFromString("ASSETA"); + unsigned long long assetNameB = assetNameFromString("ASSETB"); + unsigned long long assetNameC = assetNameFromString("ASSETC"); + Asset assetA = { issuer, assetNameA }; + Asset assetB = { issuer, assetNameB }; + Asset assetC = { issuer, assetNameC }; + qutil.issueAsset(issuer, assetNameA, 10); + qutil.issueAsset(issuer, assetNameB, 10000); + qutil.issueAsset(issuer, assetNameC, 10000000); + + // Distribute assets + // shareholder 0-2: 1 A, 500 B, 500 C + for (int i = 0; i < 3; i++) + { + qutil.transferAsset(issuer, shareholder[i], assetA, 1); + qutil.transferAsset(issuer, shareholder[i], assetB, 500); + qutil.transferAsset(issuer, shareholder[i], assetC, 600); + } + // shareholder 3-5: 0 A, 2000 B, 500 C + for (int i = 3; i < 6; i++) + { + qutil.transferAsset(issuer, shareholder[i], assetB, 2000); + qutil.transferAsset(issuer, shareholder[i], assetC, 500); + } + // shareholder 6-9: 1 A, 500 B, 0 C + for (int i = 6; i < 10; i++) + { + qutil.transferAsset(issuer, shareholder[i], assetA, 1); + qutil.transferAsset(issuer, shareholder[i], assetB, 500); + } + + QUTIL::DistributeQuToShareholders_output output; + sint64 distributorBalanceBefore, shareholderBalanceBefore; + + // Error case 1: asset without shareholders + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, { distributor, assetNameA }, 10000000); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore); + EXPECT_EQ(output.shareholders, 0); + EXPECT_EQ(output.totalShares, 0); + EXPECT_EQ(output.amountPerShare, 0); + EXPECT_EQ(output.fees, 0); + + // Error case 2: amount too low to pay fee + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, 1); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_LE(output.amountPerShare, 0); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Error case 3: amount too low to pay 1 QU per share + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER + 9); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_EQ(output.amountPerShare, 0); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetA + exactly calculated amount + sint64 amountPerShare = 50; + sint64 totalAmount = 10 * amountPerShare + 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, totalAmount); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + amountPerShare); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetA + amount with some QUs that cannot be evenly distributed and are refundet + amountPerShare = 100; + totalAmount = 10 * amountPerShare + 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, totalAmount + 7); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + amountPerShare); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetB + exactly calculated amount + amountPerShare = 1000; + totalAmount = 10000 * amountPerShare + 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetB, totalAmount); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 500 * amountPerShare); + EXPECT_EQ(output.shareholders, 11); + EXPECT_EQ(output.totalShares, 10000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetB + amount with some QUs that cannot be evenly distributed and are refundet + amountPerShare = 42; + totalAmount = 10000 * amountPerShare + 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetB, totalAmount + 9999); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 500 * amountPerShare); + EXPECT_EQ(output.shareholders, 11); + EXPECT_EQ(output.totalShares, 10000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetC + exactly calculated amount (fee is minimal) + amountPerShare = 123; + totalAmount = 10000000 * amountPerShare + 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetC, totalAmount); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 600 * amountPerShare); + EXPECT_EQ(output.shareholders, 7); + EXPECT_EQ(output.totalShares, 10000000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetC + non-minimal fee (fee payed too much is donation for running QUTIL -> burned with fee) + amountPerShare = 654; + totalAmount = 10000000 * amountPerShare + 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetC, totalAmount + 123456); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 600 * amountPerShare); + EXPECT_EQ(output.shareholders, 7); + EXPECT_EQ(output.totalShares, 10000000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); +} diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp new file mode 100644 index 000000000..15d547325 --- /dev/null +++ b/test/contract_rl.cpp @@ -0,0 +1,1283 @@ +// File: test/contract_rl.cpp +#define NO_UEFI + +#include "contract_testing.h" + +constexpr uint16 PROCEDURE_INDEX_BUY_TICKET = 1; +constexpr uint16 PROCEDURE_INDEX_SET_PRICE = 2; +constexpr uint16 PROCEDURE_INDEX_SET_SCHEDULE = 3; +constexpr uint16 FUNCTION_INDEX_GET_FEES = 1; +constexpr uint16 FUNCTION_INDEX_GET_PLAYERS = 2; +constexpr uint16 FUNCTION_INDEX_GET_WINNERS = 3; +constexpr uint16 FUNCTION_INDEX_GET_TICKET_PRICE = 4; +constexpr uint16 FUNCTION_INDEX_GET_MAX_NUM_PLAYERS = 5; +constexpr uint16 FUNCTION_INDEX_GET_STATE = 6; +constexpr uint16 FUNCTION_INDEX_GET_BALANCE = 7; +constexpr uint16 FUNCTION_INDEX_GET_NEXT_EPOCH_DATA = 8; +constexpr uint16 FUNCTION_INDEX_GET_DRAW_HOUR = 9; +constexpr uint16 FUNCTION_INDEX_GET_SCHEDULE = 10; +constexpr uint8 STATE_SELLING = static_cast(RL::EState::SELLING); +constexpr uint8 STATE_LOCKED = 0u; + +static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, + _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + +constexpr uint8 RL_ANY_DAY_DRAW_SCHEDULE = 0xFF; // 0xFF sets bits 0..6 (WED..TUE); bit 7 is unused/ignored by logic + +static uint32 makeDateStamp(uint16 year, uint8 month, uint8 day) +{ + const uint8 shortYear = static_cast(year - 2000); + return static_cast(shortYear << 9 | month << 5 | day); +} + +inline bool operator==(uint8 left, RL::EReturnCode right) +{ + return left == RL::toReturnCode(right); +} +inline bool operator==(RL::EReturnCode left, uint8 right) +{ + return right == left; +} +inline bool operator!=(uint8 left, RL::EReturnCode right) +{ + return !(left == right); +} +inline bool operator!=(RL::EReturnCode left, uint8 right) +{ + return !(right == left); +} + +// Equality operator for comparing WinnerInfo objects +// Compares all fields (address, revenue, epoch, tick, dayOfWeek) +bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) +{ + return left.winnerAddress == right.winnerAddress && left.revenue == right.revenue && left.epoch == right.epoch && left.tick == right.tick && + left.dayOfWeek == right.dayOfWeek; +} + +// Test helper that exposes internal state assertions and utilities +class RLChecker : public RL +{ +public: + void checkFees(const GetFees_output& fees) + { + EXPECT_EQ(fees.returnCode, EReturnCode::SUCCESS); + + EXPECT_EQ(fees.distributionFeePercent, distributionFeePercent); + EXPECT_EQ(fees.teamFeePercent, teamFeePercent); + EXPECT_EQ(fees.winnerFeePercent, winnerFeePercent); + EXPECT_EQ(fees.burnPercent, burnPercent); + } + + void checkPlayers(const GetPlayers_output& output) const + { + EXPECT_EQ(output.returnCode, EReturnCode::SUCCESS); + EXPECT_EQ(output.players.capacity(), players.capacity()); + EXPECT_EQ(output.playerCounter, playerCounter); + + for (uint64 i = 0; i < playerCounter; ++i) + { + EXPECT_EQ(output.players.get(i), players.get(i)); + } + } + + void checkWinners(const GetWinners_output& output) const + { + EXPECT_EQ(output.returnCode, EReturnCode::SUCCESS); + EXPECT_EQ(output.winners.capacity(), winners.capacity()); + + const uint64 expectedCount = mod(winnersCounter, winners.capacity()); + EXPECT_EQ(output.winnersCounter, expectedCount); + + for (uint64 i = 0; i < expectedCount; ++i) + { + EXPECT_EQ(output.winners.get(i), winners.get(i)); + } + } + + void randomlyAddPlayers(uint64 maxNewPlayers) + { + playerCounter = mod(maxNewPlayers, players.capacity()); + for (uint64 i = 0; i < playerCounter; ++i) + { + players.set(i, id::randomValue()); + } + } + + void randomlyAddWinners(uint64 maxNewWinners) + { + const uint64 newWinnerCount = mod(maxNewWinners, winners.capacity()); + + winnersCounter = 0; + WinnerInfo wi; + + for (uint64 i = 0; i < newWinnerCount; ++i) + { + wi.epoch = 1; + wi.tick = 1; + wi.revenue = 1000000; + wi.winnerAddress = id::randomValue(); + winners.set(winnersCounter++, wi); + } + } + + void setScheduleMask(uint8 newMask) { schedule = newMask; } + + uint64 getPlayerCounter() const { return playerCounter; } + + uint64 getTicketPrice() const { return ticketPrice; } + + uint32 getLastDrawDateStamp() const { return lastDrawDateStamp; } +}; + +class ContractTestingRL : protected ContractTesting +{ +public: + ContractTestingRL() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QX); + system.epoch = contractDescriptions[QX_CONTRACT_INDEX].constructionEpoch; + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(RL); + system.epoch = contractDescriptions[RL_CONTRACT_INDEX].constructionEpoch; + callSystemProcedure(RL_CONTRACT_INDEX, INITIALIZE); + } + + // Access internal contract state for assertions + RLChecker* state() { return reinterpret_cast(contractStates[RL_CONTRACT_INDEX]); } + + RL::GetFees_output getFees() + { + RL::GetFees_input input; + RL::GetFees_output output; + + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_FEES, input, output); + return output; + } + + RL::GetPlayers_output getPlayers() + { + RL::GetPlayers_input input; + RL::GetPlayers_output output; + + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_PLAYERS, input, output); + return output; + } + + RL::GetWinners_output getWinners() + { + RL::GetWinners_input input; + RL::GetWinners_output output; + + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_WINNERS, input, output); + return output; + } + + // Wrapper for public function RL::GetTicketPrice + RL::GetTicketPrice_output getTicketPrice() + { + RL::GetTicketPrice_input input; + RL::GetTicketPrice_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_TICKET_PRICE, input, output); + return output; + } + + // Wrapper for public function RL::GetMaxNumberOfPlayers + RL::GetMaxNumberOfPlayers_output getMaxNumberOfPlayers() + { + RL::GetMaxNumberOfPlayers_input input; + RL::GetMaxNumberOfPlayers_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_MAX_NUM_PLAYERS, input, output); + return output; + } + + // Wrapper for public function RL::GetState + RL::GetState_output getStateInfo() + { + RL::GetState_input input; + RL::GetState_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_STATE, input, output); + return output; + } + + // Wrapper for public function RL::GetBalance + // Returns current contract on-chain balance (incoming - outgoing) + RL::GetBalance_output getBalanceInfo() + { + RL::GetBalance_input input; + RL::GetBalance_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_BALANCE, input, output); + return output; + } + + // Wrapper for public function RL::GetNextEpochData + RL::GetNextEpochData_output getNextEpochData() + { + RL::GetNextEpochData_input input; + RL::GetNextEpochData_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_NEXT_EPOCH_DATA, input, output); + return output; + } + + // Wrapper for public function RL::GetDrawHour + RL::GetDrawHour_output getDrawHour() + { + RL::GetDrawHour_input input; + RL::GetDrawHour_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_DRAW_HOUR, input, output); + return output; + } + + // Wrapper for public function RL::GetSchedule + RL::GetSchedule_output getSchedule() + { + RL::GetSchedule_input input; + RL::GetSchedule_output output; + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_SCHEDULE, input, output); + return output; + } + + RL::BuyTicket_output buyTicket(const id& user, sint64 reward) + { + RL::BuyTicket_input input; + RL::BuyTicket_output output; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET, input, output, user, reward)) + { + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + // Added: wrapper for SetPrice procedure + RL::SetPrice_output setPrice(const id& invocator, uint64 newPrice) + { + RL::SetPrice_input input; + input.newPrice = newPrice; + RL::SetPrice_output output; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_PRICE, input, output, invocator, 0)) + { + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + // Added: wrapper for SetSchedule procedure + RL::SetSchedule_output setSchedule(const id& invocator, uint8 newSchedule) + { + RL::SetSchedule_input input; + input.newSchedule = newSchedule; + RL::SetSchedule_output output; + if (!invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_SET_SCHEDULE, input, output, invocator, 0)) + { + output.returnCode = RL::toReturnCode(RL::EReturnCode::UNKNOWN_ERROR); + } + return output; + } + + void BeginEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_EPOCH); } + + void EndEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, END_EPOCH); } + + void BeginTick() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_TICK); } + + // Returns the SELF contract account address + id rlSelf() const { return id(RL_CONTRACT_INDEX, 0, 0, 0); } + + // Computes remaining contract balance after winner/team/distribution/burn payouts + // Distribution is floored to a multiple of NUMBER_OF_COMPUTORS + uint64 expectedRemainingAfterPayout(uint64 before, const RL::GetFees_output& fees) + { + const uint64 burn = (before * fees.burnPercent) / 100; + const uint64 distribPer = ((before * fees.distributionFeePercent) / 100) / NUMBER_OF_COMPUTORS; + const uint64 distrib = distribPer * NUMBER_OF_COMPUTORS; // floor to a multiple + const uint64 team = (before * fees.teamFeePercent) / 100; + const uint64 winner = (before * fees.winnerFeePercent) / 100; + return before - burn - distrib - team - winner; + } + + // Fund user and buy a ticket, asserting success + void increaseAndBuy(ContractTestingRL& ctl, const id& user, uint64 ticketPrice) + { + increaseEnergy(user, ticketPrice * 2); + const RL::BuyTicket_output out = ctl.buyTicket(user, ticketPrice); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + } + + // Assert contract account balance equals the value returned by RL::GetBalance + void expectContractBalanceEqualsGetBalance(ContractTestingRL& ctl, const id& contractAddress) + { + const RL::GetBalance_output out = ctl.getBalanceInfo(); + EXPECT_EQ(out.balance, getBalance(contractAddress)); + } + + // New: set full date and hour (UTC), then sync QPI time + void setDateTime(uint16 year, uint8 month, uint8 day, uint8 hour) + { + updateTime(); + utcTime.Year = year; + utcTime.Month = month; + utcTime.Day = day; + utcTime.Hour = hour; + utcTime.Minute = 0; + utcTime.Second = 0; + utcTime.Nanosecond = 0; + updateQpiTime(); + } + + // New: advance to the next tick boundary where tick % RL_TICK_UPDATE_PERIOD == 0 and run BEGIN_TICK once + void forceBeginTick() + { + system.tick = system.tick + (RL_TICK_UPDATE_PERIOD - mod(system.tick, static_cast(RL_TICK_UPDATE_PERIOD))); + + BeginTick(); + } + + // New: helper to advance one calendar day and perform a scheduled draw at 12:00 UTC + void advanceOneDayAndDraw() + { + // Use a safe base month to avoid invalid dates: January 2025 + static uint16 y = 2025; + static uint8 m = 1; + static uint8 d = 10; // start from 10th + // advance one day within January bounds + d = static_cast(d + 1); + if (d > 31) + { + d = 1; // wrap within month for simplicity in tests + } + setDateTime(y, m, d, 12); + forceBeginTick(); + } + + // Force schedule mask directly in state (bypasses external call, suitable for tests) + void forceSchedule(uint8 scheduleMask) + { + state()->setScheduleMask(scheduleMask); + // NOTE: we do not call SetSchedule here to avoid epoch transitions in tests. + } + + void beginEpochWithDate(uint16 year, uint8 month, uint8 day, uint8 hour = static_cast(RL_DEFAULT_DRAW_HOUR + 1)) + { + setDateTime(year, month, day, hour); + BeginEpoch(); + } + + void beginEpochWithValidTime() { beginEpochWithDate(2025, 1, 20); } +}; + +TEST(ContractRandomLottery, SetPriceAndScheduleApplyNextEpoch) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + + // Default epoch configuration: draws 3 times per week at the default price + const uint64 oldPrice = ctl.state()->getTicketPrice(); + EXPECT_EQ(ctl.getSchedule().schedule, RL_DEFAULT_SCHEDULE); + + // Queue a new price (5,000,000) and limit draws to only Wednesday + constexpr uint64 newPrice = 5000000; + constexpr uint8 wednesdayOnly = static_cast(1 << WEDNESDAY); + increaseEnergy(RL_DEV_ADDRESS, 3); + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, newPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.setSchedule(RL_DEV_ADDRESS, wednesdayOnly).returnCode, RL::EReturnCode::SUCCESS); + + const RL::NextEpochData nextDataBefore = ctl.getNextEpochData().nextEpochData; + EXPECT_EQ(nextDataBefore.newPrice, newPrice); + EXPECT_EQ(nextDataBefore.schedule, wednesdayOnly); + + // Until END_EPOCH the old settings remain active + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + EXPECT_EQ(ctl.getSchedule().schedule, RL_DEFAULT_SCHEDULE); + + // Transition closes the epoch and applies both pending changes + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); + EXPECT_EQ(ctl.getSchedule().schedule, wednesdayOnly); + + const RL::NextEpochData nextDataAfter = ctl.getNextEpochData().nextEpochData; + EXPECT_EQ(nextDataAfter.newPrice, 0u); + EXPECT_EQ(nextDataAfter.schedule, 0u); + + // In the next epoch tickets must sell at the updated price + ctl.beginEpochWithDate(2025, 1, 15); // Wednesday + const id buyer = id::randomValue(); + increaseEnergy(buyer, newPrice * 2); + const uint64 balBefore = getBalance(buyer); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output buyOut = ctl.buyTicket(buyer, newPrice); + EXPECT_EQ(buyOut.returnCode, RL::EReturnCode::SUCCESS); + const uint64 playersAfterFirstBuy = playersBefore + 1; + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterFirstBuy); + EXPECT_EQ(getBalance(buyer), balBefore - newPrice); + + // Second user also buys a ticket at the new price + const id secondBuyer = id::randomValue(); + increaseEnergy(secondBuyer, newPrice * 2); + const uint64 secondBalBefore = getBalance(secondBuyer); + const RL::BuyTicket_output secondBuyOut = ctl.buyTicket(secondBuyer, newPrice); + EXPECT_EQ(secondBuyOut.returnCode, RL::EReturnCode::SUCCESS); + const uint64 playersAfterBuy = playersAfterFirstBuy + 1; + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); + EXPECT_EQ(getBalance(secondBuyer), secondBalBefore - newPrice); + + // Draws should only trigger on Wednesdays now: starting on Wednesday means the draw + // is deferred until the next Wednesday in the schedule. + const uint64 winnersBefore = ctl.getWinners().winnersCounter; + ctl.setDateTime(2025, 1, 15, RL_DEFAULT_DRAW_HOUR + 1); // current Wednesday + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); + + // No draw on non-scheduled days between Wednesdays + ctl.setDateTime(2025, 1, 21, RL_DEFAULT_DRAW_HOUR + 1); // Tuesday next week + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersAfterBuy); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); + + // Next Wednesday processes the draw + ctl.setDateTime(2025, 1, 22, RL_DEFAULT_DRAW_HOUR + 1); // next Wednesday + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore + 1); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_LOCKED); + + // After the draw and before the next epoch begins, ticket purchases are blocked + const id lockedBuyer = id::randomValue(); + increaseEnergy(lockedBuyer, newPrice); + const RL::BuyTicket_output lockedOut = ctl.buyTicket(lockedBuyer, newPrice); + EXPECT_EQ(lockedOut.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); +} + +TEST(ContractRandomLottery, DefaultInitTimeGuardSkipsPlaceholderDate) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + // Allow draws every day so weekday logic does not block BEGIN_TICK + ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); + + // Simulate the placeholder 2022-04-13 QPI date during initialization + ctl.setDateTime(2022, 4, 13, RL_DEFAULT_DRAW_HOUR + 1); + ctl.BeginEpoch(); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_LOCKED); + + // Selling is blocked until a valid date arrives + const id blockedBuyer = id::randomValue(); + increaseEnergy(blockedBuyer, ticketPrice); + const RL::BuyTicket_output denied = ctl.buyTicket(blockedBuyer, ticketPrice); + EXPECT_EQ(denied.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + + const uint64 winnersBefore = ctl.getWinners().winnersCounter; + + // BEGIN_TICK should detect the placeholder date and skip processing, but remember the sentinel day + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_LOCKED); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore); + + // First valid day re-opens selling but still skips the draw + ctl.setDateTime(2025, 1, 10, RL_DEFAULT_DRAW_HOUR + 1); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); + EXPECT_NE(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); + + const id playerA = id::randomValue(); + const id playerB = id::randomValue(); + ctl.increaseAndBuy(ctl, playerA, ticketPrice); + ctl.increaseAndBuy(ctl, playerB, ticketPrice); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 2u); + + // The immediate next valid day should run the actual draw + ctl.setDateTime(2025, 1, 11, RL_DEFAULT_DRAW_HOUR + 1); + ctl.forceBeginTick(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBefore + 1); + EXPECT_NE(ctl.state()->getLastDrawDateStamp(), RL_DEFAULT_INIT_TIME); +} + +TEST(ContractRandomLottery, SellingUnlocksWhenTimeSetBeforeScheduledDay) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + ctl.setDateTime(2022, 4, 13, RL_DEFAULT_DRAW_HOUR + 1); + ctl.BeginEpoch(); + + const id deniedBuyer = id::randomValue(); + increaseEnergy(deniedBuyer, ticketPrice); + EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); + + ctl.setDateTime(2025, 1, 14, RL_DEFAULT_DRAW_HOUR + 2); // Tuesday, not scheduled by default + ctl.forceBeginTick(); + + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); + EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), 0u); + + const id allowedBuyer = id::randomValue(); + increaseEnergy(allowedBuyer, ticketPrice); + const RL::BuyTicket_output allowed = ctl.buyTicket(allowedBuyer, ticketPrice); + EXPECT_EQ(allowed.returnCode, RL::EReturnCode::SUCCESS); +} + +TEST(ContractRandomLottery, SellingUnlocksWhenTimeSetOnDrawDay) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + ctl.setDateTime(2022, 4, 13, RL_DEFAULT_DRAW_HOUR + 1); + ctl.BeginEpoch(); + + const id deniedBuyer = id::randomValue(); + increaseEnergy(deniedBuyer, ticketPrice); + EXPECT_EQ(ctl.buyTicket(deniedBuyer, ticketPrice).returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); + + ctl.setDateTime(2025, 1, 15, RL_DEFAULT_DRAW_HOUR + 2); // Wednesday draw day + ctl.forceBeginTick(); + + const uint32 expectedStamp = makeDateStamp(2025, 1, 15); + EXPECT_EQ(ctl.state()->getLastDrawDateStamp(), expectedStamp); + EXPECT_EQ(ctl.getStateInfo().currentState, STATE_SELLING); + + const id allowedBuyer = id::randomValue(); + increaseEnergy(allowedBuyer, ticketPrice); + const RL::BuyTicket_output allowed = ctl.buyTicket(allowedBuyer, ticketPrice); + EXPECT_EQ(allowed.returnCode, RL::EReturnCode::SUCCESS); +} + +TEST(ContractRandomLottery, PostIncomingTransfer) +{ + ContractTestingRL ctl; + static constexpr uint64 transferAmount = 123456789; + + const id sender = id::randomValue(); + increaseEnergy(sender, transferAmount); + EXPECT_EQ(getBalance(sender), transferAmount); + + const id contractAddress = ctl.rlSelf(); + EXPECT_EQ(getBalance(contractAddress), 0); + + notifyContractOfIncomingTransfer(sender, contractAddress, transferAmount, QPI::TransferType::standardTransaction); + + EXPECT_EQ(getBalance(sender), transferAmount); + EXPECT_EQ(getBalance(contractAddress), 0); +} + +TEST(ContractRandomLottery, GetFees) +{ + ContractTestingRL ctl; + RL::GetFees_output output = ctl.getFees(); + ctl.state()->checkFees(output); +} + +TEST(ContractRandomLottery, GetPlayers) +{ + ContractTestingRL ctl; + + // Initially empty + RL::GetPlayers_output output = ctl.getPlayers(); + ctl.state()->checkPlayers(output); + + // Add random players directly to state (test helper) + constexpr uint64 maxPlayersToAdd = 10; + ctl.state()->randomlyAddPlayers(maxPlayersToAdd); + output = ctl.getPlayers(); + ctl.state()->checkPlayers(output); +} + +TEST(ContractRandomLottery, GetWinners) +{ + ContractTestingRL ctl; + + // Populate winners history artificially + constexpr uint64 maxNewWinners = 10; + ctl.state()->randomlyAddWinners(maxNewWinners); + RL::GetWinners_output winnersOutput = ctl.getWinners(); + ctl.state()->checkWinners(winnersOutput); +} + +TEST(ContractRandomLottery, BuyTicket) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + // 1. Attempt when state is LOCKED (should fail and refund invocation reward) + { + const id userLocked = id::randomValue(); + increaseEnergy(userLocked, ticketPrice * 2); + RL::BuyTicket_output out = ctl.buyTicket(userLocked, ticketPrice); + EXPECT_EQ(out.returnCode, RL::EReturnCode::TICKET_SELLING_CLOSED); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0); + } + + // Switch to SELLING to allow purchases + ctl.beginEpochWithValidTime(); + + // 2. Loop over several users and test invalid price, success, duplicate + constexpr uint64 userCount = 5; + uint64 expectedPlayers = 0; + + for (uint64 i = 0; i < userCount; ++i) + { + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 5); + + // (a) Invalid price (wrong reward sent) — player not added + { + // < ticketPrice + RL::BuyTicket_output outInvalid = ctl.buyTicket(user, ticketPrice - 1); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); + + // == 0 + outInvalid = ctl.buyTicket(user, 0); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); + + // < 0 + outInvalid = ctl.buyTicket(user, -1LL * ticketPrice); + EXPECT_NE(outInvalid.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); + } + + // (b) Valid purchase — player added + { + const RL::BuyTicket_output outOk = ctl.buyTicket(user, ticketPrice); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); + ++expectedPlayers; + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); + } + + // (c) Duplicate purchase — allowed, increases count + { + const RL::BuyTicket_output outDup = ctl.buyTicket(user, ticketPrice); + EXPECT_EQ(outDup.returnCode, RL::EReturnCode::SUCCESS); + ++expectedPlayers; + EXPECT_EQ(ctl.state()->getPlayerCounter(), expectedPlayers); + } + } + + // 3. Sanity check: number of tickets equals twice the number of users (due to duplicate buys) + EXPECT_EQ(ctl.state()->getPlayerCounter(), userCount * 2); +} + +// Updated: payout is triggered by BEGIN_TICK with schedule/time gating, not by END_EPOCH +TEST(ContractRandomLottery, DrawAndPayout_BeginTick) +{ + ContractTestingRL ctl; + + const id contractAddress = ctl.rlSelf(); + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + // Current fee configuration (set in INITIALIZE) + const RL::GetFees_output fees = ctl.getFees(); + const uint8 teamPercent = fees.teamFeePercent; // Team commission percent + const uint8 winnerPercent = fees.winnerFeePercent; // Winner payout percent + + // Ensure schedule allows draw any day + ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); + + // --- Scenario 1: No players (nothing to payout, no winner recorded) --- + { + ctl.beginEpochWithValidTime(); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + + RL::GetWinners_output before = ctl.getWinners(); + const uint64 winnersBefore = before.winnersCounter; + + // Need to move to a new day and call BEGIN_TICK to allow draw + ctl.advanceOneDayAndDraw(); + + RL::GetWinners_output after = ctl.getWinners(); + EXPECT_EQ(after.winnersCounter, winnersBefore); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + } + + // --- Scenario 2: Exactly one player (ticket refunded, no winner recorded) --- + { + ctl.beginEpochWithValidTime(); + + const id solo = id::randomValue(); + increaseEnergy(solo, ticketPrice); + const uint64 balanceBefore = getBalance(solo); + + const RL::BuyTicket_output out = ctl.buyTicket(solo, ticketPrice); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 1u); + EXPECT_EQ(getBalance(solo), balanceBefore - ticketPrice); + + const uint64 winnersBeforeCount = ctl.getWinners().winnersCounter; + + ctl.advanceOneDayAndDraw(); + + // Refund happened + EXPECT_EQ(getBalance(solo), balanceBefore); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + + const RL::GetWinners_output winners = ctl.getWinners(); + // No new winners appended + EXPECT_EQ(winners.winnersCounter, winnersBeforeCount); + } + + // --- Scenario 2b: Multiple tickets from the same player are treated as single participant --- + { + ctl.beginEpochWithValidTime(); + + const id solo = id::randomValue(); + increaseEnergy(solo, ticketPrice * 10); + const uint64 balanceBefore = getBalance(solo); + + for (int i = 0; i < 5; ++i) + { + EXPECT_EQ(ctl.buyTicket(solo, ticketPrice).returnCode, RL::EReturnCode::SUCCESS); + } + EXPECT_EQ(ctl.state()->getPlayerCounter(), 5u); + + const uint64 winnersBeforeCount = ctl.getWinners().winnersCounter; + + ctl.advanceOneDayAndDraw(); + + // All tickets refunded, no winner recorded + EXPECT_EQ(getBalance(solo), balanceBefore); + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersBeforeCount); + EXPECT_EQ(getBalance(contractAddress), 0u); + } + + // --- Scenario 3: Multiple players (winner chosen, fees processed, correct remaining on contract) --- + { + ctl.beginEpochWithValidTime(); + + constexpr uint32 N = 5 * 2; + struct PlayerInfo + { + id addr; + uint64 balanceBefore; + uint64 balanceAfterBuy; + }; + std::vector infos; + infos.reserve(N); + + // Add N/2 distinct players, each making two valid purchases + for (uint32 i = 0; i < N; i += 2) + { + const id randomUser = id::randomValue(); + ctl.increaseAndBuy(ctl, randomUser, ticketPrice); + ctl.increaseAndBuy(ctl, randomUser, ticketPrice); + const uint64 bBefore = getBalance(randomUser); + infos.push_back({randomUser, bBefore + (ticketPrice * 2), bBefore}); // account for ticket deduction + } + + EXPECT_EQ(ctl.state()->getPlayerCounter(), N); + + const uint64 contractBalanceBefore = getBalance(contractAddress); + EXPECT_EQ(contractBalanceBefore, ticketPrice * N); + + const uint64 teamBalanceBefore = getBalance(RL_DEV_ADDRESS); + + const RL::GetWinners_output winnersBefore = ctl.getWinners(); + const uint64 winnersCountBefore = winnersBefore.winnersCounter; + + ctl.advanceOneDayAndDraw(); + + // Players reset after draw + EXPECT_EQ(ctl.state()->getPlayerCounter(), 0u); + + const RL::GetWinners_output winnersAfter = ctl.getWinners(); + EXPECT_EQ(winnersAfter.winnersCounter, winnersCountBefore + 1); + + // Newly appended winner info + const RL::WinnerInfo wi = winnersAfter.winners.get(mod(winnersCountBefore, winnersAfter.winners.capacity())); + EXPECT_NE(wi.winnerAddress, id::zero()); + EXPECT_EQ(wi.revenue, (ticketPrice * N * winnerPercent) / 100); + + // Winner address must be one of the players + bool found = false; + for (const PlayerInfo& inf : infos) + { + if (inf.addr == wi.winnerAddress) + { + found = true; + break; + } + } + EXPECT_TRUE(found); + + // Check winner balance increment and others unchanged + for (const PlayerInfo& inf : infos) + { + const uint64 bal = getBalance(inf.addr); + const uint64 balanceAfterBuy = inf.addr == wi.winnerAddress ? inf.balanceAfterBuy + wi.revenue : inf.balanceAfterBuy; + EXPECT_EQ(bal, balanceAfterBuy); + } + + // Team fee transferred + const uint64 teamFeeExpected = (ticketPrice * N * teamPercent) / 100; + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamBalanceBefore + teamFeeExpected); + + // Burn (remaining on contract) + const uint64 burnExpected = ctl.expectedRemainingAfterPayout(contractBalanceBefore, fees); + EXPECT_EQ(getBalance(contractAddress), burnExpected); + } + + // --- Scenario 4: Several consecutive draws (winners accumulate, balances consistent) --- + { + const uint32 rounds = 3; + const uint32 playersPerRound = 6 * 2; // even number to mimic duplicates if desired + + // Remember starting winners count and team balance + const uint64 winnersStart = ctl.getWinners().winnersCounter; + const uint64 teamStartBal = getBalance(RL_DEV_ADDRESS); + + uint64 teamAccrued = 0; + + for (uint32 r = 0; r < rounds; ++r) + { + ctl.beginEpochWithValidTime(); + + struct P + { + id addr; + uint64 balAfterBuy; + }; + std::vector

roundPlayers; + roundPlayers.reserve(playersPerRound); + + // Each player buys two tickets in this round + for (uint32 i = 0; i < playersPerRound; i += 2) + { + const id u = id::randomValue(); + ctl.increaseAndBuy(ctl, u, ticketPrice); + ctl.increaseAndBuy(ctl, u, ticketPrice); + const uint64 balAfter = getBalance(u); + roundPlayers.push_back({u, balAfter}); + } + + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersPerRound); + + const uint64 winnersBefore = ctl.getWinners().winnersCounter; + const uint64 contractBefore = getBalance(contractAddress); + const uint64 teamBalBeforeRound = getBalance(RL_DEV_ADDRESS); + + ctl.advanceOneDayAndDraw(); + + // Winners should increase by exactly one + const RL::GetWinners_output wOut = ctl.getWinners(); + EXPECT_EQ(wOut.winnersCounter, winnersBefore + 1); + + // Validate winner entry + const RL::WinnerInfo newWi = wOut.winners.get(mod(winnersBefore, wOut.winners.capacity())); + EXPECT_NE(newWi.winnerAddress, id::zero()); + EXPECT_EQ(newWi.revenue, (contractBefore * winnerPercent) / 100); + + // Winner must be one of the current round players + bool inRound = false; + for (const auto& p : roundPlayers) + { + if (p.addr == newWi.winnerAddress) + { + inRound = true; + break; + } + } + EXPECT_TRUE(inRound); + + // Check players' balances after payout + for (const auto& p : roundPlayers) + { + const uint64 b = getBalance(p.addr); + const uint64 expected = (p.addr == newWi.winnerAddress) ? (p.balAfterBuy + newWi.revenue) : p.balAfterBuy; + EXPECT_EQ(b, expected); + } + + // Team fee for the whole round's contract balance + const uint64 teamFee = (contractBefore * teamPercent) / 100; + teamAccrued += teamFee; + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamBalBeforeRound + teamFee); + + // Contract remaining should match expected + const uint64 expectedRemaining = ctl.expectedRemainingAfterPayout(contractBefore, fees); + EXPECT_EQ(getBalance(contractAddress), expectedRemaining); + } + + // After all rounds winners increased by rounds and team received cumulative fees + EXPECT_EQ(ctl.getWinners().winnersCounter, winnersStart + rounds); + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamStartBal + teamAccrued); + } +} +TEST(ContractRandomLottery, GetBalance) +{ + ContractTestingRL ctl; + + const id contractAddress = ctl.rlSelf(); + const uint64 ticketPrice = ctl.state()->getTicketPrice(); + + // Initially, contract balance is 0 + { + const RL::GetBalance_output out0 = ctl.getBalanceInfo(); + EXPECT_EQ(out0.balance, 0u); + EXPECT_EQ(out0.balance, getBalance(contractAddress)); + } + + // Open selling and perform several purchases + ctl.beginEpochWithValidTime(); + + constexpr uint32 K = 3; + for (uint32 i = 0; i < K; ++i) + { + const id user = id::randomValue(); + ctl.increaseAndBuy(ctl, user, ticketPrice); + ctl.expectContractBalanceEqualsGetBalance(ctl, contractAddress); + } + + // Before draw, balance equals the total cost of tickets + { + const RL::GetBalance_output outBefore = ctl.getBalanceInfo(); + EXPECT_EQ(outBefore.balance, ticketPrice * K); + } + + // Trigger draw and verify expected remaining amount against contract balance and function output + const uint64 contractBalanceBefore = getBalance(contractAddress); + const RL::GetFees_output fees = ctl.getFees(); + + // Ensure schedule allows draw and perform it + ctl.forceSchedule(RL_ANY_DAY_DRAW_SCHEDULE); + ctl.advanceOneDayAndDraw(); + + const RL::GetBalance_output outAfter = ctl.getBalanceInfo(); + const uint64 envAfter = getBalance(contractAddress); + EXPECT_EQ(outAfter.balance, envAfter); + + const uint64 expectedRemaining = ctl.expectedRemainingAfterPayout(contractBalanceBefore, fees); + EXPECT_EQ(outAfter.balance, expectedRemaining); +} + +TEST(ContractRandomLottery, GetTicketPrice) +{ + ContractTestingRL ctl; + + const RL::GetTicketPrice_output out = ctl.getTicketPrice(); + EXPECT_EQ(out.ticketPrice, ctl.state()->getTicketPrice()); +} + +TEST(ContractRandomLottery, GetMaxNumberOfPlayers) +{ + ContractTestingRL ctl; + + const RL::GetMaxNumberOfPlayers_output out = ctl.getMaxNumberOfPlayers(); + // Compare against the known constant via GetPlayers capacity + const RL::GetPlayers_output playersOut = ctl.getPlayers(); + EXPECT_EQ(static_cast(out.numberOfPlayers), static_cast(playersOut.players.capacity())); +} + +TEST(ContractRandomLottery, GetState) +{ + ContractTestingRL ctl; + + // Initially LOCKED + { + const RL::GetState_output out0 = ctl.getStateInfo(); + EXPECT_EQ(out0.currentState, STATE_LOCKED); + } + + // After BeginEpoch — SELLING + ctl.beginEpochWithValidTime(); + { + const RL::GetState_output out1 = ctl.getStateInfo(); + EXPECT_EQ(out1.currentState, STATE_SELLING); + } + + // After END_EPOCH — back to LOCKED (selling disabled until next epoch) + ctl.EndEpoch(); + { + const RL::GetState_output out2 = ctl.getStateInfo(); + EXPECT_EQ(out2.currentState, STATE_LOCKED); + } +} + +// --- New tests for SetPrice and NextEpochData --- + +TEST(ContractRandomLottery, SetPrice_AccessControl) +{ + ContractTestingRL ctl; + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 newPrice = oldPrice * 2; + + // Random user must not have permission + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, 1); + + const RL::SetPrice_output outDenied = ctl.setPrice(randomUser, newPrice); + EXPECT_EQ(outDenied.returnCode, RL::EReturnCode::ACCESS_DENIED); + + // Price doesn't change immediately nor after END_EPOCH implicitly + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); +} + +TEST(ContractRandomLottery, SetPrice_ZeroNotAllowed) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + + const RL::SetPrice_output outInvalid = ctl.setPrice(RL_DEV_ADDRESS, 0); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::TICKET_INVALID_PRICE); + + // Price remains unchanged even after END_EPOCH + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); +} + +TEST(ContractRandomLottery, SetPrice_AppliesAfterEndEpoch) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 newPrice = oldPrice * 2; + + const RL::SetPrice_output outOk = ctl.setPrice(RL_DEV_ADDRESS, newPrice); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); + + // Check NextEpochData reflects pending change + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, newPrice); + + // Until END_EPOCH the price remains unchanged + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + + // Applied after END_EPOCH + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); + + // NextEpochData cleared + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, 0u); + + // Another END_EPOCH without a new SetPrice doesn't change the price + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); +} + +TEST(ContractRandomLottery, SetPrice_OverrideBeforeEndEpoch) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 firstPrice = oldPrice + 1000; + const uint64 secondPrice = oldPrice + 7777; + + // Two SetPrice calls before END_EPOCH — the last one should apply + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, firstPrice).returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.setPrice(RL_DEV_ADDRESS, secondPrice).returnCode, RL::EReturnCode::SUCCESS); + + // NextEpochData shows the last queued value + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, secondPrice); + + // Until END_EPOCH the old price remains + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, oldPrice); + + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, secondPrice); +} + +TEST(ContractRandomLottery, SetPrice_AffectsNextEpochBuys) +{ + ContractTestingRL ctl; + + increaseEnergy(RL_DEV_ADDRESS, 1); + + const uint64 oldPrice = ctl.state()->getTicketPrice(); + const uint64 newPrice = oldPrice * 3; + + // Open selling and buy at the old price + ctl.beginEpochWithValidTime(); + const id u1 = id::randomValue(); + increaseEnergy(u1, oldPrice * 2); + { + const RL::BuyTicket_output out1 = ctl.buyTicket(u1, oldPrice); + EXPECT_EQ(out1.returnCode, RL::EReturnCode::SUCCESS); + } + + // Set a new price, but before END_EPOCH purchases should use the old price logic (split by old price) + { + const RL::SetPrice_output setOut = ctl.setPrice(RL_DEV_ADDRESS, newPrice); + EXPECT_EQ(setOut.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.newPrice, newPrice); + } + + const id u2 = id::randomValue(); + increaseEnergy(u2, newPrice * 2); + { + const uint64 balBefore = getBalance(u2); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output outNow = ctl.buyTicket(u2, newPrice); + EXPECT_EQ(outNow.returnCode, RL::EReturnCode::SUCCESS); + // floor(newPrice/oldPrice) tickets were bought, the remainder was refunded + const uint64 bought = newPrice / oldPrice; + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + bought); + EXPECT_EQ(getBalance(u2), balBefore - bought * oldPrice); + } + + // END_EPOCH: new price will apply + ctl.EndEpoch(); + EXPECT_EQ(ctl.getTicketPrice().ticketPrice, newPrice); + + // In the next epoch, a purchase at the new price should succeed exactly once per price + ctl.beginEpochWithValidTime(); + { + const uint64 balBefore = getBalance(u2); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output outOk = ctl.buyTicket(u2, newPrice); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + 1); + EXPECT_EQ(getBalance(u2), balBefore - newPrice); + } +} + +TEST(ContractRandomLottery, BuyMultipleTickets_ExactMultiple_NoRemainder) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + const uint64 price = ctl.state()->getTicketPrice(); + const id user = id::randomValue(); + constexpr uint64 k = 7; + increaseEnergy(user, price * k); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output out = ctl.buyTicket(user, price * k); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + k); +} + +TEST(ContractRandomLottery, BuyMultipleTickets_WithRemainder_Refunded) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + const uint64 price = ctl.state()->getTicketPrice(); + const id user = id::randomValue(); + constexpr uint64 k = 5; + const uint64 r = price / 3; // partial remainder + increaseEnergy(user, price * k + r); + const uint64 balBefore = getBalance(user); + const uint64 playersBefore = ctl.state()->getPlayerCounter(); + const RL::BuyTicket_output out = ctl.buyTicket(user, price * k + r); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getPlayerCounter(), playersBefore + k); + // Remainder refunded, only k * price spent + EXPECT_EQ(getBalance(user), balBefore - k * price); +} + +TEST(ContractRandomLottery, BuyMultipleTickets_CapacityPartialRefund) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + const uint64 price = ctl.state()->getTicketPrice(); + const uint64 capacity = ctl.getPlayers().players.capacity(); + + // Fill almost up to capacity + const uint64 toFill = (capacity > 5) ? (capacity - 5) : 0; + for (uint64 i = 0; i < toFill; ++i) + { + const id u = id::randomValue(); + increaseEnergy(u, price); + EXPECT_EQ(ctl.buyTicket(u, price).returnCode, RL::EReturnCode::SUCCESS); + } + EXPECT_EQ(ctl.state()->getPlayerCounter(), toFill); + + // Try to buy 10 tickets — only remaining 5 accepted, the rest refunded + const id buyer = id::randomValue(); + increaseEnergy(buyer, price * 10); + const uint64 balBefore = getBalance(buyer); + const RL::BuyTicket_output out = ctl.buyTicket(buyer, price * 10); + EXPECT_EQ(out.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.state()->getPlayerCounter(), capacity); + EXPECT_EQ(getBalance(buyer), balBefore - price * 5); +} + +TEST(ContractRandomLottery, BuyMultipleTickets_AllSoldOut) +{ + ContractTestingRL ctl; + ctl.beginEpochWithValidTime(); + const uint64 price = ctl.state()->getTicketPrice(); + const uint64 capacity = ctl.getPlayers().players.capacity(); + + // Fill to capacity + for (uint64 i = 0; i < capacity; ++i) + { + const id u = id::randomValue(); + increaseEnergy(u, price); + EXPECT_EQ(ctl.buyTicket(u, price).returnCode, RL::EReturnCode::SUCCESS); + } + EXPECT_EQ(ctl.state()->getPlayerCounter(), capacity); + + // Any purchase refunds the full amount and returns ALL_SOLD_OUT code + const id buyer = id::randomValue(); + increaseEnergy(buyer, price * 3); + const uint64 balBefore = getBalance(buyer); + const RL::BuyTicket_output out = ctl.buyTicket(buyer, price * 3); + EXPECT_EQ(out.returnCode, RL::EReturnCode::TICKET_ALL_SOLD_OUT); + EXPECT_EQ(getBalance(buyer), balBefore); +} + +// functions related to schedule and draw hour + +TEST(ContractRandomLottery, GetSchedule_And_SetSchedule) +{ + ContractTestingRL ctl; + + // Default schedule set on initialize must include Wednesday (bit 0) + const RL::GetSchedule_output s0 = ctl.getSchedule(); + EXPECT_NE(s0.schedule, 0u); + + // Access control: random user cannot set schedule + const id rnd = id::randomValue(); + increaseEnergy(rnd, 1); + const RL::SetSchedule_output outDenied = ctl.setSchedule(rnd, RL_ANY_DAY_DRAW_SCHEDULE); + EXPECT_EQ(outDenied.returnCode, RL::EReturnCode::ACCESS_DENIED); + + // Invalid value: zero mask not allowed + increaseEnergy(RL_DEV_ADDRESS, 1); + const RL::SetSchedule_output outInvalid = ctl.setSchedule(RL_DEV_ADDRESS, 0); + EXPECT_EQ(outInvalid.returnCode, RL::EReturnCode::INVALID_VALUE); + + // Valid update queues into NextEpochData and applies after END_EPOCH + const uint8 newMask = 0x5A; // some non-zero mask (bits set for selected days) + const RL::SetSchedule_output outOk = ctl.setSchedule(RL_DEV_ADDRESS, newMask); + EXPECT_EQ(outOk.returnCode, RL::EReturnCode::SUCCESS); + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.schedule, newMask); + + // Not applied yet + EXPECT_NE(ctl.getSchedule().schedule, newMask); + + // Apply + ctl.EndEpoch(); + EXPECT_EQ(ctl.getSchedule().schedule, newMask); + EXPECT_EQ(ctl.getNextEpochData().nextEpochData.schedule, 0u); +} + +TEST(ContractRandomLottery, GetDrawHour_DefaultAfterBeginEpoch) +{ + ContractTestingRL ctl; + + // Initially drawHour is 0 (not configured) + EXPECT_EQ(ctl.getDrawHour().drawHour, 0u); + + // After BeginEpoch default is 11 UTC + ctl.beginEpochWithValidTime(); + EXPECT_EQ(ctl.getDrawHour().drawHour, RL_DEFAULT_DRAW_HOUR); +} diff --git a/test/contract_testex.cpp b/test/contract_testex.cpp index a78d4358b..9b4c7490a 100644 --- a/test/contract_testex.cpp +++ b/test/contract_testex.cpp @@ -11,6 +11,7 @@ static const id TESTEXC_CONTRACT_ID(TESTEXC_CONTRACT_INDEX, 0, 0, 0); static const id USER1(123, 456, 789, 876); static const id USER2(42, 424, 4242, 42424); static const id USER3(98, 76, 54, 3210); +static const id USER4(9878, 7645, 541, 3210); void checkPreManagementRightsTransferInput(const PreManagementRightsTransfer_input& observed, const PreManagementRightsTransfer_input& expected) { @@ -66,6 +67,16 @@ class StateCheckerTestExampleA : public TESTEXA { return this->prevPostAcquireSharesInput; } + + void checkVariablesSetByProposal( + uint64 expectedVariable1, + uint32 expectedVariable2, + sint8 expectedVariable3) const + { + EXPECT_EQ(this->dummyStateVariable1, expectedVariable1); + EXPECT_EQ(this->dummyStateVariable2, expectedVariable2); + EXPECT_EQ(this->dummyStateVariable3, expectedVariable3); + } }; class StateCheckerTestExampleB : public TESTEXB @@ -100,6 +111,16 @@ class StateCheckerTestExampleB : public TESTEXB { return this->prevPostAcquireSharesInput; } + + void checkVariablesSetByProposal( + sint64 expectedVariable1, + sint64 expectedVariable2, + sint64 expectedVariable3) const + { + EXPECT_EQ(this->fee1, expectedVariable1); + EXPECT_EQ(this->fee2, expectedVariable2); + EXPECT_EQ(this->fee3, expectedVariable3); + } }; class ContractTestingTestEx : protected ContractTesting @@ -159,37 +180,37 @@ class ContractTestingTestEx : protected ContractTesting return output.issuedNumberOfShares; } - sint64 transferShareOwnershipAndPossessionQx(const Asset& asset, const id& currentOwnerAndPossesor, const id& newOwnerAndPossesor, sint64 numberOfShares) + sint64 transferShareOwnershipAndPossessionQx(const Asset& asset, const id& currentOwnerAndPossessor, const id& newOwnerAndPossessor, sint64 numberOfShares) { QX::TransferShareOwnershipAndPossession_input input; QX::TransferShareOwnershipAndPossession_output output; input.assetName = asset.assetName; input.issuer = asset.issuer; - input.newOwnerAndPossessor = newOwnerAndPossesor; + input.newOwnerAndPossessor = newOwnerAndPossessor; input.numberOfShares = numberOfShares; - invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, currentOwnerAndPossesor, qxFees.transferFee); + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, currentOwnerAndPossessor, qxFees.transferFee); return output.transferredNumberOfShares; } template - sint64 transferShareOwnershipAndPossession(const Asset& asset, const id& currentOwnerAndPossesor, const id& newOwnerAndPossesor, sint64 numberOfShares) + sint64 transferShareOwnershipAndPossession(const Asset& asset, const id& currentOwnerAndPossessor, const id& newOwnerAndPossessor, sint64 numberOfShares) { typename StateStruct::TransferShareOwnershipAndPossession_input input; typename StateStruct::TransferShareOwnershipAndPossession_output output; input.asset = asset; - input.newOwnerAndPossessor = newOwnerAndPossesor; + input.newOwnerAndPossessor = newOwnerAndPossessor; input.numberOfShares = numberOfShares; - invokeUserProcedure(StateStruct::__contract_index, 2, input, output, currentOwnerAndPossesor, 0); + invokeUserProcedure(StateStruct::__contract_index, 2, input, output, currentOwnerAndPossessor, 0); return output.transferredNumberOfShares; } - sint64 transferShareManagementRightsQx(const Asset& asset, const id& currentOwnerAndPossesor, sint64 numberOfShares, unsigned int newManagingContractIndex, sint64 fee = 0) + sint64 transferShareManagementRightsQx(const Asset& asset, const id& currentOwnerAndPossessor, sint64 numberOfShares, unsigned int newManagingContractIndex, sint64 fee = 0) { QX::TransferShareManagementRights_input input; QX::TransferShareManagementRights_output output; @@ -198,13 +219,13 @@ class ContractTestingTestEx : protected ContractTesting input.newManagingContractIndex = newManagingContractIndex; input.numberOfShares = numberOfShares; - invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, currentOwnerAndPossesor, fee); + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, currentOwnerAndPossessor, fee); return output.transferredNumberOfShares; } template - sint64 transferShareManagementRights(const Asset& asset, const id& currentOwnerAndPossesor, sint64 numberOfShares, unsigned int newManagingContractIndex, sint64 fee = 0) + sint64 transferShareManagementRights(const Asset& asset, const id& currentOwnerAndPossessor, sint64 numberOfShares, unsigned int newManagingContractIndex, sint64 fee = 0) { typename StateStruct::TransferShareManagementRights_input input; typename StateStruct::TransferShareManagementRights_output output; @@ -213,7 +234,7 @@ class ContractTestingTestEx : protected ContractTesting input.newManagingContractIndex = newManagingContractIndex; input.numberOfShares = numberOfShares; - invokeUserProcedure(StateStruct::__contract_index, 3, input, output, currentOwnerAndPossesor, fee); + invokeUserProcedure(StateStruct::__contract_index, 3, input, output, currentOwnerAndPossessor, fee); return output.transferredNumberOfShares; } @@ -236,23 +257,23 @@ class ContractTestingTestEx : protected ContractTesting template - sint64 acquireShareManagementRights(const Asset& asset, const id& currentOwnerAndPossesor, sint64 numberOfShares, unsigned int prevManagingContractIndex, sint64 fee = 0, const id& originator = NULL_ID) + sint64 acquireShareManagementRights(const Asset& asset, const id& currentOwnerAndPossessor, sint64 numberOfShares, unsigned int prevManagingContractIndex, sint64 fee = 0, const id& originator = NULL_ID) { typename StateStruct::AcquireShareManagementRights_input input; typename StateStruct::AcquireShareManagementRights_output output; input.asset = asset; - input.ownerAndPossessor = currentOwnerAndPossesor; + input.ownerAndPossessor = currentOwnerAndPossessor; input.oldManagingContractIndex = prevManagingContractIndex; input.numberOfShares = numberOfShares; invokeUserProcedure(StateStruct::__contract_index, 6, input, output, - (isZero(originator)) ? currentOwnerAndPossesor : originator, fee); + (isZero(originator)) ? currentOwnerAndPossessor : originator, fee); return output.transferredNumberOfShares; } - sint64 getTestExAsShareManagementRightsByInvokingTestExB(const Asset& asset, const id& currentOwnerAndPossesor, sint64 numberOfShares, sint64 fee = 0) + sint64 getTestExAsShareManagementRightsByInvokingTestExB(const Asset& asset, const id& currentOwnerAndPossessor, sint64 numberOfShares, sint64 fee = 0) { TESTEXB::GetTestExampleAShareManagementRights_input input; TESTEXB::GetTestExampleAShareManagementRights_output output; @@ -260,12 +281,12 @@ class ContractTestingTestEx : protected ContractTesting input.asset = asset; input.numberOfShares = numberOfShares; - invokeUserProcedure(TESTEXB::__contract_index, 7, input, output, currentOwnerAndPossesor, fee); + invokeUserProcedure(TESTEXB::__contract_index, 7, input, output, currentOwnerAndPossessor, fee); return output.transferredNumberOfShares; } - sint64 getTestExAsShareManagementRightsByInvokingTestExC(const Asset& asset, const id& currentOwnerAndPossesor, sint64 numberOfShares, sint64 fee = 0) + sint64 getTestExAsShareManagementRightsByInvokingTestExC(const Asset& asset, const id& currentOwnerAndPossessor, sint64 numberOfShares, sint64 fee = 0) { TESTEXC::GetTestExampleAShareManagementRights_input input; TESTEXC::GetTestExampleAShareManagementRights_output output; @@ -273,7 +294,7 @@ class ContractTestingTestEx : protected ContractTesting input.asset = asset; input.numberOfShares = numberOfShares; - invokeUserProcedure(TESTEXC::__contract_index, 7, input, output, currentOwnerAndPossesor, fee); + invokeUserProcedure(TESTEXC::__contract_index, 7, input, output, currentOwnerAndPossessor, fee); return output.transferredNumberOfShares; } @@ -341,8 +362,218 @@ class ContractTestingTestEx : protected ContractTesting else return -2; } + + template + uint16 setShareholderProposal(const id& originator, const typename StateStruct::SetShareholderProposal_input& input) + { + typename StateStruct::SetShareholderProposal_output output; + EXPECT_TRUE(invokeUserProcedure(StateStruct::__contract_index, 65534, input, output, originator, 0)); + return output; + } + + template + bool setShareholderVotes(const id& originator, uint16 proposalIndex, const typename StateStruct::ProposalDataT& proposalData, sint64 voteValue) + { + // Contract procedure expects ProposalMultiVoteDataV1, but ProposalSingleVoteDataV1 is compatible + ProposalSingleVoteDataV1 input{ proposalIndex, proposalData.type, proposalData.tick, voteValue }; + typename StateStruct::SetShareholderVotes_output output; + invokeUserProcedure(StateStruct::__contract_index, 65535, input, output, originator, 0, false); + return output; + } + + template + bool setShareholderVotes(const id& originator, uint16 proposalIndex, const typename StateStruct::ProposalDataT& proposalData, + const std::vector>& voteValueCountPairs) + { + ASSERT(voteValueCountPairs.size() <= 8); + ProposalMultiVoteDataV1 input{ proposalIndex, proposalData.type, proposalData.tick }; + input.voteValues.set(0, NO_VOTE_VALUE); // default with no voteValueCountPairs (vote count 0): set all to no votes + for (size_t i = 0; i < voteValueCountPairs.size(); ++i) + { + input.voteValues.set(i, voteValueCountPairs[i].first); + input.voteCounts.set(i, voteValueCountPairs[i].second); + } + typename StateStruct::SetShareholderVotes_output output; + invokeUserProcedure(StateStruct::__contract_index, 65535, input, output, originator, 0); + return output; + } + + TESTEXB::TestInterContractCallError_output testInterContractCallError() + { + TESTEXB::TestInterContractCallError_input input; + input.dummy = 0; + TESTEXB::TestInterContractCallError_output output; + invokeUserProcedure(TESTEXB_CONTRACT_INDEX, 50, input, output, USER1, 0); + return output; + } + + template + std::vector getShareholderProposalIndices(bit activeProposals) + { + typename StateStruct::GetShareholderProposalIndices_input input{ activeProposals, -1 }; + typename StateStruct::GetShareholderProposalIndices_output output; + std::vector indices; + do + { + callFunction(StateStruct::__contract_index, 65532, input, output); + for (uint16 i = 0; i < output.numOfIndices; ++i) + indices.push_back(output.indices.get(i)); + } while (output.numOfIndices == output.indices.capacity()); + return indices; + } + + template + StateStruct::GetShareholderProposal_output getShareholderProposal(uint16 proposalIndex) + { + typename StateStruct::GetShareholderProposal_input input{ proposalIndex }; + typename StateStruct::GetShareholderProposal_output output; + callFunction(StateStruct::__contract_index, 65533, input, output); + return output; + } + + template + ProposalMultiVoteDataV1 getShareholderVotes(uint16 proposalIndex, const id& voter) + { + typename StateStruct::GetShareholderVotes_input input{ voter, proposalIndex }; + typename StateStruct::GetShareholderVotes_output output; + callFunction(StateStruct::__contract_index, 65534, input, output); + return output; + } + + template + ProposalSummarizedVotingDataV1 getShareholderVotingResults(uint16 proposalIndex) + { + typename StateStruct::GetShareholderVotingResults_input input{ proposalIndex }; + typename StateStruct::GetShareholderVotingResults_output output; + callFunction(StateStruct::__contract_index, 65535, input, output); + return output; + } + + uint16 setupShareholderProposalTestExA( + const id& proposer, uint16 type, + bool setVar1 = false, uint64 valueVar1 = 0, + bool setVar2 = false, uint32 valueVar2 = 0, + bool setVar3 = false, sint8 valueVar3 = 0, + bool expectSuccess = true) + { + TESTEXA::SetShareholderProposal_input input; + setMemory(input, 0); + input.proposalData.epoch = system.epoch; + input.proposalData.type = type; + switch (ProposalTypes::cls(type)) + { + case ProposalTypes::Class::Variable: + { + if (setVar1) + { + EXPECT_FALSE(setVar2); + EXPECT_FALSE(setVar3); + input.proposalData.variableOptions.variable = 0; + input.proposalData.variableOptions.value = valueVar1; + } + else if (setVar2) + { + EXPECT_FALSE(setVar1); + EXPECT_FALSE(setVar3); + input.proposalData.variableOptions.variable = 1; + input.proposalData.variableOptions.value = valueVar2; + } + else if (setVar3) + { + EXPECT_FALSE(setVar1); + EXPECT_FALSE(setVar2); + input.proposalData.variableOptions.variable = 2; + input.proposalData.variableOptions.value = valueVar3; + } + break; + } + case ProposalTypes::Class::MultiVariables: + input.multiVarData.hasValueDummyStateVariable1 = setVar1; + input.multiVarData.hasValueDummyStateVariable2 = setVar2; + input.multiVarData.hasValueDummyStateVariable3 = setVar3; + input.multiVarData.optionYesValues.dummyStateVariable1 = valueVar1; + input.multiVarData.optionYesValues.dummyStateVariable2 = valueVar2; + input.multiVarData.optionYesValues.dummyStateVariable3 = valueVar3; + break; + } + uint16 proposalIdx = this->setShareholderProposal(proposer, input); + if (expectSuccess) + EXPECT_NE((int)proposalIdx, (int)INVALID_PROPOSAL_INDEX); + else + EXPECT_EQ((int)proposalIdx, (int)INVALID_PROPOSAL_INDEX); + return proposalIdx; + } + + template + uint16 setProposalInOtherContractAsShareholder(const id& originator, uint16 otherContractIndex, const FullProposalDataT& fullProposalData) + { + typename StateStruct::SetProposalInOtherContractAsShareholder_input input; + copyToBuffer(input, fullProposalData); + input.otherContractIndex = otherContractIndex; + typename StateStruct::SetProposalInOtherContractAsShareholder_output output; + invokeUserProcedure(StateStruct::__contract_index, 40, input, output, originator, 0); + return output.proposalIndex; + } + + template + bool setVotesInOtherContractAsShareholder(const id& originator, uint16 otherContractIndex, uint16 proposalIndex, const ProposalDataT& proposalData, + const std::vector>& voteValueCountPairs) + { + ASSERT(voteValueCountPairs.size() <= 8); + typename StateStruct::SetVotesInOtherContractAsShareholder_input input{ {proposalIndex, proposalData.type, proposalData.tick} }; + input.otherContractIndex = otherContractIndex; + input.voteData.voteValues.set(0, NO_VOTE_VALUE); // default with no voteValueCountPairs (vote count 0): set all to no votes + for (size_t i = 0; i < voteValueCountPairs.size(); ++i) + { + input.voteData.voteValues.set(i, voteValueCountPairs[i].first); + input.voteData.voteCounts.set(i, voteValueCountPairs[i].second); + } + typename StateStruct::SetVotesInOtherContractAsShareholder_output output; + invokeUserProcedure(StateStruct::__contract_index, 41, input, output, originator, 0); + return output.success; + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(TESTEXD_CONTRACT_INDEX, END_EPOCH, expectSuccess); + callSystemProcedure(TESTEXC_CONTRACT_INDEX, END_EPOCH, expectSuccess); + callSystemProcedure(TESTEXB_CONTRACT_INDEX, END_EPOCH, expectSuccess); + callSystemProcedure(TESTEXA_CONTRACT_INDEX, END_EPOCH, expectSuccess); + callSystemProcedure(QX_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } }; +void checkVoteCounts(const ProposalMultiVoteDataV1& votes, const std::vector>& expectedVoteValueCountPairs) +{ + std::vector> expectedPairsNotFound = expectedVoteValueCountPairs; + for (int i = 0; i < votes.voteCounts.capacity(); ++i) + { + sint64 value = votes.voteValues.get(i); + uint32 count = votes.voteCounts.get(i); + std::pair pair(value, count); + auto it = std::find(expectedPairsNotFound.begin(), expectedPairsNotFound.end(), pair); + if (it != expectedPairsNotFound.end()) + { + // value-count pair found + expectedPairsNotFound.erase(it); + } + else if (count) + { + FAIL() << "Error: unexpected vote value/count pair " << value << "/" << count; + } + } + for (const auto& it : expectedPairsNotFound) + { + FAIL() << "Error: missing vote value/count pair " << it.first << "/" << it.second; + } +} + +bool operator==(const TESTEXA::MultiVariablesProposalExtraData& p1, const TESTEXA::MultiVariablesProposalExtraData& p2) +{ + return memcmp(&p1, &p2, sizeof(p1)) == 0; +} + + TEST(ContractTestEx, QpiReleaseShares) { ContractTestingTestEx test; @@ -351,7 +582,7 @@ TEST(ContractTestEx, QpiReleaseShares) const sint64 totalShareCount = 1000000000; const sint64 transferShareCount = totalShareCount/4; - // make sure the enities have enough qu + // make sure the entities have enough qu increaseEnergy(USER1, test.qxFees.assetIssuanceFee * 10); increaseEnergy(USER2, test.qxFees.assetIssuanceFee * 10); increaseEnergy(USER3, test.qxFees.assetIssuanceFee * 10); @@ -504,7 +735,7 @@ TEST(ContractTestEx, QpiAcquireShares) const sint64 totalShareCount = 100000000; const sint64 transferShareCount = totalShareCount / 4; - // make sure the enities have enough qu + // make sure the entities have enough qu increaseEnergy(USER1, test.qxFees.assetIssuanceFee * 10); increaseEnergy(USER2, test.qxFees.assetIssuanceFee * 10); increaseEnergy(USER3, test.qxFees.assetIssuanceFee * 10); @@ -684,7 +915,7 @@ TEST(ContractTestEx, GetManagementRightsByInvokingOtherContractsRelease) const sint64 totalShareCount = 1000000; const sint64 transferShareCount = totalShareCount / 5; - // make sure the enities have enough qu + // make sure the entities have enough qu increaseEnergy(USER1, test.qxFees.assetIssuanceFee * 10); increaseEnergy(USER2, test.qxFees.assetIssuanceFee * 10); increaseEnergy(USER3, test.qxFees.assetIssuanceFee * 10); @@ -1326,3 +1557,523 @@ TEST(ContractTestEx, BurnAssets) EXPECT_EQ(1000000 - 100, numberOfShares(asset, { USER1, QX_CONTRACT_INDEX }, { USER1, QX_CONTRACT_INDEX })); } } + +TEST(ContractTestEx, ShareholderProposals) +{ + ContractTestingTestEx test; + uint16 proposalIdx = 0; + + system.epoch = 200; + + increaseEnergy(USER1, 12345678); + increaseEnergy(USER2, 31427); + increaseEnergy(USER3, 218000); + increaseEnergy(USER4, 218000); + increaseEnergy(TESTEXA_CONTRACT_ID, 987654321); + increaseEnergy(TESTEXB_CONTRACT_ID, 19283764); + + // issue contract shares + std::vector> sharesTestExA{ + {USER1, 356}, + {USER2, 200}, + {TESTEXB_CONTRACT_ID, 100}, + {USER3, 20} + }; + issueContractShares(TESTEXA_CONTRACT_INDEX, sharesTestExA); + + // enable that TESTEXA accepts transfer for 0 fee + test.setPreAcquireSharesOutput(true, 0); + + // transfer management rights of some shares to other contract to cover case of multiple asset records of single possessor + const Asset TESTEXA_ASSET{ NULL_ID, assetNameFromString("TESTEXA") }; + EXPECT_EQ(test.transferShareManagementRightsQx(TESTEXA_ASSET, USER2, 50, TESTEXA_CONTRACT_INDEX), 50); + EXPECT_EQ(numberOfShares(TESTEXA_ASSET, { USER1, QX_CONTRACT_INDEX }, { USER1, QX_CONTRACT_INDEX }), 356); + EXPECT_EQ(numberOfShares(TESTEXA_ASSET, { USER2, QX_CONTRACT_INDEX }, { USER2, QX_CONTRACT_INDEX }), 150); + EXPECT_EQ(numberOfShares(TESTEXA_ASSET, { USER2, TESTEXA_CONTRACT_INDEX }, { USER2, TESTEXA_CONTRACT_INDEX }), 50); + EXPECT_EQ(numberOfShares(TESTEXA_ASSET, { TESTEXB_CONTRACT_ID, QX_CONTRACT_INDEX }, { TESTEXB_CONTRACT_ID, QX_CONTRACT_INDEX }), 100); + EXPECT_EQ(numberOfShares(TESTEXA_ASSET, { USER3, QX_CONTRACT_INDEX }, { USER3, QX_CONTRACT_INDEX }), 20); + EXPECT_EQ(numberOfShares(TESTEXA_ASSET, { USER4, QX_CONTRACT_INDEX }, { USER4, QX_CONTRACT_INDEX }), 0); + + // fail: 4 options not supported with Yes/No proposals + test.setupShareholderProposalTestExA(USER2, ProposalTypes::FourOptions, false, 0, false, 0, false, 0, false); + + // fail: no right, because no shareholder + test.setupShareholderProposalTestExA(USER4, ProposalTypes::ThreeOptions, false, 0, false, 0, false, 0, false); + + // fail: transfer not allowed + test.setupShareholderProposalTestExA(USER2, ProposalTypes::TransferYesNo, false, 0, false, 0, false, 0, false); + + // fail: invalid value of variable + test.setupShareholderProposalTestExA(USER2, ProposalTypes::VariableYesNo, false, 0, false, 0, true, 120, false); + + // check that no active/inactive proposals + EXPECT_EQ(test.getShareholderProposalIndices(true).size(), 0); + EXPECT_EQ(test.getShareholderProposalIndices(false).size(), 0); + + // success: set var3 with single-var proposal + proposalIdx = test.setupShareholderProposalTestExA(USER2, ProposalTypes::VariableYesNo, false, 0, false, 0, true, 100, true); + + // check that no active/inactive proposals + auto proposalIndices = test.getShareholderProposalIndices(true); + EXPECT_TRUE(proposalIndices.size() == 1 && proposalIndices[0] == proposalIdx); + EXPECT_EQ(test.getShareholderProposalIndices(false).size(), 0); + + // fail: try to get non-existing proposal + auto fullProposalData = test.getShareholderProposal(proposalIdx + 1); + EXPECT_EQ(fullProposalData.proposerPubicKey, NULL_ID); + EXPECT_EQ((int)fullProposalData.proposal.type, 0); + + // success: get existing proposal + fullProposalData = test.getShareholderProposal(proposalIdx); + EXPECT_EQ(fullProposalData.proposerPubicKey, USER2); + EXPECT_EQ((int)fullProposalData.proposal.type, (int)ProposalTypes::VariableYesNo); + auto proposal = fullProposalData.proposal; + + // fail: try to get shareholder votes of user who is no shareholder + auto votes = test.getShareholderVotes(proposalIdx, USER4); + EXPECT_EQ((int)votes.proposalType, 0); + + // fail: try to get shareholder votes of non-existing proposal + votes = test.getShareholderVotes(proposalIdx + 1, USER1); + EXPECT_EQ((int)votes.proposalType, 0); + + // success: get shareholder votes of user who is no shareholder + votes = test.getShareholderVotes(proposalIdx, USER1); + EXPECT_EQ((int)votes.proposalType, (int)proposal.type); + EXPECT_EQ((int)votes.proposalIndex, (int)proposalIdx); + EXPECT_EQ(votes.proposalTick, proposal.tick); + checkVoteCounts(votes, {}); + + // set all votes of USER1 to option 0 with single-vote struct + EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdx, proposal, 0)); + + // get shareholder votes of user who is no shareholder and check that they are correct + votes = test.getShareholderVotes(proposalIdx, USER1); + EXPECT_EQ((int)votes.proposalType, (int)proposal.type); + checkVoteCounts(votes, { {0, 356} }); + + // set 50 votes of USER2 to option 0 and 150 to option 1 + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdx, proposal, { {0, 50}, {1, 150} })); + votes = test.getShareholderVotes(proposalIdx, USER2); + EXPECT_EQ((int)votes.proposalType, (int)proposal.type); + checkVoteCounts(votes, { {0, 50}, {1, 150} }); + + // fail: set 51 votes of USER2 to option 1 and 150 to option 0 (more votes than shares) + EXPECT_FALSE(test.setShareholderVotes(USER2, proposalIdx, proposal, { {1, 51}, {0, 150} })); + votes = test.getShareholderVotes(proposalIdx, USER2); + EXPECT_EQ((int)votes.proposalType, (int)proposal.type); + checkVoteCounts(votes, { {0, 50}, {1, 150} }); + + // set 20 votes of USER2 to option 0 and 30 to option 1 (some votes unused) + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdx, proposal, { {0, 20}, {1, 30} })); + votes = test.getShareholderVotes(proposalIdx, USER2); + EXPECT_EQ((int)votes.proposalType, (int)proposal.type); + checkVoteCounts(votes, { {0, 20}, {1, 30} }); + + // fail: try to get voting results of invalid proposal + auto results = test.getShareholderVotingResults(proposalIdx + 1); + EXPECT_EQ(results.totalVotesAuthorized, 0); + + // check voting results + results = test.getShareholderVotingResults(proposalIdx); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ((int)results.optionCount, 2); + EXPECT_EQ(results.optionVoteCount.get(0), 20 + 356); + EXPECT_EQ(results.optionVoteCount.get(1), 30); + EXPECT_EQ(results.totalVotesCasted, 20 + 356 + 30); + EXPECT_EQ(results.getAcceptedOption(), -1); + EXPECT_EQ(results.getMostVotedOption(), 0); + + // set 1 vote of USER3 to option 0 and 19 to option 1 + EXPECT_TRUE(test.setShareholderVotes(USER3, proposalIdx, proposal, { {0, 1}, {1, 19} })); + + // change votes of USER1 + EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdx, proposal, { {0, 300}, {1, 50} })); + + votes = test.getShareholderVotes(proposalIdx, USER3); + EXPECT_EQ((int)votes.proposalType, (int)proposal.type); + checkVoteCounts(votes, { {0, 1}, {1, 19} }); + votes = test.getShareholderVotes(proposalIdx, USER2); + checkVoteCounts(votes, { {0, 20}, {1, 30} }); + votes = test.getShareholderVotes(proposalIdx, USER1); + checkVoteCounts(votes, { {0, 300}, {1, 50} }); + + results = test.getShareholderVotingResults(proposalIdx); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ((int)results.optionCount, 2); + EXPECT_EQ(results.optionVoteCount.get(0), 1 + 20 + 300); + EXPECT_EQ(results.optionVoteCount.get(1), 19 + 30 + 50); + EXPECT_EQ(results.totalVotesCasted, 1 + 20 + 300 + 19 + 30 + 50); + EXPECT_EQ(results.getAcceptedOption(), -1); + EXPECT_EQ(results.getMostVotedOption(), 0); + + // withdraw votes of USER1 and USER3 + EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdx, proposal, std::vector>())); + EXPECT_TRUE(test.setShareholderVotes(USER3, proposalIdx, proposal, NO_VOTE_VALUE)); + + votes = test.getShareholderVotes(proposalIdx, USER3); + checkVoteCounts(votes, {}); + votes = test.getShareholderVotes(proposalIdx, USER2); + checkVoteCounts(votes, { {0, 20}, {1, 30} }); + votes = test.getShareholderVotes(proposalIdx, USER1); + checkVoteCounts(votes, {}); + + results = test.getShareholderVotingResults(proposalIdx); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ(results.optionVoteCount.get(0), 20); + EXPECT_EQ(results.optionVoteCount.get(1), 30); + EXPECT_EQ(results.totalVotesCasted, 20 + 30); + EXPECT_EQ(results.getAcceptedOption(), -1); + EXPECT_EQ(results.getMostVotedOption(), 1); + + // fail: try to set all votes of USER2 to invalid value with single-vote struct + // (uses Multi-Vote internally for testing compatibility, so votes of the user are reset) + EXPECT_FALSE(test.setShareholderVotes(USER2, proposalIdx, proposal, 4)); + votes = test.getShareholderVotes(proposalIdx, USER2); + checkVoteCounts(votes, {}); + + // fail: try to set votes of invalid proposal index + EXPECT_FALSE(test.setShareholderVotes(USER2, 0xffff, proposal, { { 0, 111 } })); + + // fail: try to set votes of inactive proposal + ++system.epoch; + EXPECT_FALSE(test.setShareholderVotes(USER2, proposalIdx, proposal, { { 0, 111 } })); + --system.epoch; + + // fail: try to set votes of with wrong proposal type + proposal.type = ProposalTypes::VariableThreeValues; + EXPECT_FALSE(test.setShareholderVotes(USER2, proposalIdx, proposal, { { 0, 111 } })); + proposal.type = ProposalTypes::VariableYesNo; + + // fail: try to set votes of with wrong proposal tick + ++proposal.tick; + EXPECT_FALSE(test.setShareholderVotes(USER2, proposalIdx, proposal, { { 0, 111 } })); + --proposal.tick; + + // fail: try to set votes for USER4 who is no shareholder + EXPECT_FALSE(test.setShareholderVotes(USER4, proposalIdx, proposal, { { 0, 111 } })); + + // success: set votes with duplicate values in array + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdx, proposal, { { 0, 111 }, {1, 10}, {0, 22}, {1, 3} })); + votes = test.getShareholderVotes(proposalIdx, USER2); + checkVoteCounts(votes, { {0, 133}, {1, 13} }); + + // fail: try to set votes of USER2 to invalid value with multi-vote struct + // (votes of the user are reset) + EXPECT_FALSE(test.setShareholderVotes(USER2, proposalIdx, proposal, { {0, 12}, {1, 23}, {2, 34} })); + votes = test.getShareholderVotes(proposalIdx, USER2); + checkVoteCounts(votes, {}); + + // voting of TESTEXB as shareholder of TESTEXA (originator not checked by procedure) + // user procedure TESTEXB::setVotesInOtherContractAsShareholder + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER4, TESTEXA_CONTRACT_INDEX, proposalIdx, proposal, { {0, 10}, {1, 20}, {0, 70} })); + votes = test.getShareholderVotes(proposalIdx, TESTEXB_CONTRACT_ID); + checkVoteCounts(votes, { {0, 80}, {1, 20} }); + + results = test.getShareholderVotingResults(proposalIdx); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ(results.optionVoteCount.get(0), 80); + EXPECT_EQ(results.optionVoteCount.get(1), 20); + EXPECT_EQ(results.totalVotesCasted, 100); + EXPECT_EQ(results.getAcceptedOption(), -1); + EXPECT_EQ(results.getMostVotedOption(), 0); + + ////////////////////////////////////////////////////// + // create new shareholder proposal in TESTEXA as shareholder TESTEXB + TESTEXA::SetShareholderProposal_input setShareholderProposalInput2; + setShareholderProposalInput2.proposalData.type = ProposalTypes::MultiVariablesYesNo; + setShareholderProposalInput2.proposalData.epoch = system.epoch; + setMemory(setShareholderProposalInput2.multiVarData, 0); + + // fails to create proposal, because multiVarData is invalid (originator not checked by procedure) + uint16 proposalIdx2 = test.setProposalInOtherContractAsShareholder(USER4, TESTEXA_CONTRACT_INDEX, setShareholderProposalInput2); + EXPECT_EQ((int)proposalIdx2, (int)INVALID_PROPOSAL_INDEX); + + // create proposal (originator not checked by procedure) + setShareholderProposalInput2.multiVarData.hasValueDummyStateVariable1 = true; + setShareholderProposalInput2.multiVarData.hasValueDummyStateVariable2 = true; + setShareholderProposalInput2.multiVarData.hasValueDummyStateVariable3 = true; + setShareholderProposalInput2.multiVarData.optionYesValues.dummyStateVariable1 = 1; + setShareholderProposalInput2.multiVarData.optionYesValues.dummyStateVariable2 = 2; + setShareholderProposalInput2.multiVarData.optionYesValues.dummyStateVariable3 = 3; + proposalIdx2 = test.setProposalInOtherContractAsShareholder(USER4, TESTEXA_CONTRACT_INDEX, setShareholderProposalInput2); + + // get and check new proposal + auto fullProposalData2 = test.getShareholderProposal(proposalIdx2); + EXPECT_EQ(fullProposalData2.proposerPubicKey, TESTEXB_CONTRACT_ID); + EXPECT_EQ((int)fullProposalData2.proposal.type, (int)ProposalTypes::MultiVariablesYesNo); + auto proposal2 = fullProposalData2.proposal; + EXPECT_EQ(fullProposalData2.multiVarData, setShareholderProposalInput2.multiVarData); + + // cast votes + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER4, TESTEXA_CONTRACT_INDEX, proposalIdx2, proposal2, { {1, 90} })); + EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdx2, proposal2, { {0, 50}, {1, 260} })); + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdx2, proposal2, { {0, 10}, {1, 160} })); + EXPECT_TRUE(test.setShareholderVotes(USER3, proposalIdx2, proposal2, { {0, 1}, {1, 15} })); + results = test.getShareholderVotingResults(proposalIdx2); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ(results.optionVoteCount.get(0), 61); + EXPECT_EQ(results.optionVoteCount.get(1), 525); + EXPECT_EQ(results.getAcceptedOption(), 1); + EXPECT_EQ(results.totalVotesCasted, 61 + 525); + + // test proposal listing function (2 active, 0 inactive) + proposalIndices = test.getShareholderProposalIndices(true); + EXPECT_TRUE(proposalIndices.size() == 2 && proposalIndices[0] == proposalIdx && proposalIndices[1] == proposalIdx2); + EXPECT_EQ(test.getShareholderProposalIndices(false).size(), 0); + + // test that variables are set correctly after epoch switch + test.getStateTestExampleA()->checkVariablesSetByProposal(0, 0, 0); + test.endEpoch(); + ++system.epoch; + test.getStateTestExampleA()->checkVariablesSetByProposal(1, 2, 3); + + // test proposal listing function (2 inactive by USER2/TESTEXB, 0 active) + proposalIndices = test.getShareholderProposalIndices(false); + EXPECT_TRUE(proposalIndices.size() == 2 && proposalIndices[0] == proposalIdx && proposalIndices[1] == proposalIdx2); + EXPECT_EQ(test.getShareholderProposalIndices(true).size(), 0); + + // Setup proposal to change variable 1 + uint16 proposalIdxA1 = test.setupShareholderProposalTestExA(USER1, ProposalTypes::VariableYesNo, true, 13); + EXPECT_NE((int)proposalIdxA1, (int)INVALID_PROPOSAL_INDEX); + auto proposalDataA1 = test.getShareholderProposal(proposalIdxA1); + auto proposalA1 = proposalDataA1.proposal; + EXPECT_EQ((int)proposalA1.type, (int)ProposalTypes::VariableYesNo); + + // Setup proposal to change variable 2 and 3 + uint16 proposalIdxA2 = test.setupShareholderProposalTestExA(USER2, ProposalTypes::MultiVariablesYesNo, false, 0, true, 4, true, 5); + EXPECT_NE((int)proposalIdxA2, (int)INVALID_PROPOSAL_INDEX); + auto proposalDataA2 = test.getShareholderProposal(proposalIdxA2); + auto proposalA2 = proposalDataA2.proposal; + EXPECT_EQ((int)proposalA2.type, (int)ProposalTypes::MultiVariablesYesNo); + EXPECT_EQ(proposalDataA2.proposerPubicKey, USER2); + EXPECT_EQ(proposalDataA2.multiVarData.optionYesValues.dummyStateVariable2, 4); + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdxA2, proposalA2, { {0, 3} })); + checkVoteCounts(test.getShareholderVotes(proposalIdxA2, USER2), { {0, 3} }); + + // Overwrite proposal to change variable 2 and 3 + proposalIdxA2 = test.setupShareholderProposalTestExA(USER2, ProposalTypes::MultiVariablesYesNo, false, 0, true, 1337, true, 42); + EXPECT_NE((int)proposalIdxA2, (int)INVALID_PROPOSAL_INDEX); + checkVoteCounts(test.getShareholderVotes(proposalIdxA2, USER2), {}); + + /////////////////////////////////////////////////////////////// + // Proposals in TestExB + + // issue contract shares + std::vector> sharesTestExB{ + {TESTEXA_CONTRACT_ID, 256}, + {USER2, 200}, + {USER3, 100}, + {USER4, 120} + }; + issueContractShares(TESTEXB_CONTRACT_INDEX, sharesTestExB); + const Asset TESTEXB_ASSET{ NULL_ID, assetNameFromString("TESTEXB") }; + EXPECT_EQ(numberOfShares(TESTEXB_ASSET, { TESTEXA_CONTRACT_ID, QX_CONTRACT_INDEX }, { TESTEXA_CONTRACT_ID, QX_CONTRACT_INDEX }), 256); + EXPECT_EQ(numberOfShares(TESTEXB_ASSET, { USER2, QX_CONTRACT_INDEX }, { USER2, QX_CONTRACT_INDEX }), 200); + EXPECT_EQ(numberOfShares(TESTEXB_ASSET, { USER3, QX_CONTRACT_INDEX }, { USER3, QX_CONTRACT_INDEX }), 100); + EXPECT_EQ(numberOfShares(TESTEXB_ASSET, { USER4, QX_CONTRACT_INDEX }, { USER4, QX_CONTRACT_INDEX }), 120); + EXPECT_EQ(numberOfShares(TESTEXB_ASSET, { USER1, QX_CONTRACT_INDEX }, { USER1, QX_CONTRACT_INDEX }), 0); + + // Create scalar variable proposal + TESTEXB::ProposalDataT proposalB1; + proposalB1.epoch = system.epoch; + proposalB1.type = ProposalTypes::VariableScalarMean; + proposalB1.variableScalar.variable = 0; + proposalB1.variableScalar.minValue = 0; + proposalB1.variableScalar.maxValue = MAX_AMOUNT; + proposalB1.variableScalar.proposedValue = 1000; + uint16 proposalIdxB1 = test.setShareholderProposal(USER2, { proposalB1 }); + EXPECT_NE((int)proposalIdxB1, (int)INVALID_PROPOSAL_INDEX); + auto proposalDataB1 = test.getShareholderProposal(proposalIdxB1); + proposalB1 = proposalDataB1.proposal; // needed to set tick + EXPECT_EQ((int)proposalDataB1.proposal.type, (int)ProposalTypes::VariableScalarMean); + EXPECT_EQ(proposalDataB1.proposerPubicKey, USER2); + EXPECT_EQ(proposalDataB1.proposal.variableScalar.maxValue, MAX_AMOUNT); + EXPECT_EQ(proposalDataB1.proposal.variableScalar.proposedValue, 1000); + + // Create multi-option variable proposal as shareholder TESTEXA + TESTEXB::ProposalDataT proposalB2; + proposalB2.epoch = system.epoch; + proposalB2.type = ProposalTypes::VariableFourValues; + proposalB2.variableOptions.variable = 1; + proposalB2.variableOptions.values.set(0, 100); + proposalB2.variableOptions.values.set(1, 1000); + proposalB2.variableOptions.values.set(2, 10000); + proposalB2.variableOptions.values.set(3, 100000); + uint16 proposalIdxB2 = test.setProposalInOtherContractAsShareholder(USER1, TESTEXB_CONTRACT_INDEX, TESTEXB::SetShareholderProposal_input{ proposalB2 }); + EXPECT_NE((int)proposalIdxB2, (int)INVALID_PROPOSAL_INDEX); + auto proposalDataB2 = test.getShareholderProposal(proposalIdxB2); + proposalB2 = proposalDataB2.proposal; // needed to set tick + EXPECT_EQ((int)proposalDataB2.proposal.type, (int)ProposalTypes::VariableFourValues); + EXPECT_EQ(proposalDataB2.proposerPubicKey, TESTEXA_CONTRACT_ID); + EXPECT_EQ(proposalDataB2.proposal.variableOptions.variable, 1); + EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(0), 100); + EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(1), 1000); + EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(2), 10000); + EXPECT_EQ(proposalDataB2.proposal.variableOptions.values.get(3), 100000); + + // cast votes in A1 + EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdxA1, proposalA1, { {0, 60}, {1, 270} })); + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdxA1, proposalA1, { {0, 15}, {1, 180} })); + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER4, TESTEXA_CONTRACT_INDEX, proposalIdxA1, proposalA1, { {1, 80}, {0, 15} })); + EXPECT_TRUE(test.setShareholderVotes(USER3, proposalIdxA1, proposalA1, { {0, 9}, {1, 11} })); + results = test.getShareholderVotingResults(proposalIdxA1); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ(results.optionVoteCount.get(0), 99); + EXPECT_EQ(results.optionVoteCount.get(1), 541); + EXPECT_EQ(results.getAcceptedOption(), 1); + EXPECT_EQ(results.totalVotesCasted, 99 + 541); + + // cast votes in A2 + EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdxA2, proposalA2, { {0, 150}, {1, 150} })); + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdxA2, proposalA2, { {0, 100}, {1, 100} })); + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER4, TESTEXA_CONTRACT_INDEX, proposalIdxA2, proposalA2, { {1, 50}, {0, 50} })); + EXPECT_TRUE(test.setShareholderVotes(USER3, proposalIdxA2, proposalA2, { {0, 10}, {1, 10} })); + results = test.getShareholderVotingResults(proposalIdxA2); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ(results.optionVoteCount.get(0), 310); + EXPECT_EQ(results.optionVoteCount.get(1), 310); + EXPECT_EQ(results.getAcceptedOption(), 0); + EXPECT_EQ(results.totalVotesCasted, 620); + EXPECT_TRUE(test.setShareholderVotes(USER1, proposalIdxA2, proposalA2, { {0, 0}, {1, 350} })); + results = test.getShareholderVotingResults(proposalIdxA2); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ(results.optionVoteCount.get(0), 160); + EXPECT_EQ(results.optionVoteCount.get(1), 510); + EXPECT_EQ(results.getAcceptedOption(), 1); + EXPECT_EQ(results.totalVotesCasted, 670); + + // cast votes in B1 + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER1, TESTEXB_CONTRACT_INDEX, proposalIdxB1, proposalB1, { {0, 10}, {100, 20}, {1000, 200}, {10000, 10}, {100000, 5}, {1000000, 5}, {10000000, 2}, {100000000, 2} })); + checkVoteCounts(test.getShareholderVotes(proposalIdxB1, TESTEXA_CONTRACT_ID), { {0, 10}, {100, 20}, {1000, 200}, {10000, 10}, {100000, 5}, {1000000, 5}, {10000000, 2}, {100000000, 2} }); + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdxB1, proposalB1, { {100, 200} })); + checkVoteCounts(test.getShareholderVotes(proposalIdxB1, USER2), { {100, 200} }); + EXPECT_TRUE(test.setShareholderVotes(USER3, proposalIdxB1, proposalB1, { {150, 90}, {200, 10} })); + checkVoteCounts(test.getShareholderVotes(proposalIdxB1, USER3), { {150, 90}, {200, 10} }); + EXPECT_TRUE(test.setShareholderVotes(USER4, proposalIdxB1, proposalB1, { {300, 99}, {11974, 1} })); + checkVoteCounts(test.getShareholderVotes(proposalIdxB1, USER4), { {300, 99}, {11974, 1} }); + results = test.getShareholderVotingResults(proposalIdxB1); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ((int)results.optionCount, 0); + EXPECT_EQ(results.scalarVotingResult, 345381); + EXPECT_EQ(results.totalVotesCasted, 654); + + // cast votes in B2 + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER1, TESTEXB_CONTRACT_INDEX, proposalIdxB2, proposalB2, { {0, 10}, {1, 20}, {2, 30}, {3, 40} })); + checkVoteCounts(test.getShareholderVotes(proposalIdxB2, TESTEXA_CONTRACT_ID), { {0, 10}, {1, 20}, {2, 30}, {3, 40} }); + EXPECT_TRUE(test.setShareholderVotes(USER2, proposalIdxB2, proposalB2, { {0, 20}, {1, 30}, {2, 40}, {3, 50}, {4, 3} })); + EXPECT_TRUE(test.setShareholderVotes(USER3, proposalIdxB2, proposalB2, { {0, 5}, {1, 10}, {2, 15}, {3, 20}, {4, 2} })); + EXPECT_TRUE(test.setShareholderVotes(USER4, proposalIdxB2, proposalB2, { {0, 25}, {1, 20}, {2, 15}, {3, 10} })); + results = test.getShareholderVotingResults(proposalIdxB2); + EXPECT_EQ(results.totalVotesAuthorized, 676); + EXPECT_EQ(results.optionVoteCount.get(0), 60); + EXPECT_EQ(results.optionVoteCount.get(1), 80); + EXPECT_EQ(results.optionVoteCount.get(2), 100); + EXPECT_EQ(results.optionVoteCount.get(3), 120); + EXPECT_EQ(results.optionVoteCount.get(4), 5); + EXPECT_EQ(results.getAcceptedOption(), -1); + EXPECT_EQ(results.totalVotesCasted, 365); + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER1, TESTEXB_CONTRACT_INDEX, proposalIdxB2, proposalB2, { {0, 45}, {1, 50}, {2, 55}, {3, 50}, {4, 5} })); + results = test.getShareholderVotingResults(proposalIdxB2); + EXPECT_EQ(results.optionVoteCount.get(0), 95); + EXPECT_EQ(results.optionVoteCount.get(1), 110); + EXPECT_EQ(results.optionVoteCount.get(2), 125); + EXPECT_EQ(results.optionVoteCount.get(3), 130); + EXPECT_EQ(results.optionVoteCount.get(4), 10); + EXPECT_EQ(results.getAcceptedOption(), -1); + EXPECT_EQ(results.totalVotesCasted, 470); + EXPECT_TRUE(test.setVotesInOtherContractAsShareholder(USER1, TESTEXB_CONTRACT_INDEX, proposalIdxB2, proposalB2, { {0, 5}, {1, 5}, {2, 5}, {3, 240} })); + results = test.getShareholderVotingResults(proposalIdxB2); + EXPECT_EQ(results.optionVoteCount.get(0), 55); + EXPECT_EQ(results.optionVoteCount.get(1), 65); + EXPECT_EQ(results.optionVoteCount.get(2), 75); + EXPECT_EQ(results.optionVoteCount.get(3), 320); + EXPECT_EQ(results.optionVoteCount.get(4), 5); + EXPECT_EQ(results.getAcceptedOption(), 3); + EXPECT_EQ(results.totalVotesCasted, 520); + + // test proposal listing function in TESTEXA: 1 inactive by TESTEXB, 2 active by USER2/USER1 + proposalIndices = test.getShareholderProposalIndices(false); + EXPECT_TRUE(proposalIndices.size() == 1 && proposalIndices[0] == proposalIdx2); + proposalIndices = test.getShareholderProposalIndices(true); + EXPECT_TRUE(proposalIndices.size() == 2 && proposalIndices[0] == proposalIdxA2 && proposalIndices[1] == proposalIdxA1); + + // test proposal listing function in TESTEXB: 0 inactive, 2 active by USER1/TESTEXA + proposalIndices = test.getShareholderProposalIndices(false); + EXPECT_TRUE(proposalIndices.size() == 0); + proposalIndices = test.getShareholderProposalIndices(true); + EXPECT_TRUE(proposalIndices.size() == 2 && proposalIndices[0] == proposalIdxB1 && proposalIndices[1] == proposalIdxB2); + + // test that variables are set correctly after epoch switch + test.getStateTestExampleA()->checkVariablesSetByProposal(1, 2, 3); + test.getStateTestExampleB()->checkVariablesSetByProposal(0, 0, 0); + test.endEpoch(); + ++system.epoch; + test.getStateTestExampleA()->checkVariablesSetByProposal(13, 1337, 42); + test.getStateTestExampleB()->checkVariablesSetByProposal(345381, 10000, 0); + + // test proposal listing function in TESTEXA: 3 inactive by TESTEXB/USER2/USER1, 0 active + EXPECT_TRUE(test.getShareholderProposalIndices(false).size() == 3); + EXPECT_TRUE(test.getShareholderProposalIndices(true).size() == 0); + + // test proposal listing function in TESTEXB: 2 inactive by USER1/TESTEXA, 0 active + EXPECT_TRUE(test.getShareholderProposalIndices(false).size() == 2); + EXPECT_TRUE(test.getShareholderProposalIndices(true).size() == 0); +} + +TEST(ContractTestEx, InterContractCallInsufficientFees) +{ + ContractTestingTestEx test; + increaseEnergy(USER1, 1000000); + + // First verify call works normally (TestExampleA has fees from constructor) + auto output1 = test.testInterContractCallError(); + EXPECT_EQ(output1.errorCode, QPI::NoCallError); + EXPECT_EQ(output1.callSucceeded, 1); + + // Save original fee reserve + long long originalFeeReserve = getContractFeeReserve(TESTEXA_CONTRACT_INDEX); + + // Drain TestExampleA's fee reserve + setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 0); + + // Verify fee reserve is now 0 + EXPECT_EQ(getContractFeeReserve(TESTEXA_CONTRACT_INDEX), 0); + + // Try the call again - should fail with insufficient fees + auto output2 = test.testInterContractCallError(); + EXPECT_EQ(output2.errorCode, QPI::CallErrorInsufficientFees); + EXPECT_EQ(output2.callSucceeded, 0); + + // Restore fee reserve for other tests + setContractFeeReserve(TESTEXA_CONTRACT_INDEX, originalFeeReserve); +} + +TEST(ContractTestEx, SystemCallbacksWithNegativeFeeReserve) +{ + ContractTestingTestEx test; + + // Set TESTEXC fee reserve to negative value + setContractFeeReserve(TESTEXC_CONTRACT_INDEX, -1000); + EXPECT_EQ(getContractFeeReserve(TESTEXC_CONTRACT_INDEX), -1000); + + const auto initialIncomingC = test.getIncomingTransferAmounts(); + const sint64 initialBalanceC = getBalance(TESTEXC_CONTRACT_ID); + + // Give TESTEXB balance to make the transfer + increaseEnergy(TESTEXB_CONTRACT_ID, 10000); + increaseEnergy(USER1, 10000); + const sint64 transferAmount = 5000; + EXPECT_TRUE(test.qpiTransfer(TESTEXC_CONTRACT_ID, transferAmount, 1000, USER1)); + + // Verify callback executed and modified state + const auto afterIncomingC = test.getIncomingTransferAmounts(); + EXPECT_EQ(afterIncomingC.qpiTransferAmount, initialIncomingC.qpiTransferAmount + transferAmount); + EXPECT_EQ(getBalance(TESTEXC_CONTRACT_ID), initialBalanceC + transferAmount); + + // Verify TESTEXB not in error state + EXPECT_EQ(contractError[TESTEXB_CONTRACT_INDEX], NoContractError); + + // Verify TESTEXC fee reserve is still negative + EXPECT_LT(getContractFeeReserve(TESTEXC_CONTRACT_INDEX), 0); +} diff --git a/test/contract_testing.h b/test/contract_testing.h index 2e92a267b..61d3a71dc 100644 --- a/test/contract_testing.h +++ b/test/contract_testing.h @@ -19,6 +19,7 @@ #include "contract_core/qpi_system_impl.h" #include "contract_core/qpi_ticking_impl.h" #include "contract_core/qpi_ipo_impl.h" +#include "contract_core/qpi_mining_impl.h" #include "test_util.h" @@ -28,6 +29,10 @@ class ContractTesting : public LoggingTest public: ContractTesting() { + +#ifdef __AVX512F__ + initAVX512FourQConstants(); +#endif initCommonBuffers(); initContractExec(); initSpecialEntities(); @@ -150,10 +155,12 @@ class ContractTesting : public LoggingTest #define INIT_CONTRACT(contractName) { \ constexpr unsigned int contractIndex = contractName##_CONTRACT_INDEX; \ EXPECT_LT(contractIndex, contractCount); \ - const unsigned long long size = contractDescriptions[contractIndex].stateSize; \ - contractStates[contractIndex] = (unsigned char*)malloc(size); \ - setMem(contractStates[contractIndex], size, 0); \ + const unsigned long long stateSize = contractDescriptions[contractIndex].stateSize; \ + EXPECT_GE(stateSize, max(sizeof(contractName), sizeof(IPO))); \ + contractStates[contractIndex] = (unsigned char*)malloc(stateSize); \ + setMem(contractStates[contractIndex], stateSize, 0); \ REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(contractName); \ + setContractFeeReserve(contractIndex, 10000000); \ } static inline long long getBalance(const id& pubKey) @@ -196,7 +203,7 @@ static inline void checkContractExecCleanup() } // Issue contract shares and transfer ownership/possession of all shares to one entity -static inline void issueContractShares(unsigned int contractIndex, std::vector>& initialOwnerShares) +static inline void issueContractShares(unsigned int contractIndex, std::vector>& initialOwnerShares, bool warnOnTooFewShares = true) { int issuanceIndex, ownershipIndex, possessionIndex, dstOwnershipIndex, dstPossessionIndex; EXPECT_EQ(issueAsset(m256i::zero(), (char*)contractDescriptions[contractIndex].assetName, 0, CONTRACT_ASSET_UNIT_OF_MEASUREMENT, NUMBER_OF_COMPUTORS, QX_CONTRACT_INDEX, &issuanceIndex, &ownershipIndex, &possessionIndex), NUMBER_OF_COMPUTORS); @@ -207,7 +214,8 @@ static inline void issueContractShares(unsigned int contractIndex, std::vector + +#include "contract_testing.h" + +namespace { +constexpr unsigned short PROCEDURE_CREATE_ORDER = 1; +constexpr unsigned short PROCEDURE_TRANSFER_TO_CONTRACT = 6; + +uint64 requiredFee(uint64 amount) +{ + // Total fee is 0.5% (ETH) + 0.5% (Qubic) = 1% of amount + return 2 * ((amount * 5000000ULL) / 1000000000ULL); +} +} + +class ContractTestingVottunBridge : protected ContractTesting +{ +public: + using ContractTesting::invokeUserProcedure; + + ContractTestingVottunBridge() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(VOTTUNBRIDGE); + callSystemProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, INITIALIZE); + } + + VOTTUNBRIDGE* state() + { + return reinterpret_cast(contractStates[VOTTUNBRIDGE_CONTRACT_INDEX]); + } + + bool findOrder(uint64 orderId, VOTTUNBRIDGE::BridgeOrder& out) + { + for (uint64 i = 0; i < state()->orders.capacity(); ++i) + { + VOTTUNBRIDGE::BridgeOrder order = state()->orders.get(i); + if (order.orderId == orderId) + { + out = order; + return true; + } + } + return false; + } + + bool findProposal(uint64 proposalId, VOTTUNBRIDGE::AdminProposal& out) + { + for (uint64 i = 0; i < state()->proposals.capacity(); ++i) + { + VOTTUNBRIDGE::AdminProposal proposal = state()->proposals.get(i); + if (proposal.proposalId == proposalId) + { + out = proposal; + return true; + } + } + return false; + } + + bool setOrderById(uint64 orderId, const VOTTUNBRIDGE::BridgeOrder& updated) + { + for (uint64 i = 0; i < state()->orders.capacity(); ++i) + { + VOTTUNBRIDGE::BridgeOrder order = state()->orders.get(i); + if (order.orderId == orderId) + { + state()->orders.set(i, updated); + return true; + } + } + return false; + } + + bool setProposalById(uint64 proposalId, const VOTTUNBRIDGE::AdminProposal& updated) + { + for (uint64 i = 0; i < state()->proposals.capacity(); ++i) + { + VOTTUNBRIDGE::AdminProposal proposal = state()->proposals.get(i); + if (proposal.proposalId == proposalId) + { + state()->proposals.set(i, updated); + return true; + } + } + return false; + } + + VOTTUNBRIDGE::createOrder_output createOrder( + const id& user, uint64 amount, bit fromQubicToEthereum, uint64 fee) + { + VOTTUNBRIDGE::createOrder_input input{}; + VOTTUNBRIDGE::createOrder_output output{}; + input.qubicDestination = id(9, 0, 0, 0); + input.amount = amount; + input.fromQubicToEthereum = fromQubicToEthereum; + for (uint64 i = 0; i < 42; ++i) + { + input.ethAddress.set(i, static_cast('A')); + } + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, PROCEDURE_CREATE_ORDER, + input, output, user, static_cast(fee)); + return output; + } + + void seedBalance(const id& user, uint64 amount) + { + increaseEnergy(user, amount); + } + + VOTTUNBRIDGE::transferToContract_output transferToContract( + const id& user, uint64 amount, uint64 orderId, uint64 invocationReward) + { + VOTTUNBRIDGE::transferToContract_input input{}; + VOTTUNBRIDGE::transferToContract_output output{}; + input.amount = amount; + input.orderId = orderId; + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, PROCEDURE_TRANSFER_TO_CONTRACT, + input, output, user, static_cast(invocationReward)); + return output; + } + + VOTTUNBRIDGE::createProposal_output createProposal( + const id& admin, uint8 proposalType, const id& target, const id& oldAddress, uint64 amount) + { + VOTTUNBRIDGE::createProposal_input input{}; + VOTTUNBRIDGE::createProposal_output output{}; + input.proposalType = proposalType; + input.targetAddress = target; + input.oldAddress = oldAddress; + input.amount = amount; + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, 9, input, output, admin, 0); + return output; + } + + VOTTUNBRIDGE::approveProposal_output approveProposal(const id& admin, uint64 proposalId) + { + VOTTUNBRIDGE::approveProposal_input input{}; + VOTTUNBRIDGE::approveProposal_output output{}; + input.proposalId = proposalId; + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, 10, input, output, admin, 0); + return output; + } + + VOTTUNBRIDGE::cancelProposal_output cancelProposal(const id& user, uint64 proposalId) + { + VOTTUNBRIDGE::cancelProposal_input input{}; + VOTTUNBRIDGE::cancelProposal_output output{}; + input.proposalId = proposalId; + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, 11, input, output, user, 0); + return output; + } +}; + +TEST(VottunBridge, CreateOrder_RequiresFee) +{ + ContractTestingVottunBridge bridge; + const id user = id(1, 0, 0, 0); + const uint64 amount = 1000; + const uint64 fee = requiredFee(amount); + + std::cout << "[VottunBridge] CreateOrder_RequiresFee: amount=" << amount + << " fee=" << fee << " (sending fee-1)" << std::endl; + + increaseEnergy(user, fee - 1); + auto output = bridge.createOrder(user, amount, true, fee - 1); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::insufficientTransactionFee)); +} + +TEST(VottunBridge, TransferToContract_RejectsMissingReward) +{ + ContractTestingVottunBridge bridge; + const id user = id(2, 0, 0, 0); + const uint64 amount = 200; + const uint64 fee = requiredFee(amount); + const id contractId = id(VOTTUNBRIDGE_CONTRACT_INDEX, 0, 0, 0); + + std::cout << "[VottunBridge] TransferToContract_RejectsMissingReward: amount=" << amount + << " fee=" << fee << " reward=0 contractBalanceSeed=1000" << std::endl; + + // Seed balances: user only has fees; contract already has balance > amount + increaseEnergy(user, fee); + increaseEnergy(contractId, 1000); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + const uint64 lockedBefore = bridge.state()->lockedTokens; + const long long contractBalanceBefore = getBalance(contractId); + const long long userBalanceBefore = getBalance(user); + + auto transferOutput = bridge.transferToContract(user, amount, orderOutput.orderId, 0); + + EXPECT_EQ(transferOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidAmount)); + EXPECT_EQ(bridge.state()->lockedTokens, lockedBefore); + EXPECT_EQ(getBalance(contractId), contractBalanceBefore); + EXPECT_EQ(getBalance(user), userBalanceBefore); +} + +TEST(VottunBridge, TransferToContract_AcceptsExactReward) +{ + ContractTestingVottunBridge bridge; + const id user = id(3, 0, 0, 0); + const uint64 amount = 500; + const uint64 fee = requiredFee(amount); + const id contractId = id(VOTTUNBRIDGE_CONTRACT_INDEX, 0, 0, 0); + + std::cout << "[VottunBridge] TransferToContract_AcceptsExactReward: amount=" << amount + << " fee=" << fee << " reward=amount" << std::endl; + + increaseEnergy(user, fee + amount); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + const uint64 lockedBefore = bridge.state()->lockedTokens; + const long long contractBalanceBefore = getBalance(contractId); + + auto transferOutput = bridge.transferToContract(user, amount, orderOutput.orderId, amount); + + EXPECT_EQ(transferOutput.status, 0); + EXPECT_EQ(bridge.state()->lockedTokens, lockedBefore + amount); + EXPECT_EQ(getBalance(contractId), contractBalanceBefore + amount); +} + +TEST(VottunBridge, TransferToContract_OrderNotFound) +{ + ContractTestingVottunBridge bridge; + const id user = id(5, 0, 0, 0); + const uint64 amount = 100; + + bridge.seedBalance(user, amount); + + auto output = bridge.transferToContract(user, amount, 9999, amount); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::orderNotFound)); +} + +TEST(VottunBridge, TransferToContract_InvalidAmountMismatch) +{ + ContractTestingVottunBridge bridge; + const id user = id(6, 0, 0, 0); + const uint64 amount = 100; + const uint64 fee = requiredFee(amount); + + bridge.seedBalance(user, fee + amount + 1); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + auto output = bridge.transferToContract(user, amount + 1, orderOutput.orderId, amount + 1); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidAmount)); +} + +TEST(VottunBridge, TransferToContract_InvalidOrderState) +{ + ContractTestingVottunBridge bridge; + const id user = id(7, 0, 0, 0); + const uint64 amount = 150; + const uint64 fee = requiredFee(amount); + + bridge.seedBalance(user, fee + amount); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + VOTTUNBRIDGE::BridgeOrder order{}; + ASSERT_TRUE(bridge.findOrder(orderOutput.orderId, order)); + order.status = 1; // completed + ASSERT_TRUE(bridge.setOrderById(orderOutput.orderId, order)); + + auto output = bridge.transferToContract(user, amount, orderOutput.orderId, amount); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidOrderState)); +} + +TEST(VottunBridge, CreateOrder_CleansCompletedAndRefundedSlots) +{ + ContractTestingVottunBridge bridge; + const id user = id(4, 0, 0, 0); + const uint64 amount = 1000; + const uint64 fee = requiredFee(amount); + + VOTTUNBRIDGE::BridgeOrder filledOrder{}; + filledOrder.orderId = 1; + filledOrder.amount = amount; + filledOrder.status = 1; // completed + filledOrder.fromQubicToEthereum = true; + filledOrder.qubicSender = user; + + for (uint64 i = 0; i < bridge.state()->orders.capacity(); ++i) + { + filledOrder.orderId = i + 1; + filledOrder.status = (i % 2 == 0) ? 1 : 2; // completed/refunded + bridge.state()->orders.set(i, filledOrder); + } + + increaseEnergy(user, fee); + auto output = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(output.status, 0); + + VOTTUNBRIDGE::BridgeOrder createdOrder{}; + EXPECT_TRUE(bridge.findOrder(output.orderId, createdOrder)); + EXPECT_EQ(createdOrder.status, 0); + + uint64 emptySlots = 0; + for (uint64 i = 0; i < bridge.state()->orders.capacity(); ++i) + { + if (bridge.state()->orders.get(i).status == 255) + { + emptySlots++; + } + } + EXPECT_GT(emptySlots, 0); +} + +TEST(VottunBridge, CreateProposal_CleansExecutedProposalsWhenFull) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + for (uint64 i = 2; i < bridge.state()->admins.capacity(); ++i) + { + bridge.state()->admins.set(i, NULL_ID); + } + + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + VOTTUNBRIDGE::AdminProposal proposal{}; + proposal.approvalsCount = 1; + proposal.active = true; + proposal.executed = false; + for (uint64 i = 0; i < bridge.state()->proposals.capacity(); ++i) + { + proposal.proposalId = i + 1; + proposal.executed = (i % 2 == 0); + bridge.state()->proposals.set(i, proposal); + } + + auto output = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + + EXPECT_EQ(output.status, 0); + + VOTTUNBRIDGE::AdminProposal createdProposal{}; + EXPECT_TRUE(bridge.findProposal(output.proposalId, createdProposal)); + EXPECT_TRUE(createdProposal.active); + EXPECT_FALSE(createdProposal.executed); + + uint64 clearedSlots = 0; + for (uint64 i = 0; i < bridge.state()->proposals.capacity(); ++i) + { + VOTTUNBRIDGE::AdminProposal p = bridge.state()->proposals.get(i); + if (!p.active && p.proposalId == 0) + { + clearedSlots++; + } + } + EXPECT_GT(clearedSlots, 0); +} + +TEST(VottunBridge, CreateProposal_InvalidTypeRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + + bridge.state()->numberOfAdmins = 1; + bridge.state()->requiredApprovals = 1; + bridge.state()->admins.set(0, admin1); + bridge.seedBalance(admin1, 1); + + auto output = bridge.createProposal(admin1, 99, NULL_ID, NULL_ID, 0); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidAmount)); +} + +TEST(VottunBridge, ApproveProposal_NotOwnerRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + const id outsider = id(99, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + bridge.seedBalance(outsider, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(outsider, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::notOwner)); + EXPECT_FALSE(approveOutput.executed); +} + +TEST(VottunBridge, ApproveProposal_DoubleApprovalRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(admin1, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::proposalAlreadyApproved)); + EXPECT_FALSE(approveOutput.executed); +} + +TEST(VottunBridge, ApproveProposal_ExecutesChangeThreshold) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(admin2, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, 0); + EXPECT_TRUE(approveOutput.executed); + EXPECT_EQ(bridge.state()->requiredApprovals, 2); +} + +TEST(VottunBridge, ApproveProposal_ProposalNotFound) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(12, 0, 0, 0); + + bridge.state()->numberOfAdmins = 1; + bridge.state()->requiredApprovals = 1; + bridge.state()->admins.set(0, admin1); + bridge.seedBalance(admin1, 1); + + auto output = bridge.approveProposal(admin1, 12345); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::proposalNotFound)); + EXPECT_FALSE(output.executed); +} + +TEST(VottunBridge, ApproveProposal_AlreadyExecuted) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(13, 0, 0, 0); + const id admin2 = id(14, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(admin2, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, 0); + EXPECT_TRUE(approveOutput.executed); + + auto secondApprove = bridge.approveProposal(admin1, proposalOutput.proposalId); + EXPECT_EQ(secondApprove.status, static_cast(VOTTUNBRIDGE::EthBridgeError::proposalAlreadyExecuted)); + EXPECT_FALSE(secondApprove.executed); +} + +TEST(VottunBridge, TransferToContract_RefundsExcess) +{ + ContractTestingVottunBridge bridge; + const id user = id(20, 0, 0, 0); + const uint64 amount = 500; + const uint64 excess = 100; + const uint64 fee = requiredFee(amount); + const id contractId = id(VOTTUNBRIDGE_CONTRACT_INDEX, 0, 0, 0); + + std::cout << "[VottunBridge] TransferToContract_RefundsExcess: amount=" << amount + << " excess=" << excess << " fee=" << fee << std::endl; + + increaseEnergy(user, fee + amount + excess); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + const uint64 lockedBefore = bridge.state()->lockedTokens; + const long long userBalanceBefore = getBalance(user); + + // Send more than required (amount + excess) + auto transferOutput = bridge.transferToContract(user, amount, orderOutput.orderId, amount + excess); + + EXPECT_EQ(transferOutput.status, 0); + // Should lock only the required amount + EXPECT_EQ(bridge.state()->lockedTokens, lockedBefore + amount); + // User should get excess back + EXPECT_EQ(getBalance(user), userBalanceBefore - amount); +} + +TEST(VottunBridge, TransferToContract_RefundsAllOnInsufficient) +{ + ContractTestingVottunBridge bridge; + const id user = id(21, 0, 0, 0); + const uint64 amount = 500; + const uint64 insufficientAmount = 200; + const uint64 fee = requiredFee(amount); + const id contractId = id(VOTTUNBRIDGE_CONTRACT_INDEX, 0, 0, 0); + + std::cout << "[VottunBridge] TransferToContract_RefundsAllOnInsufficient: amount=" << amount + << " sent=" << insufficientAmount << std::endl; + + increaseEnergy(user, fee + insufficientAmount); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + const uint64 lockedBefore = bridge.state()->lockedTokens; + const long long userBalanceBefore = getBalance(user); + + // Send less than required + auto transferOutput = bridge.transferToContract(user, amount, orderOutput.orderId, insufficientAmount); + + EXPECT_EQ(transferOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidAmount)); + // Should NOT lock anything + EXPECT_EQ(bridge.state()->lockedTokens, lockedBefore); + // User should get everything back + EXPECT_EQ(getBalance(user), userBalanceBefore); +} + +TEST(VottunBridge, CancelProposal_Success) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(22, 0, 0, 0); + + bridge.state()->numberOfAdmins = 1; + bridge.state()->requiredApprovals = 1; + bridge.state()->admins.set(0, admin1); + bridge.seedBalance(admin1, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + // Verify proposal is active + VOTTUNBRIDGE::AdminProposal proposal; + EXPECT_TRUE(bridge.findProposal(proposalOutput.proposalId, proposal)); + EXPECT_TRUE(proposal.active); + + // Cancel the proposal + auto cancelOutput = bridge.cancelProposal(admin1, proposalOutput.proposalId); + EXPECT_EQ(cancelOutput.status, 0); + + // Verify proposal is inactive + EXPECT_TRUE(bridge.findProposal(proposalOutput.proposalId, proposal)); + EXPECT_FALSE(proposal.active); +} + +TEST(VottunBridge, CancelProposal_NotCreatorRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(23, 0, 0, 0); + const id admin2 = id(24, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + // Admin1 creates proposal + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + // Admin2 tries to cancel (should fail - not the creator) + auto cancelOutput = bridge.cancelProposal(admin2, proposalOutput.proposalId); + EXPECT_EQ(cancelOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::notAuthorized)); + + // Verify proposal is still active + VOTTUNBRIDGE::AdminProposal proposal; + EXPECT_TRUE(bridge.findProposal(proposalOutput.proposalId, proposal)); + EXPECT_TRUE(proposal.active); +} + +TEST(VottunBridge, CancelProposal_AlreadyExecutedRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(25, 0, 0, 0); + const id admin2 = id(26, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + // Execute the proposal by approving with a different admin (threshold is 2) + auto approveOutput = bridge.approveProposal(admin2, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, 0); + EXPECT_TRUE(approveOutput.executed); + + // Ensure proposal is marked executed in state (explicit for this cancellation test) + VOTTUNBRIDGE::AdminProposal proposal; + ASSERT_TRUE(bridge.findProposal(proposalOutput.proposalId, proposal)); + proposal.executed = true; + ASSERT_TRUE(bridge.setProposalById(proposalOutput.proposalId, proposal)); + ASSERT_TRUE(bridge.findProposal(proposalOutput.proposalId, proposal)); + ASSERT_TRUE(proposal.executed); + + // Trying to cancel an executed proposal should fail + auto cancelOutput = bridge.cancelProposal(admin1, proposalOutput.proposalId); + EXPECT_EQ(cancelOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::proposalAlreadyExecuted)); +} diff --git a/test/custom_mining.cpp b/test/custom_mining.cpp index 176812740..9a68f8dcf 100644 --- a/test/custom_mining.cpp +++ b/test/custom_mining.cpp @@ -19,13 +19,13 @@ TEST(CustomMining, TaskStorageGeneral) { constexpr unsigned long long NUMBER_OF_TASKS = 100; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); for (unsigned long long i = 0; i < NUMBER_OF_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = NUMBER_OF_TASKS - i; storage.addData(&task); @@ -34,8 +34,8 @@ TEST(CustomMining, TaskStorageGeneral) // Expect the task are sort in ascending order for (unsigned long long i = 0; i < NUMBER_OF_TASKS - 1; i++) { - CustomMiningTask* task0 = storage.getDataByIndex(i); - CustomMiningTask* task1 = storage.getDataByIndex(i + 1); + CustomMiningTaskV2* task0 = storage.getDataByIndex(i); + CustomMiningTaskV2* task1 = storage.getDataByIndex(i + 1); EXPECT_LT(task0->taskIndex, task1->taskIndex); } EXPECT_EQ(storage.getCount(), NUMBER_OF_TASKS); @@ -47,14 +47,14 @@ TEST(CustomMining, TaskStorageDuplicatedItems) { constexpr unsigned long long NUMBER_OF_TASKS = 100; constexpr unsigned long long DUPCATED_TASKS = 10; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); // For DUPCATED_TASKS will only recorded 1 task for (unsigned long long i = 0; i < DUPCATED_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = 1; storage.addData(&task); @@ -62,7 +62,7 @@ TEST(CustomMining, TaskStorageDuplicatedItems) for (unsigned long long i = DUPCATED_TASKS; i < NUMBER_OF_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = i; storage.addData(&task); @@ -78,19 +78,19 @@ TEST(CustomMining, TaskStorageExistedItem) { constexpr unsigned long long NUMBER_OF_TASKS = 100; constexpr unsigned long long DUPCATED_TASKS = 10; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); for (unsigned long long i = 1; i < NUMBER_OF_TASKS + 1 ; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = i; storage.addData(&task); } // Test an existed task - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = NUMBER_OF_TASKS - 10; storage.addData(&task); @@ -119,19 +119,19 @@ TEST(CustomMining, TaskStorageExistedItem) TEST(CustomMining, TaskStorageOverflow) { constexpr unsigned long long NUMBER_OF_TASKS = CUSTOM_MINING_TASK_STORAGE_COUNT; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); for (unsigned long long i = 0; i < NUMBER_OF_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = i; storage.addData(&task); } // Overflow. Add one more and get error status - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = NUMBER_OF_TASKS + 1; EXPECT_NE(storage.addData(&task), 0); diff --git a/test/execution_fees.cpp b/test/execution_fees.cpp new file mode 100644 index 000000000..6fdf2cbd1 --- /dev/null +++ b/test/execution_fees.cpp @@ -0,0 +1,375 @@ +#define NO_UEFI + +#include "contract_testing.h" +#include "../src/ticking/execution_fee_report_collector.h" +#include "../src/contract_core/execution_time_accumulator.h" + +// Helper to create a valid baseline test transaction with given entries +static Transaction* createTestTransaction(unsigned char* buffer, size_t bufferSize, + unsigned int numEntries, + const unsigned int* contractIndices, + const long long* executionFees) +{ + unsigned int alignmentPadding = (numEntries % 2 == 1) ? sizeof(unsigned int) : 0; + const unsigned int inputSize = sizeof(unsigned int) + sizeof(unsigned int) + + (numEntries * sizeof(unsigned int)) + alignmentPadding + + (numEntries * sizeof(long long)) + + sizeof(m256i); + + if (sizeof(Transaction) + inputSize > bufferSize) + { + return nullptr; + } + + Transaction* tx = (Transaction*)buffer; + tx->sourcePublicKey = m256i::zero(); + tx->destinationPublicKey = m256i::zero(); + tx->amount = 0; + tx->tick = 1000; + tx->inputType = EXECUTION_FEE_REPORT_INPUT_TYPE; + tx->inputSize = inputSize; + + unsigned char* inputPtr = tx->inputPtr(); + *(unsigned int*)inputPtr = 5; // phaseNumber + *(unsigned int*)(inputPtr + 4) = numEntries; + + unsigned int* txIndices = (unsigned int*)(inputPtr + 8); + for (unsigned int i = 0; i < numEntries; i++) + { + txIndices[i] = contractIndices[i]; + } + + long long* txFees = (long long*)(inputPtr + 8 + (numEntries * sizeof(unsigned int)) + alignmentPadding); + for (unsigned int i = 0; i < numEntries; i++) + { + txFees[i] = executionFees[i]; + } + + m256i* dataLock = (m256i*)(inputPtr + 8 + (numEntries * sizeof(unsigned int)) + alignmentPadding + (numEntries * sizeof(long long))); + *dataLock = m256i::zero(); + + return tx; +} + +TEST(ExecutionFeeReportCollector, InitAndStore) +{ + ExecutionFeeReportCollector collector; + collector.init(); + + collector.storeReport(2, 0, 1000); + collector.storeReport(2, 1, 2000); + collector.storeReport(1, 0, 5000); + collector.storeReport(1, 500, 6000); + + const unsigned long long* reports = collector.getReportsForContract(2); + ASSERT_NE(reports, nullptr); + EXPECT_EQ(reports[0], 1000); + EXPECT_EQ(reports[1], 2000); + + reports = collector.getReportsForContract(1); + ASSERT_NE(reports, nullptr); + EXPECT_EQ(reports[0], 5000); + EXPECT_EQ(reports[500], 6000); +} + +TEST(ExecutionFeeReportCollector, Reset) +{ + ExecutionFeeReportCollector collector; + collector.init(); + + for (unsigned int i = 0; i < 10; i++) { + collector.storeReport(1, i, i * 1000); + } + + const unsigned long long* reports = collector.getReportsForContract(1); + EXPECT_EQ(reports[5], 5000); + + collector.reset(); + + reports = collector.getReportsForContract(1); + EXPECT_EQ(reports[5], 0); +} + +TEST(ExecutionFeeReportCollector, BoundaryValidation) +{ + ExecutionFeeReportCollector collector; + collector.init(); + + collector.storeReport(1, 0, 100); + collector.storeReport(contractCount - 1, NUMBER_OF_COMPUTORS - 1, 200); + + collector.storeReport(contractCount, 0, 100); + collector.storeReport(1, NUMBER_OF_COMPUTORS, 100); + + const unsigned long long* reports = collector.getReportsForContract(1); + EXPECT_EQ(reports[0], 100); + + reports = collector.getReportsForContract(contractCount - 1); + EXPECT_EQ(reports[NUMBER_OF_COMPUTORS - 1], 200); +} + +TEST(ExecutionFeeReportTransaction, ParseValidTransaction) +{ + unsigned char buffer[512]; + unsigned int contractIndices[2] = {2, 1}; + long long executionFees[2] = {1000, 2000}; + + Transaction* tx = createTestTransaction(buffer, sizeof(buffer), 2, contractIndices, executionFees); + ASSERT_NE(tx, nullptr); + + EXPECT_TRUE(ExecutionFeeReportTransactionPrefix::isValidExecutionFeeReport(tx)); + EXPECT_TRUE(ExecutionFeeReportTransactionPrefix::isValidEntryAlignment(tx)); + EXPECT_EQ(ExecutionFeeReportTransactionPrefix::getNumEntries(tx), 2u); + + const unsigned int* parsedIndices = ExecutionFeeReportTransactionPrefix::getContractIndices(tx); + const unsigned long long* parsedFees = ExecutionFeeReportTransactionPrefix::getExecutionFees(tx); + EXPECT_EQ(parsedIndices[0], 2u); + EXPECT_EQ(parsedFees[0], 1000); + EXPECT_EQ(parsedIndices[1], 1u); + EXPECT_EQ(parsedFees[1], 2000); +} + +TEST(ExecutionFeeReportTransaction, RejectNonZeroAmount) { + unsigned char buffer[512]; + unsigned int contractIndices[1] = {1}; + long long executionFees[1] = {1000}; + + Transaction* tx = createTestTransaction(buffer, sizeof(buffer), 1, contractIndices, executionFees); + ASSERT_NE(tx, nullptr); + + // Valid initially + EXPECT_TRUE(ExecutionFeeReportTransactionPrefix::isValidExecutionFeeReport(tx)); + + // Make amount non-zero (execution fee reports must have amount = 0) + tx->amount = 100; + + // Should now be invalid + EXPECT_FALSE(ExecutionFeeReportTransactionPrefix::isValidExecutionFeeReport(tx)); +} + +TEST(ExecutionFeeReportTransaction, RejectMisalignedEntries) { + unsigned char buffer[512]; + unsigned int contractIndices[1] = {1}; + long long executionFees[1] = {1000}; + + Transaction* tx = createTestTransaction(buffer, sizeof(buffer), 1, contractIndices, executionFees); + ASSERT_NE(tx, nullptr); + + // Valid initially + EXPECT_TRUE(ExecutionFeeReportTransactionPrefix::isValidEntryAlignment(tx)); + + // Break alignment by adding 1 byte to inputSize + // Payload size will no longer match expected size for numEntries + tx->inputSize += 1; + + // Should now have invalid alignment + EXPECT_FALSE(ExecutionFeeReportTransactionPrefix::isValidEntryAlignment(tx)); +} + +TEST(ExecutionFeeReportCollector, ValidateReportEntries) { + ExecutionFeeReportCollector collector; + collector.init(); + + // Valid entries + unsigned int validIndices[2] = {1, 2}; + unsigned long long validFees[2] = {1000, 2000}; + EXPECT_TRUE(collector.validateReportEntries(validIndices, validFees, 2)); + + // Invalid: contractIndex >= contractCount + unsigned int invalidContractIndices[1] = {contractCount}; + unsigned long long invalidContractFees[1] = {1000}; + EXPECT_FALSE(collector.validateReportEntries(invalidContractIndices, invalidContractFees, 1)); + + // Invalid: executionFee <= 0 + unsigned int zeroFeeIndices[1] = {1}; + unsigned long long zeroFees[1] = {0}; + EXPECT_FALSE(collector.validateReportEntries(zeroFeeIndices, zeroFees, 1)); + + // Invalid: one good entry, one bad + unsigned int mixedIndices[2] = {1, contractCount + 5}; + unsigned long long mixedFees[2] = {1000, 2000}; + EXPECT_FALSE(collector.validateReportEntries(mixedIndices, mixedFees, 2)); +} + +TEST(ExecutionFeeReportCollector, StoreReportEntries) { + ExecutionFeeReportCollector collector; + collector.init(); + + unsigned int contractIndices[3] = {1, 2, 5}; + unsigned long long executionFees[3] = {1000, 3000, 7000}; + + unsigned int computorIndex = 10; + collector.storeReportEntries(contractIndices, executionFees, 3, computorIndex); + + // Verify entries were stored at correct positions + const unsigned long long* reports1 = collector.getReportsForContract(1); + EXPECT_EQ(reports1[computorIndex], 1000); + + const unsigned long long* reports2 = collector.getReportsForContract(2); + EXPECT_EQ(reports2[computorIndex], 3000); + + const unsigned long long* reports5 = collector.getReportsForContract(5); + EXPECT_EQ(reports5[computorIndex], 7000); + + // Verify other positions remain zero + EXPECT_EQ(reports1[0], 0); + EXPECT_EQ(reports1[computorIndex + 1], 0); +} + +TEST(ExecutionFeeReportCollector, MultipleComputorsReporting) { + ExecutionFeeReportCollector collector; + collector.init(); + + // Computor 0 reports for contracts 3 and 1 + unsigned int comp0Indices[2] = {3, 1}; + unsigned long long comp0Fees[2] = {1000, 2000}; + collector.storeReportEntries(comp0Indices, comp0Fees, /*numEntries=*/2, /*computorIndex=*/0); + + // Computor 5 reports for contracts 3 and 2 (different fee for contract 3) + unsigned int comp5Indices[2] = {3, 2}; + unsigned long long comp5Fees[2] = {1500, 3000}; + collector.storeReportEntries(comp5Indices, comp5Fees, /*numEntries=*/2, /*computorIndex=*/5); + + // Computor 10 reports for contract 1 (different fee than computor 0) + unsigned int comp10Indices[1] = {1}; + unsigned long long comp10Fees[1] = {2500}; + collector.storeReportEntries(comp10Indices, comp10Fees, /*numEntries=*/1, /*computorIndex=*/10); + + // Verify contract 3 has reports from computors 0 and 5 + const unsigned long long* reports3 = collector.getReportsForContract(3); + EXPECT_EQ(reports3[0], 1000); + EXPECT_EQ(reports3[5], 1500); + EXPECT_EQ(reports3[10], 0); // Computor 10 didn't report for contract 3 + + // Verify contract 1 has reports from computors 0 and 10 + const unsigned long long* reports1 = collector.getReportsForContract(1); + EXPECT_EQ(reports1[0], 2000); + EXPECT_EQ(reports1[5], 0); // Computor 5 didn't report for contract 1 + EXPECT_EQ(reports1[10], 2500); + + // Verify contract 2 has report only from computor 5 + const unsigned long long* reports2 = collector.getReportsForContract(2); + EXPECT_EQ(reports2[0], 0); + EXPECT_EQ(reports2[5], 3000); + EXPECT_EQ(reports2[10], 0); +} + +TEST(ExecutionFeeReportBuilder, BuildAndParseEvenEntries) { + ExecutionFeeReportPayload payload; + unsigned long long contractTimes[contractCount] = {0}; + contractTimes[1] = 200; + contractTimes[3] = 100; + + unsigned int entryCount = buildExecutionFeeReportPayload(payload, contractTimes, 5, 1, 1); + EXPECT_EQ(entryCount, 2u); + + // Verify transaction is valid and parseable + Transaction* tx = (Transaction*)&payload; + EXPECT_TRUE(ExecutionFeeReportTransactionPrefix::isValidEntryAlignment(tx)); + EXPECT_EQ(ExecutionFeeReportTransactionPrefix::getNumEntries(tx), 2u); + + const unsigned int* indices = ExecutionFeeReportTransactionPrefix::getContractIndices(tx); + const unsigned long long* fees = ExecutionFeeReportTransactionPrefix::getExecutionFees(tx); + EXPECT_EQ(indices[0], 1u); + EXPECT_EQ(fees[0], 200); // (200 * 1) / 1 + EXPECT_EQ(indices[1], 3u); + EXPECT_EQ(fees[1], 100); // (100 * 1) / 1 +} + +TEST(ExecutionFeeReportBuilder, BuildAndParseOddEntries) { + ExecutionFeeReportPayload payload; + unsigned long long contractTimes[contractCount] = {0}; + contractTimes[1] = 100; + contractTimes[2] = 300; + contractTimes[5] = 600; + + unsigned int entryCount = buildExecutionFeeReportPayload(payload, contractTimes, 10, 1, 1); + EXPECT_EQ(entryCount, 3u); + + // Verify transaction is valid and parseable (with alignment padding) + Transaction* tx = (Transaction*)&payload; + EXPECT_TRUE(ExecutionFeeReportTransactionPrefix::isValidEntryAlignment(tx)); + EXPECT_EQ(ExecutionFeeReportTransactionPrefix::getNumEntries(tx), 3u); + + const unsigned int* indices = ExecutionFeeReportTransactionPrefix::getContractIndices(tx); + const unsigned long long* fees = ExecutionFeeReportTransactionPrefix::getExecutionFees(tx); + EXPECT_EQ(indices[0], 1u); + EXPECT_EQ(fees[0], 100); // (100 * 1) / 1 + EXPECT_EQ(indices[1], 2u); + EXPECT_EQ(fees[1], 300); // (300 * 1) / 1 + EXPECT_EQ(indices[2], 5u); + EXPECT_EQ(fees[2], 600); // (600 * 1) / 1 +} + +TEST(ExecutionFeeReportBuilder, NoEntriesReturnsZero) { + ExecutionFeeReportPayload payload; + unsigned long long contractTimes[contractCount] = {0}; + + unsigned int entryCount = buildExecutionFeeReportPayload(payload, contractTimes, 7, 1, 1); + EXPECT_EQ(entryCount, 0u); +} + +TEST(ExecutionFeeReportBuilder, BuildWithDivisionMultiplier) { + ExecutionFeeReportPayload payload; + unsigned long long contractTimes[contractCount] = {0}; + contractTimes[1] = 5; // Will become 0 after (5 * 1) / 10 - should be excluded + contractTimes[2] = 25; // Will become 2 after (25 * 1) / 10 + contractTimes[3] = 100; // Will become 10 after (100 * 1) / 10 + + unsigned int entryCount = buildExecutionFeeReportPayload(payload, contractTimes, 5, 1, 10); + + // Only contracts with non-zero fees after division should be included + EXPECT_EQ(entryCount, 2u); // contracts 0 and 2 (contract 1 becomes 0) + + Transaction* tx = (Transaction*)&payload; + EXPECT_TRUE(ExecutionFeeReportTransactionPrefix::isValidEntryAlignment(tx)); + + const unsigned int* indices = ExecutionFeeReportTransactionPrefix::getContractIndices(tx); + const unsigned long long* fees = ExecutionFeeReportTransactionPrefix::getExecutionFees(tx); + EXPECT_EQ(indices[0], 2u); + EXPECT_EQ(fees[0], 2); // (100 * 1) / 10 + EXPECT_EQ(indices[1], 3u); + EXPECT_EQ(fees[1], 10); // (25 * 1) / 10 +} + +TEST(ExecutionFeeReportBuilder, BuildWithMultiplicationMultiplier) { + ExecutionFeeReportPayload payload; + unsigned long long contractTimes[contractCount] = {0}; + contractTimes[1] = 10; + contractTimes[3] = 25; + + unsigned int entryCount = buildExecutionFeeReportPayload(payload, contractTimes, 8, 100, 1); + EXPECT_EQ(entryCount, 2u); + + const unsigned int* indices = ExecutionFeeReportTransactionPrefix::getContractIndices((Transaction*)&payload); + const unsigned long long* fees = ExecutionFeeReportTransactionPrefix::getExecutionFees((Transaction*)&payload); + EXPECT_EQ(indices[0], 1u); + EXPECT_EQ(fees[0], 1000); // (10 * 100) / 1 + EXPECT_EQ(indices[1], 3u); + EXPECT_EQ(fees[1], 2500); // (25 * 100) / 1 +} + +TEST(ExecutionTimeAccumulatorTest, AddingAndPhaseSwitching) +{ + ExecutionTimeAccumulator accum; + + accum.init(); + frequency = 0; // Bypass microseconds conversion + + accum.addTime(/*contractIndex=*/0, /*time=*/52784); + accum.addTime(/*contractIndex=*/contractCount/2, /*time=*/8795); + + accum.startNewAccumulation(); + + const unsigned long long* prevPhaseTimes = accum.getPrevPhaseAccumulatedTimes(); + + for (unsigned int c = 0; c < contractCount; ++c) + { + if (c == 0) + EXPECT_EQ(prevPhaseTimes[c], 52784); + else if (c == contractCount / 2) + EXPECT_EQ(prevPhaseTimes[c], 8795); + else + EXPECT_EQ(prevPhaseTimes[c], 0); + } +} diff --git a/test/fourq.cpp b/test/fourq.cpp new file mode 100644 index 000000000..1f606ef9d --- /dev/null +++ b/test/fourq.cpp @@ -0,0 +1,264 @@ +#define NO_UEFI + +#include "../src/platform/memory.h" +#include "../src/four_q.h" +#include "utils.h" + +#include +#include "gtest/gtest.h" + +#include +#include + +static constexpr int ID_SIZE = 61; +static inline void getIDChar(const unsigned char* key, char* identity, bool isLowerCase) +{ + CHAR16 computorID[61]; + getIdentity(key, computorID, true); + for (int k = 0; k < 60; ++k) + { + identity[k] = computorID[k] - L'a' + 'a'; + } + identity[60] = 0; +} + +TEST(TestFourQ, TestMultiply) +{ + // 8 test cases for 256-bit multiplication + unsigned long long a[8][4] = { + {9951791076627133056ULL, 8515301911953011018ULL, 10503917255838740547ULL, 9403542041099946340ULL}, + {9634782769625085733ULL, 3923345248364070851ULL, 12874006609097115757ULL, 9445681298461330583ULL}, + {9314926113594160360ULL, 9012577733633554087ULL, 15853326627100346762ULL, 3353532907889994600ULL}, + {11822735244239455150ULL, 14860878323222532373ULL, 839169842161576273ULL, 8384082473945502970ULL}, + {6391904870724534887ULL, 7752608459014781040ULL, 8834893383869603648ULL, 14432583643443481392ULL}, + {9034457083341789982ULL, 15550692794033658766ULL, 18370398459251091929ULL, 161212377777301450ULL}, + {12066041174979511630ULL, 6197228902632247602ULL, 15544684064627230784ULL, 8662358800126738212ULL}, + {2997608593061094407ULL, 10746661492960439270ULL, 13066743968851273858ULL, 901611315508727516ULL} + }; + + unsigned long long b[8][4] = { + {14556080569315562443ULL, 4784279743451576405ULL, 16952050128007612055ULL, 17448141405813274955ULL}, + {16953856751996506377ULL, 5957469746201176117ULL, 413985909494190460ULL, 5019301766552018644ULL}, + {8337584125020700765ULL, 9891896711220896307ULL, 3688562803407556063ULL, 15879907979249125147ULL}, + {5253913930687524613ULL, 14356908424098313115ULL, 7294083945257658276ULL, 11357758627518780620ULL}, + {6604082675214113798ULL, 8102242472442817269ULL, 4231600794557460268ULL, 9254306641367892880ULL}, + {15307070962626904180ULL, 14565308158529607085ULL, 7804612167412830134ULL, 11197002641182899202ULL}, + {5681082236069360781ULL, 11354469612480482261ULL, 10740484893427922886ULL, 4093428096946105430ULL}, + {16936346349005670285ULL, 16111331879026478134ULL, 281576863978497861ULL, 4843225515675739317ULL} + }; + + unsigned long long expectedMultiplicationResults[8][8] = { + {13505937776277228416ULL, 10691581058996783029ULL, 15857294677093499275ULL, 10551077288120234079ULL, + 10488747005868148888ULL, 3163167577502768305ULL, 12011108917152358447ULL, 8894487319443104894ULL}, + + {11258722506082215245ULL, 3752109657065586715ULL, 9754007644313481322ULL, 10650543212606486248ULL, + 14000725040689989368ULL, 6868107242688413590ULL, 12132679480588703742ULL, 2570140542862762927ULL}, + + {7980800202961401928ULL, 18091087109835938886ULL, 11937230724836153237ULL, 18437285308724511498ULL, + 9256451621004954121ULL, 2817287347866660760ULL, 7356871972350029435ULL, 2886893956455686033ULL}, + + {14821519003237893222ULL, 11951435221854993875ULL, 5649570579164725909ULL, 16529503750125471729ULL, + 5712698943065886767ULL, 10417044944053178538ULL, 10215165497768617151ULL, 5162124257364100363ULL}, + + {10299854845140439658ULL, 5620198573463725080ULL, 18403939479767000599ULL, 3997239017815343129ULL, + 17558583433366224073ULL, 16662387952814143598ULL, 16240400534467578973ULL, 7240494806558978920ULL}, + + {15852260790186789272ULL, 6843720495231156925ULL, 18209245341578934878ULL, 3229051715759855960ULL, + 6553675393672969791ULL, 11442787882881602486ULL, 5043402961965006398ULL, 97854418782578342ULL}, + + {16919816915648955382ULL, 15728350531867604818ULL, 262149976282468082ULL, 16822220236393767682ULL, + 17482650320082366559ULL, 4634282717190265856ULL, 3892072508178358212ULL, 1922222304195309433ULL}, + + {3674093358531938523ULL, 797775358977430453ULL, 6686355987721165902ULL, 16831290265741585642ULL, + 11378657779800286238ULL, 14872963278680745844ULL, 15850192255010623436ULL, 236719656924026199ULL} + }; + + long long averageProcessingTime = 0; + for (int i = 0; i < 8; i++) + { + unsigned long long result[8] = { 0 }; + + multiply(a[i], b[i], result); + + for (int k = 0; k < 8; k++) + { + EXPECT_EQ(result[k], expectedMultiplicationResults[i][k]) << " at [" << k << "]"; + } + } +} + +TEST(TestFourQ, TestMontgomeryMultiplyModOrder) +{ + + // 8 test cases for 256-bit MontgomeryMultiplyMod + unsigned long long a[8][4] = { + {9951791076627133056ULL, 8515301911953011018ULL, 10503917255838740547ULL, 9403542041099946340ULL}, + {9634782769625085733ULL, 3923345248364070851ULL, 12874006609097115757ULL, 9445681298461330583ULL}, + {9314926113594160360ULL, 9012577733633554087ULL, 15853326627100346762ULL, 3353532907889994600ULL}, + {11822735244239455150ULL, 14860878323222532373ULL, 839169842161576273ULL, 8384082473945502970ULL}, + {6391904870724534887ULL, 7752608459014781040ULL, 8834893383869603648ULL, 14432583643443481392ULL}, + {9034457083341789982ULL, 15550692794033658766ULL, 18370398459251091929ULL, 161212377777301450ULL}, + {12066041174979511630ULL, 6197228902632247602ULL, 15544684064627230784ULL, 8662358800126738212ULL}, + {2997608593061094407ULL, 10746661492960439270ULL, 13066743968851273858ULL, 901611315508727516ULL} + }; + + unsigned long long b[8][4] = { + {14556080569315562443ULL, 4784279743451576405ULL, 16952050128007612055ULL, 17448141405813274955ULL}, + {16953856751996506377ULL, 5957469746201176117ULL, 413985909494190460ULL, 5019301766552018644ULL}, + {8337584125020700765ULL, 9891896711220896307ULL, 3688562803407556063ULL, 15879907979249125147ULL}, + {5253913930687524613ULL, 14356908424098313115ULL, 7294083945257658276ULL, 11357758627518780620ULL}, + {6604082675214113798ULL, 8102242472442817269ULL, 4231600794557460268ULL, 9254306641367892880ULL}, + {15307070962626904180ULL, 14565308158529607085ULL, 7804612167412830134ULL, 11197002641182899202ULL}, + {5681082236069360781ULL, 11354469612480482261ULL, 10740484893427922886ULL, 4093428096946105430ULL}, + {16936346349005670285ULL, 16111331879026478134ULL, 281576863978497861ULL, 4843225515675739317ULL} + }; + + unsigned long long expectedMontgomeryMultiplyModOrderResults[8][4] = { + {1178600784049730938ULL,13475129099769568773ULL,8171380610981515619ULL,8889798462048389782ULL,}, + {9346893806433251032ULL,3783576366952291632ULL,9006661425833189295ULL,2561156787602149305ULL,}, + {15012255874803770290ULL,11566062810664104635ULL,4497422501535458145ULL,2875434161571900946ULL,}, + {12004931297526373125ULL,10222857380028780508ULL,17154413062382081055ULL,5158706721726943589ULL,}, + {9724623868153589743ULL,17410218506619807138ULL,7496133043478274651ULL,7229774243864893754ULL,}, + {10156145653863087884ULL,16403847498912163678ULL,18326820829694769537ULL,90612319098335675ULL,}, + {2160566501146101694ULL,16888000406840707060ULL,8270191582668357443ULL,1911769260568884212ULL,}, + {6774298701428010308ULL,11825708701777781499ULL,11766967071579472107ULL,229574109642630470ULL,}, + }; + + for (int i = 0; i < 8; i++) + { + unsigned long long result[4] = { 0 }; + // (a * b) mod n + Montgomery_multiply_mod_order(a[i], b[i], result); + + for (int k = 0; k < 4; k++) + { + EXPECT_EQ(result[k], expectedMontgomeryMultiplyModOrderResults[i][k]) << " at [" << k << "]"; + } + } +} + +TEST(TestFourQ, TestGenerateKeys) +{ + // Data generate from clang-compiled qubic-cli + unsigned char computorSeeds[][56] = { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nhtighfbfdvgxnxrwxnbfmisknawppoewsycodciozvqpeqegttwofg", + "fcvgljppwwwjhrawxeywxqdgssttiihcmikxbnnunugldvcitkhcrfl", + "eijytgswxqzfkotmqvwulivpbximuhmgydvnaozwszguqflpfvltqge", + }; + unsigned char expectedPrivateIDs[][ID_SIZE] = { + "cctwbaulwuyhybijykxrmxnyrvzbalwryiiahltfwanuafhyfhepcjjgvaec", + "qfldkxspcgsnbgnccmsjftuhxvlfrmarkqrvqjvjaebwoasbytasvcffdfwd", + "mqlgaugwpdaphhqsacmqdcioomybxkaisrwyefyisayrikqjlckwkpdhuqlc", + "qwqmcjhdlzphgabvnjbedsgwrgpbvplcipmxkzuogglbhzfjiytunaeactsn", + }; + unsigned char expectedPublicIDs[][ID_SIZE] = { + "bzbqfllbncxemglobhuvftluplvcpquassilfaboffbcadqssupnwlzbqexk", + "lsgscfhdoahhlbdmlyzrfkvsfrqbbuznganescizcetyxkcdhljhemofxcwb", + "zsvpltnzfdyjzetanimltroldybdzoctvfguybpbvdxbndsrhyreppgccspo", + "xcfqbuwxxtufpfwyteglgchgnqyanubfbkpwtivfobxybgaqcgiqmzlgscwe" + }; + + unsigned char computorSubseeds[32]; + unsigned char computorPrivateKeys[32]; + unsigned char computorPublicKeys[32]; + char privakeyKeyId[ID_SIZE]; + char publicKeyId[ID_SIZE]; + + int numberOfTests = sizeof(computorSeeds) / sizeof(computorSeeds[0]); + + for (int i = 0; i < numberOfTests; ++i) + { + getSubseed(computorSeeds[i], computorSubseeds); + getPrivateKey(computorSubseeds, computorPrivateKeys); + getPublicKey(computorPrivateKeys, computorPublicKeys); + + getIDChar(computorPrivateKeys, privakeyKeyId, true); + getIDChar(computorPublicKeys, publicKeyId, true); + // Verification + for (int k = 0; k < ID_SIZE; ++k) + { + EXPECT_EQ(expectedPrivateIDs[i][k], privakeyKeyId[k]) << " at [" << i << "][" << k << "]"; + EXPECT_EQ(expectedPublicIDs[i][k], publicKeyId[k]) << " at [" << i << "][" << k << "]"; + } + } +} + +// sign(const unsigned char* subseed, const unsigned char* publicKey, const unsigned char* messageDigest, unsigned char* signature) +TEST(TestFourQ, TestSign) +{ + // For sign and verification, some constants need to be set +#ifdef __AVX512F__ + initAVX512FourQConstants(); +#endif + + const std::string subSeedsStr[] = { + "4ac19e2bf0d3776519aabe31924f7dc2589b3d0e7411a65f84c9b16df72c038e", + "e8217c5b40aa91df662803ce4dbf18722e35f1097ac68fb5da10643a825799e3", + "6d02f48bcb53ac397fc71a9028e4165df9b87044c53e116a0192d7fa83254bb0", + "3cfa1097be482f6e5ce132c2aa657d0fb9d84121048de6f05b90a2cc136bf73a", + "5208dd447a21f3c9911cae547637c580e74fa40d2a9c5e1fb26dcbfa408539d1", + "987b163dc6fd492573b4e18af7016c52a027ced5345fb8904e69037ed2ac1b81", + "d462ab0c83f931c4a87f12953e20bd576be849da5a8f1402c3b176ef92486d05", + "713fc86e289f124bdb2a65f6a37c01e0559a148ebf430f6729c184de76b23a90", + "15cd5b0700000000b168de3a00000000743af15000000000efcdab0000000000" + }; + + const std::string messageDigestsStr[] = { + "94e120a4d3f58c217a53eb9046d9f2c5b11288a9fe340d6ce5a771cf04b82e63", + "77f493b58ea40162dc33f9a718e2543b05f629884d7ca0e31598c45f021ae7c0", + "5cc82fa973101da5bfb3e2448196f0a7d7d3324c86fbbe42907613d5c8c2f1a4", + "c01ae5f2879d11439b30ddae5f4c7b22689f023e17b4955c3b2f05e8d9089af6", + "2f893e70ad52c9186eb4b60dfe137288c4a9e0fb6d34a51897e2365a01b0d443", + "ec09a3f415c2dda5f8419026678ab03524f67ed9817ba230cd24513750e01bc6", + "48b59f32a61dff0e13528e4c7937bdf080c3efa7d221364be87f01d5c60f882a", + "b31e704c8a3f1d02692f05a7d8e5f6c911d370f4a68b2ec3fa4c51d7289003de", + "89d5f92a895987457400219e121e8730f6b248a1fd28bfee017611ef079105b5" + }; + + const std::string expectedSignaturesStr[] = + { + "357d47b1366f33eed311a4458ec7326d35728e9292328a9b7ff8d4ec0f7b0df9323f5d1cd01bd5a380a1a8e4f29ad3ae9c5d94e84f4181a61ca73030d6d11600", + "7d2479a15746839c4c5e1fdf0aadb167974c292ceee80593e18b5135763db63163d8eee5bd309c506f47b16cd1242ddfe985887b19d3943c14ec6ab79a9c1900", + "1b8ed83af3dc12deb1554f48df46bf5bc5e4654f62f97ef20656fae4e965ac87762c9fe6189dfe89192a619bca4a6c390f4e97bb1f926041263f2ba4206b0c00", + "470ad247ff6b2e55d44e9f2a79ce402bfc5e8c5322ed297f71939a9c5398b6fcb5058c05e614d10d90d6bdec8ee4ecc6462cdd54e0ea830fde6be465de3f2900", + "0851db3d4021bdc8cf3816b4672aba2f5f7cd0bf19e779e28ee60241bc4246dabe7442a11953703a44ed1cadd0af9fce683c5a312326341ac7a3e55a18c40100", + "54466ae5ecad45c83798e4e3e02ab40e834bf8d3f4f1628b300601ab87894599a43278efd48be7e9615cd569e656356a9e2307ae257b85a3f1f0f333f2302200", + "6d67294ccd03dc51fdb3bca649b7e060d3cf06c417e7053472ca617b93e5926928a7a48b1791c2487c7e83eeb4919046493709508c0541d1c02e9545401b2100", + "20764a88943fb4e796f81a560bde5e652c82ffb203c00b4846102a268ae68f64cdee6c7a3edbf4de48dd25fd423a4b40e79d97a2a47fd11030b6f30a09130b00", + "9f71d3138ff8a72db3b39883e056ce7f5bfe40de6387e64eff0c17e72bd1862ccd848000be1841725f1da87654235329b685e1c81c939cb0154bbc8d30a20c00", + }; + + constexpr size_t numberOfTests = sizeof(subSeedsStr) / sizeof(subSeedsStr[0]); + m256i subseeds[numberOfTests]; + m256i messageDigests[numberOfTests]; + unsigned char expectedSignatures[numberOfTests][64]; + + for (unsigned long long i = 0; i < numberOfTests; ++i) + { + subseeds[i] = test_utils::hexTo32Bytes(subSeedsStr[i], 32); + messageDigests[i] = test_utils::hexTo32Bytes(messageDigestsStr[i], 32); + test_utils::hexToByte(expectedSignaturesStr[i], 64, expectedSignatures[i]); + } + + for (unsigned long long i = 0; i < numberOfTests; ++i) + { + unsigned char publicKey[32]; + unsigned char privateKey[32]; + getPrivateKey(subseeds[i].m256i_u8, privateKey); + getPublicKey(privateKey, publicKey); + + unsigned char signature[64]; + sign(subseeds[i].m256i_u8, publicKey, messageDigests[i].m256i_u8, signature); + + // Verify functions + bool verifyStatus = verify(publicKey, messageDigests[i].m256i_u8, signature); + + EXPECT_TRUE(verifyStatus); + + for (int k = 0; k < 64; ++k) + { + EXPECT_EQ(expectedSignatures[i][k], signature[k]) << " at [" << i << "][" << k << "]"; + } + } +} diff --git a/test/logging_test.h b/test/logging_test.h index 08bfe909a..cefbacf46 100644 --- a/test/logging_test.h +++ b/test/logging_test.h @@ -15,6 +15,16 @@ #undef MAX_NUMBER_OF_TICKS_PER_EPOCH #define MAX_NUMBER_OF_TICKS_PER_EPOCH 3000 +// Reduce virtual memory size for testing +#undef LOG_BUFFER_PAGE_SIZE +#undef PMAP_LOG_PAGE_SIZE +#undef IMAP_LOG_PAGE_SIZE +#undef VM_NUM_CACHE_PAGE +#define LOG_BUFFER_PAGE_SIZE 10000000ULL +#define PMAP_LOG_PAGE_SIZE 1000000ULL +#define IMAP_LOG_PAGE_SIZE 300ULL +#define VM_NUM_CACHE_PAGE 1 + #include "logging/logging.h" class LoggingTest diff --git a/test/packages.config b/test/packages.config index a450605d2..d6f4da2b1 100644 --- a/test/packages.config +++ b/test/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/test/pending_txs_pool.cpp b/test/pending_txs_pool.cpp new file mode 100644 index 000000000..b0c6338fc --- /dev/null +++ b/test/pending_txs_pool.cpp @@ -0,0 +1,542 @@ +#define NO_UEFI + +#include "gtest/gtest.h" + +// workaround for name clash with stdlib +#define system qubicSystemStruct + +#include "../src/contract_core/contract_def.h" +#include "../src/contract_core/contract_exec.h" +#include "../src/contract_core/qpi_spectrum_impl.h" + +#include "../src/public_settings.h" +#undef PENDING_TXS_POOL_NUM_TICKS +#define PENDING_TXS_POOL_NUM_TICKS 50ULL +#undef NUMBER_OF_TRANSACTIONS_PER_TICK +#define NUMBER_OF_TRANSACTIONS_PER_TICK 128ULL +#include "../src/ticking/pending_txs_pool.h" + +#include +#include + +static constexpr unsigned int NUM_INITIALIZED_ENTITIES = 200U; + +class TestPendingTxsPool : public PendingTxsPool +{ + unsigned char transactionBuffer[MAX_TRANSACTION_SIZE]; +public: + TestPendingTxsPool() + { + // we need the spectrum for tx priority calculation + EXPECT_TRUE(initSpectrum()); + memset(spectrum, 0, spectrumSizeInBytes); + for (unsigned int i = 0; i < NUM_INITIALIZED_ENTITIES; i++) + { + // create NUM_INITIALIZED_ENTITIES entities with balance > 0 to get desired txs priority + spectrum[i].incomingAmount = i + 1; + spectrum[i].outgoingAmount = 0; + spectrum[i].publicKey = m256i{0, 0, 0, i + 1 }; + + // create NUM_INITIALIZED_ENTITIES entities with balance = 0 for testing + spectrum[NUM_INITIALIZED_ENTITIES + i].incomingAmount = 0; + spectrum[NUM_INITIALIZED_ENTITIES + i].outgoingAmount = 0; + spectrum[NUM_INITIALIZED_ENTITIES + i].publicKey = m256i{ 0, 0, 0, NUM_INITIALIZED_ENTITIES + i + 1 }; + } + updateSpectrumInfo(); + } + + ~TestPendingTxsPool() + { + deinitSpectrum(); + } + + static constexpr unsigned int getMaxNumTxsPerTick() + { + return maxNumTxsPerTick; + } + + bool addTransaction(unsigned int tick, long long amount, unsigned int inputSize, const m256i* dest = nullptr, const m256i* src = nullptr) + { + Transaction* transaction = (Transaction*)transactionBuffer; + transaction->amount = amount; + if (dest == nullptr) + transaction->destinationPublicKey.setRandomValue(); + else + transaction->destinationPublicKey.assign(*dest); + if (src == nullptr) + transaction->sourcePublicKey.setRandomValue(); + else + transaction->sourcePublicKey.assign(*src); + transaction->inputSize = inputSize; + transaction->inputType = 0; + transaction->tick = tick; + + return add(transaction); + } + + unsigned int addTickTransactions(unsigned int tick, unsigned long long seed, unsigned int maxTransactions) + { + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + unsigned int numTransactionsAdded = 0; + + // add transactions of tick + unsigned int transactionNum = gen64() % (maxTransactions + 1); + for (unsigned int transaction = 0; transaction < transactionNum; ++transaction) + { + unsigned int inputSize = gen64() % MAX_INPUT_SIZE; + long long amount = gen64() % MAX_AMOUNT; + m256i srcPublicKey = m256i{ 0, 0, 0, (gen64() % NUM_INITIALIZED_ENTITIES) + 1 }; + if (addTransaction(tick, amount, inputSize, /*dest=*/nullptr, &srcPublicKey)) + numTransactionsAdded++; + } + checkStateConsistencyWithAssert(); + + return numTransactionsAdded; + } + + void checkTickTransactions(unsigned int tick, unsigned long long seed, unsigned int maxTransactions) + { + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // check transactions of tick + unsigned int transactionNum = gen64() % (maxTransactions + 1); + + for (unsigned int transaction = 0; transaction < transactionNum; ++transaction) + { + unsigned int expectedInputSize = gen64() % MAX_INPUT_SIZE; + long long expectedAmount = gen64() % MAX_AMOUNT; + m256i expectedSrcPublicKey = m256i{ 0, 0, 0, (gen64() % NUM_INITIALIZED_ENTITIES) + 1 }; + + Transaction* tp = getTx(tick, transaction); + + ASSERT_NE(tp, nullptr); + + EXPECT_TRUE(tp->checkValidity()); + EXPECT_EQ(tp->tick, tick); + EXPECT_EQ(static_cast(tp->inputSize), expectedInputSize); + EXPECT_EQ(tp->amount, expectedAmount); + EXPECT_TRUE(tp->sourcePublicKey == expectedSrcPublicKey); + + m256i* digest = getDigest(tick, transaction); + + ASSERT_NE(digest, nullptr); + + m256i tpDigest; + KangarooTwelve(tp, tp->totalSize(), &tpDigest, 32); + EXPECT_EQ(*digest, tpDigest); + } + } +}; + + +TEST(TestPendingTxsPool, EpochTransition) +{ + TestPendingTxsPool pendingTxsPool; + + unsigned long long seed = 42; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 3 epoch transitions + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + constexpr unsigned int firstEpochTicks = PENDING_TXS_POOL_NUM_TICKS; + // second epoch start will reset the pool completely because secondEpochTick0 is not contained + constexpr unsigned int secondEpochTicks = PENDING_TXS_POOL_NUM_TICKS / 2; + // thirdEpochTick0 will be contained with newInitialIndex >= buffersBeginIndex + constexpr unsigned int thirdEpochTicks = PENDING_TXS_POOL_NUM_TICKS / 2 + PENDING_TXS_POOL_NUM_TICKS / 4; + // fourthEpochTick0 will be contained with newInitialIndex < buffersBeginIndex + const unsigned int firstEpochTick0 = gen64() % 10000000; + const unsigned int secondEpochTick0 = firstEpochTick0 + firstEpochTicks; + const unsigned int thirdEpochTick0 = secondEpochTick0 + secondEpochTicks; + const unsigned int fourthEpochTick0 = thirdEpochTick0 + thirdEpochTicks; + unsigned long long firstEpochSeeds[firstEpochTicks]; + unsigned long long secondEpochSeeds[secondEpochTicks]; + unsigned long long thirdEpochSeeds[thirdEpochTicks]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + for (int i = 0; i < secondEpochTicks; ++i) + secondEpochSeeds[i] = gen64(); + for (int i = 0; i < thirdEpochTicks; ++i) + thirdEpochSeeds[i] = gen64(); + unsigned int numAdded = 0; + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + // add ticks transactions + for (int i = 0; i < firstEpochTicks; ++i) + numAdded = pendingTxsPool.addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + + // check ticks transactions + for (int i = 0; i < firstEpochTicks; ++i) + pendingTxsPool.checkTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + + pendingTxsPool.checkStateConsistencyWithAssert(); + + // Epoch transistion + pendingTxsPool.beginEpoch(secondEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(secondEpochTick0), 0); + + // add ticks transactions + for (int i = 0; i < secondEpochTicks; ++i) + numAdded = pendingTxsPool.addTickTransactions(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); + + // check ticks transactions + for (int i = 0; i < secondEpochTicks; ++i) + pendingTxsPool.checkTickTransactions(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); + + // add a transaction for the next epoch + numAdded = pendingTxsPool.addTickTransactions(thirdEpochTick0 + 1, thirdEpochSeeds[1], maxTransactions); + + pendingTxsPool.checkStateConsistencyWithAssert(); + + // Epoch transistion + pendingTxsPool.beginEpoch(thirdEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(thirdEpochTick0), numAdded); + + // add ticks transactions + for (int i = 2; i < thirdEpochTicks; ++i) + numAdded = pendingTxsPool.addTickTransactions(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); + + // check ticks transactions + for (int i = 1; i < thirdEpochTicks; ++i) + pendingTxsPool.checkTickTransactions(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); + + // add a transaction for the next epoch + numAdded = pendingTxsPool.addTickTransactions(fourthEpochTick0 + 1, /*seed=*/42, maxTransactions); + + pendingTxsPool.checkStateConsistencyWithAssert(); + + // Epoch transistion + pendingTxsPool.beginEpoch(fourthEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(fourthEpochTick0), numAdded); + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, TotalNumberOfPendingTxs) +{ + TestPendingTxsPool pendingTxsPool; + unsigned long long seed = 1337; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 1 epoch + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const int firstEpochTicks = (gen64() % (4 * PENDING_TXS_POOL_NUM_TICKS)) + 1; + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned long long firstEpochSeeds[4 * PENDING_TXS_POOL_NUM_TICKS]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add ticks transactions + std::vector numTransactionsAdded(firstEpochTicks); + std::vector numPendingTransactions(firstEpochTicks, 0); + for (int i = firstEpochTicks - 1; i >= 0; --i) + { + numTransactionsAdded[i] = pendingTxsPool.addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + if (i > 0) + { + numPendingTransactions[i - 1] = numPendingTransactions[i] + numTransactionsAdded[i]; + } + } + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), (unsigned int)numTransactionsAdded[0] + numPendingTransactions[0]); + for (int i = 0; i < firstEpochTicks; ++i) + { + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 + i), (unsigned int)numPendingTransactions[i]); + } + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, NumberOfPendingTickTxs) +{ + TestPendingTxsPool pendingTxsPool; + unsigned long long seed = 67534; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 1 epoch + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + constexpr unsigned int firstEpochTicks = PENDING_TXS_POOL_NUM_TICKS; + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned long long firstEpochSeeds[firstEpochTicks]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add ticks transactions + std::vector numTransactionsAdded(firstEpochTicks); + for (int i = firstEpochTicks - 1; i >= 0; --i) + { + numTransactionsAdded[i] = pendingTxsPool.addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + } + + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0 - 1), 0); + for (int i = 0; i < firstEpochTicks; ++i) + { + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0 + i), (unsigned int)numTransactionsAdded[i]); + } + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, IncrementFirstStoredTick) +{ + TestPendingTxsPool pendingTxsPool; + unsigned long long seed = 84129; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 1 epoch + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const int firstEpochTicks = (gen64() % (4 * PENDING_TXS_POOL_NUM_TICKS)) + 1; + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned long long firstEpochSeeds[4 * PENDING_TXS_POOL_NUM_TICKS]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add ticks transactions + std::vector numTransactionsAdded(firstEpochTicks); + std::vector numPendingTransactions(firstEpochTicks, 0); + for (int i = firstEpochTicks - 1; i >= 0; --i) + { + numTransactionsAdded[i] = pendingTxsPool.addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + if (i > 0) + { + numPendingTransactions[i - 1] = numPendingTransactions[i] + numTransactionsAdded[i]; + } + } + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), (unsigned int)numTransactionsAdded[0] + numPendingTransactions[0]); + for (int i = 0; i < firstEpochTicks; ++i) + { + pendingTxsPool.incrementFirstStoredTick(); + for (int tx = 0; tx < numTransactionsAdded[i]; ++tx) + { + EXPECT_EQ(pendingTxsPool.getTx(firstEpochTick0 + i, 0), nullptr); + EXPECT_EQ(pendingTxsPool.getDigest(firstEpochTick0 + i, 0), nullptr); + } + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 + i), (unsigned int)numPendingTransactions[i]); + } + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, TxsPrioritizationMoreThanMaxTxs) +{ + TestPendingTxsPool pendingTxsPool; + unsigned long long seed = 9532; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned int numAdditionalTxs = 64; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add more than `pendingTxsPool.getMaxNumTxsPerTick()` with increasing priority + // (entities were set up in a way that u64._0 of the public key corresponds to their balance) + m256i srcPublicKey = m256i::zero(); + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick() + numAdditionalTxs; ++t) + { + srcPublicKey.u64._3 = t + 1; + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, /*amount=*/t + 1, /*inputSize=*/0, /*dest=*/nullptr, &srcPublicKey)); + } + + // adding lower priority tx does not work + srcPublicKey.u64._3 = 1; + EXPECT_FALSE(pendingTxsPool.addTransaction(firstEpochTick0, /*amount=*/1, /*inputSize=*/0, /*dest=*/nullptr, &srcPublicKey)); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), pendingTxsPool.getMaxNumTxsPerTick()); + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0), pendingTxsPool.getMaxNumTxsPerTick()); + + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick(); ++t) + { + if (t < numAdditionalTxs) + EXPECT_EQ(pendingTxsPool.getTx(firstEpochTick0, t)->amount, pendingTxsPool.getMaxNumTxsPerTick() + t + 1); + else + EXPECT_EQ(pendingTxsPool.getTx(firstEpochTick0, t)->amount, t + 1); + } + + pendingTxsPool.deinit(); +} + +TEST(TestPendingTxsPool, RejectDuplicateTxs) +{ + TestPendingTxsPool pendingTxsPool; + unsigned long long seed = 9532; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + constexpr unsigned int numTxs = pendingTxsPool.getMaxNumTxsPerTick() / 2; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // try to add duplicate transactions: same dest, src, amount, tick, input size/type + m256i dest{ 562, 789, 234, 121 }; + m256i src{ 0, 0, 0, NUM_INITIALIZED_ENTITIES / 3 }; + long long amount = 1; + + // first add should succeed, all others should fail + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, amount, /*inputSize=*/0, &dest, &src)); + for (unsigned int t = 1; t < numTxs; ++t) + EXPECT_FALSE(pendingTxsPool.addTransaction(firstEpochTick0, amount, /*inputSize=*/0, &dest, &src)); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), 1); + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0), 1); + + Transaction* tx = pendingTxsPool.getTx(firstEpochTick0, 0); + EXPECT_TRUE(tx->checkValidity()); + EXPECT_EQ(tx->amount, amount); + EXPECT_EQ(tx->tick, firstEpochTick0); + EXPECT_EQ(static_cast(tx->inputSize), 0U); + EXPECT_TRUE(tx->destinationPublicKey == dest); + EXPECT_TRUE(tx->sourcePublicKey == src); + + pendingTxsPool.deinit(); +} + +TEST(TestPendingTxsPool, ProtocolLevelTxsMaxPriority) +{ + TestPendingTxsPool pendingTxsPool; + unsigned long long seed = 9532; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // fill the PendingTxsPool completely for tick `firstEpochTick0` + m256i srcPublicKey = m256i::zero(); + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick(); ++t) + { + srcPublicKey.u64._3 = t + 1; + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + } + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), pendingTxsPool.getMaxNumTxsPerTick()); + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0), pendingTxsPool.getMaxNumTxsPerTick()); + + Transaction tx { + .sourcePublicKey = m256i{ 0, 0, 0, (gen64() % NUM_INITIALIZED_ENTITIES) + 1 }, + .destinationPublicKey = m256i::zero(), + .amount = 0, .tick = firstEpochTick0, + .inputType = VOTE_COUNTER_INPUT_TYPE, + .inputSize = 0, + }; + + EXPECT_TRUE(pendingTxsPool.add(&tx)); + + tx.inputType = CustomMiningSolutionTransaction::transactionType(); + + EXPECT_TRUE(pendingTxsPool.add(&tx)); + + pendingTxsPool.deinit(); +} + +TEST(TestPendingTxsPool, TxsWithSrcBalance0AreRejected) +{ + TestPendingTxsPool pendingTxsPool; + unsigned long long seed = 3452; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // partially fill the PendingTxsPool for tick `firstEpochTick0` + m256i srcPublicKey = m256i::zero(); + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick() / 2; ++t) + { + srcPublicKey.u64._3 = t + 1; + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + } + + // public key with balance 0 + srcPublicKey.u64._3 = NUM_INITIALIZED_ENTITIES + 1 + (gen64() % NUM_INITIALIZED_ENTITIES); + EXPECT_FALSE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + + // non-existant public key + srcPublicKey = m256i{ 0, gen64() % MAX_AMOUNT, 0, 0}; + EXPECT_FALSE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + + pendingTxsPool.deinit(); + } +} \ No newline at end of file diff --git a/test/qpi.cpp b/test/qpi.cpp index 2f6b15ba6..9ea0b67fe 100644 --- a/test/qpi.cpp +++ b/test/qpi.cpp @@ -1,19 +1,9 @@ #define NO_UEFI -#include "gtest/gtest.h" +#include "contract_testing.h" #include -// workaround for name clash with stdlib -#define system qubicSystemStruct - -#include "contract_core/contract_def.h" -#include "contract_core/contract_exec.h" - -#include "../src/contract_core/qpi_trivial_impl.h" -#include "../src/contract_core/qpi_proposal_voting.h" -#include "../src/contract_core/qpi_system_impl.h" - // changing offset simulates changed computor set with changed epoch void initComputors(unsigned short computorIdOffset) { @@ -23,6 +13,126 @@ void initComputors(unsigned short computorIdOffset) } } +TEST(TestCoreQPI, SafeMath) +{ + { + sint64 a = -1000000000000LL; // This is valid - negative signed integer + sint64 b = 2; // Positive signed integer + EXPECT_EQ(smul(a, b), -2000000000000); + } + + { + uint64_t a = 1000000; + uint64_t b = 2000000; + uint64_t expected_ok = 2000000000000ULL; + EXPECT_EQ(smul(a, b), expected_ok); + } + { + sint64 a = INT64_MIN; // -9223372036854775808 + sint64 b = -1; // -1 + EXPECT_EQ(smul(a, b), INT64_MAX); + } + { + uint64_t a = 123456789ULL; + uint64_t b = 987654321ULL; + + // Case: Multiplication by zero. + EXPECT_EQ(smul(a, 0ULL), 0ULL); + EXPECT_EQ(smul(0ULL, b), 0ULL); + + // Case: Multiplication by one. + EXPECT_EQ(smul(a, 1ULL), a); + EXPECT_EQ(smul(1ULL, b), b); + } + { + // Case: A clear overflow case. + // UINT64_MAX is approximately 1.84e19. + uint64_t c = 4000000000ULL; + uint64_t d = 5000000000ULL; // c * d is 2e19, which overflows. + EXPECT_EQ(smul(c, d), UINT64_MAX); + } + { + // Case: Test the exact boundary of overflow. + uint64_t max_val = UINT64_MAX; + uint64_t divisor = 2; + uint64_t limit = max_val / divisor; + + // This should not overflow. + EXPECT_EQ(smul(limit, divisor), limit * 2); + + // This should overflow and clamp. + EXPECT_EQ(smul(limit + 1, divisor), UINT64_MAX); + } + { + // Case: A simple multiplication that does not overflow. + int64_t e = 1000000; + int64_t f = -2000000; + EXPECT_EQ(smul(e, f), -2000000000000LL); + } + { + // Case: Positive * Positive, causing overflow. + int64_t a = INT64_MAX / 2; + int64_t b = 3; + EXPECT_EQ(smul(a, b), INT64_MAX); + } + { + int64_t a = INT64_MAX / 2; + int64_t b = 3; + int64_t c = -3; + int64_t d = INT64_MIN / 2; + + // Case: Positive * Negative, causing underflow. + EXPECT_EQ(smul(a, c), INT64_MIN); + + // Case: Negative * Positive, causing underflow. + EXPECT_EQ(smul(d, b), INT64_MIN); + } + { + // Case: Negative * Negative, causing overflow. + int64_t c = -3; + int64_t d = INT64_MIN / 2; + EXPECT_EQ(smul(d, c), INT64_MAX); + } + { + // --- Unsigned 32-bit Tests --- + // No Overflow + uint32_t a_u32 = 60000; + uint32_t b_u32 = 60000; + EXPECT_EQ(smul(a_u32, b_u32), 3600000000U); + + // Overflow + uint32_t c_u32 = 70000; + uint32_t d_u32 = 70000; // 70000*70000 = 4,900,000,000 which is > UINT32_MAX (~4.29e9) + EXPECT_EQ(smul(c_u32, d_u32), UINT32_MAX); + + // Boundary + uint32_t limit_u32 = UINT32_MAX / 2; + uint32_t divisor_u32 = 2; + EXPECT_EQ(smul(limit_u32, divisor_u32), limit_u32 * 2); + EXPECT_EQ(smul(limit_u32 + 1, divisor_u32), UINT32_MAX); + + // --- Signed 32-bit Tests --- + // No Overflow + int32_t a_s32 = 10000; + int32_t b_s32 = -10000; + EXPECT_EQ(smul(a_s32, b_s32), -100000000); + + // Positive Overflow + int32_t c_s32 = INT32_MAX / 2; + int32_t d_s32 = 3; + EXPECT_EQ(smul(c_s32, d_s32), INT32_MAX); + + // Underflow + int32_t e_s32 = INT32_MIN / 2; + int32_t f_s32 = 3; + EXPECT_EQ(smul(e_s32, f_s32), INT32_MIN); + + // Negative * Negative, causing overflow. + int32_t g_s32 = -3; + int32_t h_s32 = INT32_MIN / 2; + EXPECT_EQ(smul(h_s32, g_s32), INT32_MAX); + } +} TEST(TestCoreQPI, Array) { @@ -97,15 +207,15 @@ TEST(TestCoreQPI, BitArray) EXPECT_EQ(b1.get(0), 0); b1.set(0, true); EXPECT_EQ(b1.get(0), 1); - - b1.setAll(0); + + b1.setAll(0); QPI::BitArray<1> b1_2; b1_2.setAll(0); QPI::BitArray<1> b1_3; b1_3.setAll(1); - EXPECT_TRUE(b1 == b1_2); - EXPECT_TRUE(b1 != b1_3); - EXPECT_FALSE(b1 == b1_3); + EXPECT_TRUE(b1 == b1_2); + EXPECT_TRUE(b1 != b1_3); + EXPECT_FALSE(b1 == b1_3); QPI::BitArray<64> b64; EXPECT_EQ(b64.capacity(), 64); @@ -128,7 +238,7 @@ TEST(TestCoreQPI, BitArray) llu1.setMem(b64); EXPECT_EQ(llu1.get(0), 0xffffffffffffffffllu); - + b64.setMem(0x11llu); QPI::BitArray<64> b64_2; EXPECT_EQ(b64.capacity(), 64); @@ -136,10 +246,10 @@ TEST(TestCoreQPI, BitArray) QPI::BitArray<64> b64_3; EXPECT_EQ(b64.capacity(), 64); b64_3.setMem(0x55llu); - EXPECT_TRUE(b64 == b64_2); - EXPECT_TRUE(b64 != b64_3); - EXPECT_FALSE(b64 == b64_3); - + EXPECT_TRUE(b64 == b64_2); + EXPECT_TRUE(b64 != b64_3); + EXPECT_FALSE(b64 == b64_3); + //QPI::BitArray<96> b96; // must trigger compile error QPI::BitArray<128> b128; @@ -173,15 +283,15 @@ TEST(TestCoreQPI, BitArray) { EXPECT_EQ(b128.get(i), i % 2 == 0); } - + b128.setAll(1); QPI::BitArray<128> b128_2; QPI::BitArray<128> b128_3; b128_2.setAll(1); b128_3.setAll(0); - EXPECT_TRUE(b128 == b128_2); - EXPECT_TRUE(b128 != b128_3); - EXPECT_FALSE(b128 == b128_3); + EXPECT_TRUE(b128 == b128_2); + EXPECT_TRUE(b128 != b128_3); + EXPECT_FALSE(b128 == b128_3); } TEST(TestCoreQPI, Div) { @@ -255,19 +365,21 @@ TEST(TestCoreQPI, ProposalAndVotingByComputors) // Memory must be zeroed to work, which is done in contract states on init QPI::setMemory(pv, 0); - // voter index is computor index + // vote index is computor index for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) { - EXPECT_EQ(pv.getVoterIndex(qpi, qpi.computor(i)), i); + EXPECT_EQ(pv.getVoteIndex(qpi, qpi.computor(i)), i); EXPECT_EQ(pv.getVoterId(qpi, i), qpi.computor(i)); + EXPECT_EQ(pv.getVoteCount(qpi, i), 1); } for (int i = NUMBER_OF_COMPUTORS; i < 800; ++i) { QPI::id testId(i, 9, 8, 7); - EXPECT_EQ(pv.getVoterIndex(qpi, testId), QPI::INVALID_VOTER_INDEX); + EXPECT_EQ(pv.getVoteIndex(qpi, testId), QPI::INVALID_VOTE_INDEX); EXPECT_EQ(pv.getVoterId(qpi, i), QPI::NULL_ID); + EXPECT_EQ(pv.getVoteCount(qpi, i), 0); } - EXPECT_EQ(pv.getVoterIndex(qpi, qpi.originator()), QPI::INVALID_VOTER_INDEX); + EXPECT_EQ(pv.getVoteIndex(qpi, qpi.originator()), QPI::INVALID_VOTE_INDEX); // valid proposers are computors for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) @@ -279,7 +391,11 @@ TEST(TestCoreQPI, ProposalAndVotingByComputors) // no existing proposals for (int i = 0; i < 2*NUMBER_OF_COMPUTORS; ++i) + { EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, qpi.computor(i)), (int)QPI::INVALID_PROPOSAL_INDEX); + EXPECT_EQ(pv.getProposerId(qpi, i), NULL_ID); + } + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, NULL_ID), (int)QPI::INVALID_PROPOSAL_INDEX); // fill all slots for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) @@ -293,6 +409,7 @@ TEST(TestCoreQPI, ProposalAndVotingByComputors) { int j = NUMBER_OF_COMPUTORS - 1 - i; EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, qpi.computor(j)), i); + EXPECT_EQ(pv.getProposerId(qpi, i), qpi.computor(j)); } // using other ID fails if full (new computor ID after epoch change) @@ -321,6 +438,240 @@ TEST(TestCoreQPI, ProposalAndVotingByComputors) } } +static void sortContractShareVector(std::vector> & shareholders) +{ + std::sort(shareholders.begin(), shareholders.end(), + [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); +} + +template +static void checkShareholderVotingRights(const QpiContextUserProcedureCall& qpi, const ProposalAndVotingByShareholders& pv, uint16 proposalIdx, std::vector>& shareholderVec) +{ + unsigned int voterIdx = 0; + for (const auto& ownerSharesPair : shareholderVec) + { + const m256i owner = ownerSharesPair.first; + const auto shareCount = ownerSharesPair.second; + EXPECT_EQ(pv.getVoteIndex(qpi, owner, proposalIdx), voterIdx); + for (unsigned int i = 0; i < shareCount; ++i) + { + EXPECT_EQ(pv.getVoterId(qpi, voterIdx, proposalIdx), owner); + EXPECT_EQ(pv.getVoteCount(qpi, voterIdx, proposalIdx), shareCount - i); + ++voterIdx; + } + } + EXPECT_EQ(voterIdx, NUMBER_OF_COMPUTORS); + EXPECT_EQ(pv.getVoteIndex(qpi, NULL_ID, proposalIdx), INVALID_VOTE_INDEX); + EXPECT_EQ(pv.getVoteIndex(qpi, id(12345678, 901234, 5678, 90), proposalIdx), INVALID_VOTE_INDEX); + EXPECT_EQ(pv.getVoterId(qpi, voterIdx, proposalIdx), NULL_ID); + EXPECT_EQ(pv.getVoteCount(qpi, voterIdx, proposalIdx), 0); +} + +TEST(TestCoreQPI, ProposalAndVotingByShareholders) +{ + ContractTesting test; + test.initEmptyUniverse(); + QpiContextUserProcedureCall qpi(QX_CONTRACT_INDEX, QPI::id(1, 2, 3, 4), 123); + ProposalAndVotingByShareholders<3, MSVAULT_ASSET_NAME> pv; + initComputors(0); + + // Memory must be zeroed to work, which is done in contract states on init + setMemory(pv, 0); + + // create contract shares + std::vector> initialPossessorShares{ + {id(100, 2, 3, 4), 10}, + {id(1, 2, 3, 4), 200}, + {id(1, 2, 2, 1), 65}, + {id(1, 2, 3, 1), 1}, + {id(0, 0, 0, 1), 400}, + }; + issueContractShares(MSVAULT_CONTRACT_INDEX, initialPossessorShares); + sortContractShareVector(initialPossessorShares); + + // no existing proposals + for (int i = 0; i < initialPossessorShares.size(); ++i) + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[i].first), (int)INVALID_PROPOSAL_INDEX); + + // valid proposers are current shareholders + for (int i = 0; i < initialPossessorShares.size(); ++i) + EXPECT_TRUE(pv.isValidProposer(qpi, initialPossessorShares[i].first)); + for (int i = 0; i < 2 * NUMBER_OF_COMPUTORS; ++i) + { + EXPECT_FALSE(pv.isValidProposer(qpi, QPI::id(i, 9, 8, 7))); + EXPECT_EQ(pv.getProposerId(qpi, i), NULL_ID); + } + EXPECT_FALSE(pv.isValidProposer(qpi, QPI::NULL_ID)); + + // add proposal + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[0].first), 0); + for (int i = 0; i < initialPossessorShares.size(); ++i) + { + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[i].first), (i == 0) ? 0 : (int)INVALID_PROPOSAL_INDEX); + EXPECT_EQ(pv.getProposerId(qpi, i), (i == 0) ? initialPossessorShares[i].first : NULL_ID); + } + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, NULL_ID), (int)QPI::INVALID_PROPOSAL_INDEX); + + // check voting rights (voters are shareholders at time of creating/updating proposal) + checkShareholderVotingRights(qpi, pv, 0, initialPossessorShares); + + // transfer some shares + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 1, id(20, 2, 3, 5)), 199); + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 1, id(30, 2, 3, 5)), 198); + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 1, id(40, 2, 3, 5)), 197); + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 1, id(50, 2, 3, 5)), 196); + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 16, id(1, 2, 3, 5)), 180); + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 60, id(1, 2, 3, 1)), 120); + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 39, id(1, 2, 3, 0)), 81); + EXPECT_EQ(qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 1, id(1, 2, 3, 2)), 80); + std::vector> changedPossessorShares{ + {id(0, 0, 0, 1), 400}, + {id(1, 2, 2, 1), 65}, + {id(1, 2, 3, 0), 39}, + {id(1, 2, 3, 1), 61}, + {id(1, 2, 3, 2), 1}, + {id(1, 2, 3, 4), 80}, + {id(1, 2, 3, 5), 16}, + {id(20, 2, 3, 5), 1}, + {id(30, 2, 3, 5), 1}, + {id(40, 2, 3, 5), 1}, + {id(50, 2, 3, 5), 1}, + {id(100, 2, 3, 4), 10}, + }; + + // assetsEndEpoch() and as.indexLists.rebuild(), which are called at the end of each epoch, lead to speeding up creating a new proposal (by requiring less sorting operations) + as.indexLists.rebuild(); + + // add another proposal (has changed set of voters) + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[1].first), 1); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[0].first), 0); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[1].first), 1); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[2].first), (int)INVALID_PROPOSAL_INDEX); + + // check voting rights (voters are shareholders at time of creating/updating proposal) + checkShareholderVotingRights(qpi, pv, 0, initialPossessorShares); + checkShareholderVotingRights(qpi, pv, 1, changedPossessorShares); + + // add third proposal (has changed set of voters) + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[2].first), 2); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[0].first), 0); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[1].first), 1); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[2].first), 2); + + // check voting rights (voters are shareholders at time of creating/updating proposal) + checkShareholderVotingRights(qpi, pv, 0, initialPossessorShares); + checkShareholderVotingRights(qpi, pv, 1, changedPossessorShares); + checkShareholderVotingRights(qpi, pv, 2, changedPossessorShares); + + // transfer some shares + qpi.transferShareOwnershipAndPossession(MSVAULT_ASSET_NAME, NULL_ID, qpi.invocator(), qpi.invocator(), 80, id(1, 2, 2, 0)); + std::vector> changedPossessorShares2{ + {id(0, 0, 0, 1), 400}, + {id(1, 2, 2, 0), 80}, + {id(1, 2, 2, 1), 65}, + {id(1, 2, 3, 0), 39}, + {id(1, 2, 3, 1), 61}, + {id(1, 2, 3, 2), 1}, + //{id(1, 2, 3, 4), 0}, + {id(1, 2, 3, 5), 16}, + {id(20, 2, 3, 5), 1}, + {id(30, 2, 3, 5), 1}, + {id(40, 2, 3, 5), 1}, + {id(50, 2, 3, 5), 1}, + {id(100, 2, 3, 4), 10}, + }; + + // all slots filled -> adding proposal using other ID fails + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[3].first), (int)INVALID_PROPOSAL_INDEX); + + // free one slot + pv.freeProposalByIndex(qpi, 1); + + // using other ID now works + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[3].first), 1); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[0].first), 0); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[1].first), (int)INVALID_PROPOSAL_INDEX); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[2].first), 2); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[3].first), 1); + + // check voting rights (voters are shareholders at time of creating/updating proposal) + checkShareholderVotingRights(qpi, pv, 0, initialPossessorShares); + checkShareholderVotingRights(qpi, pv, 1, changedPossessorShares2); + checkShareholderVotingRights(qpi, pv, 2, changedPossessorShares); + + // reusing all slots should work (overwriting proposals sets voters) + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[0].first), 0); + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[1].first), (int)INVALID_PROPOSAL_INDEX); + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[2].first), 2); + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[3].first), 1); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[0].first), 0); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[1].first), (int)INVALID_PROPOSAL_INDEX); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[2].first), 2); + EXPECT_EQ((int)pv.getExistingProposalIndex(qpi, initialPossessorShares[3].first), 1); + + // check voting rights (voters are shareholders at time of creating/updating proposal) + checkShareholderVotingRights(qpi, pv, 0, changedPossessorShares2); + checkShareholderVotingRights(qpi, pv, 1, changedPossessorShares2); + checkShareholderVotingRights(qpi, pv, 2, changedPossessorShares2); +} + +TEST(TestCoreQPI, ProposalAndVotingByShareholdersTestSorting) +{ + constexpr int shareholderFactor = 512; // set 2 to run more tests + constexpr int shareholdersWithTwoRecordsStep = 5; // set 1 to run more tests + + static constexpr uint64 MSVAULT_ASSET_NAME = 23727827095802701; + for (int shareholders = 1; shareholders <= 512; shareholders *= shareholderFactor) + { + for (int shareholdersWithTwoRecords = min(shareholders, 4); shareholdersWithTwoRecords > 0; shareholdersWithTwoRecords -= shareholdersWithTwoRecordsStep) + { + ContractTesting test; + test.initEmptyUniverse(); + QpiContextUserProcedureCall qpi(QX_CONTRACT_INDEX, QPI::id(1, 2, 3, 4), 123); + ProposalAndVotingByShareholders<3, MSVAULT_ASSET_NAME> pv; + initComputors(0); + + // Memory must be zeroed to work, which is done in contract states on init + setMemory(pv, 0); + + // create contract shares + std::vector> initialPossessorShares; + std::vector twoRecords; + for (int i = 0; i < shareholders; ++i) + { + initialPossessorShares.push_back({ id::randomValue(), 1 }); + } + for (int i = 0; i < shareholdersWithTwoRecords; ++i) + { + // randomly select entries that shall have two records + int idx = (int)initialPossessorShares[i].first.u64._0 % initialPossessorShares.size(); + initialPossessorShares[idx].second = 2; + twoRecords.push_back(idx); + } + issueContractShares(MSVAULT_CONTRACT_INDEX, initialPossessorShares, false); + + // transfer assset management rights in order to create two records + Asset asset{ NULL_ID, MSVAULT_ASSET_NAME }; + for (int i = 0; i < shareholdersWithTwoRecords; ++i) + { + const id& possessorId = initialPossessorShares[twoRecords[i]].first; + AssetPossessionIterator it(asset, { possessorId, QX_CONTRACT_INDEX }, { possessorId, QX_CONTRACT_INDEX }); + EXPECT_TRUE(transferShareManagementRights(it.ownershipIndex(), it.possessionIndex(), MSVAULT_CONTRACT_INDEX, MSVAULT_CONTRACT_INDEX, 1, nullptr, nullptr, true)); + } + + // add proposal + EXPECT_EQ((int)pv.getNewProposalIndex(qpi, initialPossessorShares[0].first), 0); + + // check voting rights (voters are shareholders at time of creating/updating proposal) + sortContractShareVector(initialPossessorShares); + checkShareholderVotingRights(qpi, pv, 0, initialPossessorShares); + } + } +} + // Test internal class ProposalWithAllVoteData that stores valid proposals along with its votes template void testProposalWithAllVoteDataOptionVotes( @@ -477,6 +828,30 @@ void testProposalWithAllVoteData() testProposalWithAllVoteDataOptionVotes(pwav, proposal, 26); else EXPECT_FALSE(pwav.set(proposal)); + + // MultiVariablesYesNo proposal + proposal.type = QPI::ProposalTypes::MultiVariablesYesNo; + testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); + + // MultiVariablesThreeOptions proposal + proposal.type = QPI::ProposalTypes::MultiVariablesThreeOptions; + testProposalWithAllVoteDataOptionVotes(pwav, proposal, 3); + + // MultiVariablesFourOptions proposal + proposal.type = QPI::ProposalTypes::MultiVariablesFourOptions; + testProposalWithAllVoteDataOptionVotes(pwav, proposal, 4); + + // MultiVariables proposal with 8 options + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::MultiVariables, 8); + testProposalWithAllVoteDataOptionVotes(pwav, proposal, 8); + + // fail: test MultiVariables proposal with too many or too few options + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::MultiVariables, 1); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + EXPECT_FALSE(proposal.checkValidity()); + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::MultiVariables, 9); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + EXPECT_FALSE(proposal.checkValidity()); } TEST(TestCoreQPI, ProposalWithAllVoteDataWithScalarVoteSupport) @@ -491,6 +866,8 @@ TEST(TestCoreQPI, ProposalWithAllVoteDataWithoutScalarVoteSupport) TEST(TestCoreQPI, ProposalWithAllVoteDataYesNoProposals) { + // Using ProposalDataYesNo saves storage space by only supporting yes/no choices + // (or up to 3 options for proposal classes that don't store option values) ContractExecInitDeinitGuard initDeinitGuard; typedef QPI::ProposalDataYesNo ProposalT; QPI::ProposalWithAllVoteData pwav; @@ -500,7 +877,7 @@ TEST(TestCoreQPI, ProposalWithAllVoteDataYesNoProposals) proposal.type = QPI::ProposalTypes::YesNo; testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); - // ThreeOption proposal (accepted for general proposal only, because it does not cost anything) + // ThreeOption proposal (accepted for general proposal, because it does not cost anything) proposal.type = QPI::ProposalTypes::ThreeOptions; testProposalWithAllVoteDataOptionVotes(pwav, proposal, 3); @@ -555,6 +932,18 @@ TEST(TestCoreQPI, ProposalWithAllVoteDataYesNoProposals) // VariableScalarMean proposal proposal.type = QPI::ProposalTypes::VariableScalarMean; EXPECT_FALSE(proposal.checkValidity()); + + // MultiVariablesYesNo proposal + proposal.type = QPI::ProposalTypes::MultiVariablesYesNo; + testProposalWithAllVoteDataOptionVotes(pwav, proposal, 2); + + // MultiVariablesThreeOptions proposal (accepted for multiple variables proposal, because it does not cost anything) + proposal.type = QPI::ProposalTypes::MultiVariablesThreeOptions; + testProposalWithAllVoteDataOptionVotes(pwav, proposal, 3); + + // MultiVariablesFourOptions proposal + proposal.type = QPI::ProposalTypes::MultiVariablesFourOptions; + EXPECT_FALSE(proposal.checkValidity()); } template @@ -565,7 +954,7 @@ void expectNoVotes( ) { QPI::ProposalSingleVoteDataV1 vote; - for (QPI::uint32 i = 0; i < pv->maxVoters; ++i) + for (QPI::uint32 i = 0; i < pv->maxVotes; ++i) { EXPECT_TRUE(qpi(*pv).getVote(proposalIndex, i, vote)); EXPECT_EQ(vote.voteValue, QPI::NO_VOTE_VALUE); @@ -573,8 +962,9 @@ void expectNoVotes( QPI::ProposalSummarizedVotingDataV1 votingSummaryReturned; EXPECT_TRUE(qpi(*pv).getVotingSummary(proposalIndex, votingSummaryReturned)); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, 0); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 0); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); } template @@ -599,6 +989,11 @@ bool operator==(const QPI::ProposalSingleVoteDataV1& p1, const QPI::ProposalSing return memcmp(&p1, &p2, sizeof(p1)) == 0; } +bool operator==(const QPI::ProposalMultiVoteDataV1& p1, const QPI::ProposalMultiVoteDataV1& p2) +{ + return memcmp(&p1, &p2, sizeof(p1)) == 0; +} + template void setProposalWithSuccessCheck(const QPI::QpiContextProcedureCall& qpi, const ProposalVotingType& pv, const QPI::id& proposerId, const ProposalDataType& proposal) @@ -630,8 +1025,8 @@ void voteWithValidVoter( QPI::sint64 voteValue ) { - QPI::uint32 voterIdx = qpi(pv).voterIndex(voterId); - EXPECT_NE(voterIdx, QPI::INVALID_VOTER_INDEX); + QPI::uint32 voterIdx = qpi(pv).voteIndex(voterId); + EXPECT_NE(voterIdx, QPI::INVALID_VOTE_INDEX); QPI::id voterIdReturned = qpi(pv).voterId(voterIdx); EXPECT_EQ(voterIdReturned, voterId); @@ -663,6 +1058,109 @@ void voteWithValidVoter( } } +template +void voteWithValidVoterMultiVote( + const QPI::QpiContextProcedureCall& qpi, + ProposalVotingType& pv, + const QPI::id& voterId, + QPI::uint16 proposalIndex, + QPI::uint16 proposalType, + QPI::uint32 proposalTick, + QPI::sint64 voteValue, + QPI::uint32 voteCount = 0, // for first voteValue, 0 means all + QPI::sint64 voteValue2 = 0, + QPI::uint32 voteCount2 = 0, + QPI::sint64 voteValue3 = 0, + QPI::uint32 voteCount3 = 0 +) +{ + QPI::uint32 voterIdx = qpi(pv).voteIndex(voterId); + EXPECT_NE(voterIdx, QPI::INVALID_VOTE_INDEX); + QPI::id voterIdReturned = qpi(pv).voterId(voterIdx); + EXPECT_EQ(voterIdReturned, voterId); + + QPI::ProposalMultiVoteDataV1 voteReturnedBefore; + bool oldVoteAvailable = qpi(pv).getVotes(proposalIndex, voterId, voteReturnedBefore); + + // set all votes of voter (1 in computor voting) with multi-data voting function + QPI::ProposalMultiVoteDataV1 vote; + vote.proposalIndex = proposalIndex; + vote.proposalType = proposalType; + vote.proposalTick = proposalTick; + vote.voteValues.setAll(0); + vote.voteCounts.setAll(0); + vote.voteValues.set(0, voteValue); + if (voteCount != 0) + { + vote.voteCounts.set(0, voteCount); + vote.voteValues.set(1, voteValue2); + vote.voteCounts.set(1, voteCount2); + vote.voteValues.set(2, voteValue3); + vote.voteCounts.set(2, voteCount3); + } + EXPECT_EQ(qpi(pv).vote(voterId, vote), successExpected); + + QPI::ProposalMultiVoteDataV1 voteReturned; + if (successExpected) + { + EXPECT_TRUE(qpi(pv).getVotes(vote.proposalIndex, voterId, voteReturned)); + EXPECT_EQ((int)vote.proposalIndex, (int)voteReturned.proposalIndex); + EXPECT_EQ((int)vote.proposalType, (int)voteReturned.proposalType); + EXPECT_EQ(vote.proposalTick, voteReturned.proposalTick); + int totalVoteCount = qpi(pv).voteCount(voterIdx, proposalIndex); + if (voteCount == 0) + { + for (int i = 0; i < voteReturned.voteCounts.capacity(); ++i) + { + if (voteReturned.voteValues.get(i) == voteValue) + { + EXPECT_EQ(voteReturned.voteCounts.get(i), totalVoteCount); + totalVoteCount = 0; // for duplicates (e.g. value 0), expect count 0 + } + else + { + EXPECT_EQ(voteReturned.voteCounts.get(i), 0); + } + } + } + else + { + std::set voteInputIdx = { 0, 1, 2 }; + for (int i = 0; i < voteReturned.voteCounts.capacity(); ++i) + { + if (voteReturned.voteCounts.get(i) != 0) + { + bool found = false; + for (int j = 0; j < 4; ++j) + { + if (vote.voteValues.get(j) == voteReturned.voteValues.get(i)) + { + EXPECT_EQ(vote.voteCounts.get(j), voteReturned.voteCounts.get(i)); + EXPECT_TRUE(voteInputIdx.contains(j)); + voteInputIdx.erase(j); + found = true; + break; + } + } + EXPECT_TRUE(found); + } + } + EXPECT_TRUE(voteInputIdx.empty() || voteCount3 == 0); + } + + typename ProposalVotingType::ProposalDataType proposalReturned; + EXPECT_TRUE(qpi(pv).getProposal(vote.proposalIndex, proposalReturned)); + EXPECT_TRUE(proposalReturned.type == voteReturned.proposalType); + EXPECT_TRUE(proposalReturned.tick == voteReturned.proposalTick); + } + else if (oldVoteAvailable) + { + EXPECT_TRUE(qpi(pv).getVotes(vote.proposalIndex, voterId, voteReturned)); + EXPECT_TRUE(voteReturnedBefore == voteReturned); + } +} + + template void voteWithInvalidVoter( const QPI::QpiContextProcedureCall& qpi, @@ -680,6 +1178,15 @@ void voteWithInvalidVoter( vote.proposalTick = proposalTick; vote.voteValue = voteValue; EXPECT_FALSE(qpi(pv).vote(voterId, vote)); + + QPI::ProposalMultiVoteDataV1 vote2; + vote2.proposalIndex = proposalIndex; + vote2.proposalType = proposalType; + vote2.proposalTick = proposalTick; + vote2.voteValues.setAll(0); + vote2.voteValues.set(0, voteValue); + vote2.voteCounts.setAll(0); + EXPECT_FALSE(qpi(pv).vote(voterId, vote2)); } @@ -710,7 +1217,7 @@ int countFinishedProposals( } template -void testProposalVotingV1() +void testProposalVotingComputorsV1() { ContractExecInitDeinitGuard initDeinitGuard; @@ -738,9 +1245,17 @@ void testProposalVotingV1() QPI::ProposalSummarizedVotingDataV1 votingSummaryReturned; for (int i = 0; i < pv->maxProposals; ++i) { + proposalReturned.type = 42; // test that additional error indicator is set 0 EXPECT_FALSE(qpi(*pv).getProposal(i, proposalReturned)); + EXPECT_EQ((int)proposalReturned.type, 0); + + voteDataReturned.proposalType = 42; // test that additional error indicator is set 0 EXPECT_FALSE(qpi(*pv).getVote(i, 0, voteDataReturned)); + EXPECT_EQ((int)voteDataReturned.proposalType, 0); + + votingSummaryReturned.totalVotesAuthorized = 42; // test that additional error indicator is set 0 EXPECT_FALSE(qpi(*pv).getVotingSummary(i, votingSummaryReturned)); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, 0); } EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), -1); EXPECT_EQ(qpi(*pv).nextProposalIndex(0), -1); @@ -767,17 +1282,20 @@ void testProposalVotingV1() EXPECT_EQ(qpi(*pv).proposerId(1), QPI::NULL_ID); // okay: voters are available independently of proposals (= computors) - for (int i = 0; i < pv->maxVoters; ++i) + for (int i = 0; i < pv->maxVotes; ++i) { - EXPECT_EQ(qpi(*pv).voterIndex(qpi.computor(i)), i); + EXPECT_EQ(qpi(*pv).voteIndex(qpi.computor(i)), i); + EXPECT_EQ(qpi(*pv).voteCount(i), 1); EXPECT_EQ(qpi(*pv).voterId(i), qpi.computor(i)); } // fail: IDs / indices of non-voters - EXPECT_EQ(qpi(*pv).voterIndex(qpi.originator()), QPI::INVALID_VOTER_INDEX); - EXPECT_EQ(qpi(*pv).voterIndex(QPI::NULL_ID), QPI::INVALID_VOTER_INDEX); - EXPECT_EQ(qpi(*pv).voterId(pv->maxVoters), QPI::NULL_ID); - EXPECT_EQ(qpi(*pv).voterId(pv->maxVoters + 1), QPI::NULL_ID); + EXPECT_EQ(qpi(*pv).voteIndex(qpi.originator()), QPI::INVALID_VOTE_INDEX); + EXPECT_EQ(qpi(*pv).voteIndex(QPI::NULL_ID), QPI::INVALID_VOTE_INDEX); + EXPECT_EQ(qpi(*pv).voteCount(QPI::INVALID_VOTE_INDEX), 0); + EXPECT_EQ(qpi(*pv).voteCount(1000), 0); + EXPECT_EQ(qpi(*pv).voterId(pv->maxVotes), QPI::NULL_ID); + EXPECT_EQ(qpi(*pv).voterId(pv->maxVotes + 1), QPI::NULL_ID); // okay: set proposal for computor 0 QPI::ProposalDataV1 proposal; @@ -792,11 +1310,15 @@ void testProposalVotingV1() // fail: vote although no proposal is available at proposal index voteWithValidVoter(qpi, *pv, qpi.computor(0), 1, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 1, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 12345, QPI::ProposalTypes::YesNo, qpi.tick(), 0); voteWithValidVoter(qpi, *pv, qpi.computor(0), 12345, QPI::ProposalTypes::YesNo, qpi.tick(), 0); // fail: vote with wrong type + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::TransferYesNo, qpi.tick(), 0); voteWithValidVoter(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::TransferYesNo, qpi.tick(), 0); voteWithValidVoter(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::VariableScalarMean, qpi.tick(), 0); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::VariableScalarMean, qpi.tick(), 0); // fail: vote with non-computor voteWithInvalidVoter(qpi, *pv, qpi.originator(), 0, QPI::ProposalTypes::YesNo, qpi.tick(), 0); @@ -804,24 +1326,34 @@ void testProposalVotingV1() // fail: vote with invalid value voteWithValidVoter(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick(), 2); voteWithValidVoter(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick(), 2); // fail: vote with wrong tick + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick()-1, 0); voteWithValidVoter(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick()-1, 0); voteWithValidVoter(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick()+1, 0); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick()+1, 0); // okay: correct votes in proposalIndex 0 expectNoVotes(qpi, pv, 0); - for (int i = 0; i < pv->maxVoters; ++i) + for (int i = 0; i < pv->maxVotes; ++i) + { + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), 0, QPI::ProposalTypes::YesNo, qpi.tick(), i % 2); voteWithValidVoter(qpi, *pv, qpi.computor(i), 0, QPI::ProposalTypes::YesNo, qpi.tick(), i % 2); + } voteWithValidVoter(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 0, QPI::ProposalTypes::YesNo, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote EXPECT_TRUE(qpi(*pv).getVotingSummary(0, votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 0); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, pv->maxVoters - 1); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, pv->maxVotes - 1); EXPECT_EQ((int)votingSummaryReturned.optionCount, 2); - EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), pv->maxVoters / 2 - 1); - EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), pv->maxVoters / 2); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), pv->maxVotes / 2 - 1); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), pv->maxVotes / 2); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), 1); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), 1); if (proposalByComputorsOnly) { @@ -872,6 +1404,8 @@ void testProposalVotingV1() // fail: vote with invalid values (for yes/no only the values 0 and 1 are valid) voteWithValidVoter(qpi, *pv, qpi.computor(0), 1, proposal.type, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), 1, proposal.type, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(1), 1, proposal.type, qpi.tick(), 4); voteWithValidVoter(qpi, *pv, qpi.computor(1), 1, proposal.type, qpi.tick(), 4); // fail: vote with non-computor @@ -881,19 +1415,24 @@ void testProposalVotingV1() // okay: correct votes in proposalIndex 1 (first use) expectNoVotes(qpi, pv, 1); - for (int i = 0; i < pv->maxVoters; ++i) + for (int i = 0; i < pv->maxVotes; ++i) + { voteWithValidVoter(qpi, *pv, qpi.computor(i), 1, proposal.type, qpi.tick(), (i < 100) ? i % 4 : 3); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), 1, proposal.type, qpi.tick(), (i < 100) ? i % 4 : 3); + } EXPECT_TRUE(qpi(*pv).getVotingSummary(1, votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 1); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, pv->maxVoters); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, pv->maxVotes); EXPECT_EQ((int)votingSummaryReturned.optionCount, 4); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), 25); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), 25); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(2), 25); - EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(3), pv->maxVoters - 75); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(3), pv->maxVotes - 75); for (int i = 4; i < votingSummaryReturned.optionVoteCount.capacity(); ++i) EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(i), 0); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), 3); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), 3); // fail: proposal of transfer with wrong address proposal.type = QPI::ProposalTypes::TransferYesNo; @@ -935,20 +1474,29 @@ void testProposalVotingV1() QPI::uint16 secondProposalIdx = qpi(*pv).proposalIndex(secondProposer); voteWithValidVoter(qpi, *pv, qpi.computor(0), secondProposalIdx, proposal.type, qpi.tick(), -1); voteWithValidVoter(qpi, *pv, qpi.computor(1), secondProposalIdx, proposal.type, qpi.tick(), 2); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), secondProposalIdx, proposal.type, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(1), secondProposalIdx, proposal.type, qpi.tick(), 2); // okay: correct votes in proposalIndex 1 (reused) expectNoVotes(qpi, pv, 1); // checks that setProposal clears previous votes - for (int i = 0; i < pv->maxVoters; ++i) + for (int i = 0; i < pv->maxVotes; ++i) + { voteWithValidVoter(qpi, *pv, qpi.computor(i), secondProposalIdx, proposal.type, qpi.tick(), i % 2); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), secondProposalIdx, proposal.type, qpi.tick(), i % 2); + } + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(3), secondProposalIdx, proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(5), secondProposalIdx, proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote voteWithValidVoter(qpi, *pv, qpi.computor(3), secondProposalIdx, proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote voteWithValidVoter(qpi, *pv, qpi.computor(5), secondProposalIdx, proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote EXPECT_TRUE(qpi(*pv).getVotingSummary(1, votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 1); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, pv->maxVoters - 2); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, pv->maxVotes - 2); EXPECT_EQ((int)votingSummaryReturned.optionCount, 2); - EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), pv->maxVoters / 2); - EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), pv->maxVoters / 2 - 2); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), pv->maxVotes / 2); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), pv->maxVotes / 2 - 2); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), 0); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), 0); if (!supportScalarVotes) { @@ -989,17 +1537,25 @@ void testProposalVotingV1() // okay: votes in proposalIndex of computor 1 for testing overflow-avoiding summary algorithm for average expectNoVotes(qpi, pv, qpi(*pv).proposalIndex(qpi.computor(1))); for (int i = 0; i < 99; ++i) + { voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.maxSupportedValue - 2 + i % 3); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.maxSupportedValue - 2 + i % 3); + } EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(qpi.computor(1)), votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, (int)qpi(*pv).proposalIndex(qpi.computor(1))); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, 99); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 99); EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), -1); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.maxSupportedValue - 1); for (int i = 0; i < 555; ++i) + { + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.minSupportedValue + 10 - i % 5); voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(1)), proposal.type, qpi.tick(), proposal.variableScalar.minSupportedValue + 10 - i % 5); + } EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(qpi.computor(1)), votingSummaryReturned)); - EXPECT_EQ(votingSummaryReturned.totalVotes, 555); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 555); EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.minSupportedValue + 8); @@ -1017,28 +1573,41 @@ void testProposalVotingV1() // fail: vote with invalid values voteWithValidVoter(qpi, *pv, qpi.computor(0), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), -1001); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(0), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), -1001); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(1), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 1001); voteWithValidVoter(qpi, *pv, qpi.computor(1), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 1001); // okay: correct votes in proposalIndex of computor 10 expectNoVotes(qpi, pv, qpi(*pv).proposalIndex(qpi.computor(10))); for (int i = 0; i < 603; ++i) + { voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), (i % 201) - 100); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), (i % 201) - 100); + } EXPECT_TRUE(qpi(*pv).getVotingSummary(3, votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 3); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, 603); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 603); EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); EXPECT_EQ(votingSummaryReturned.scalarVotingResult, 0); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), -1); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); // another case for scalar voting summary for (int i = 0; i < 603; ++i) + { + voteWithValidVoterMultiVote (qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + } for (int i = 0; i < 200; ++i) + { voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), i + 1); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), i + 1); + } EXPECT_TRUE(qpi(*pv).getVotingSummary(3, votingSummaryReturned)); EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 3); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, 200); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 200); EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); EXPECT_EQ(votingSummaryReturned.scalarVotingResult, (200 * 201 / 2) / 200); } @@ -1087,11 +1656,11 @@ void testProposalVotingV1() for (int i = 0; i < 20; ++i) voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 0); for (int i = 20; i < 60; ++i) - voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 1); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 1); for (int i = 60; i < 160; ++i) voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 2); for (int i = 160; i < 360; ++i) - voteWithValidVoter(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 3); + voteWithValidVoterMultiVote(qpi, *pv, qpi.computor(i), qpi(*pv).proposalIndex(qpi.computor(10)), proposal.type, qpi.tick(), 3); // simulate epoch change ++system.epoch; @@ -1109,14 +1678,16 @@ void testProposalVotingV1() // okay: query voting summary of other epoch EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(qpi.computor(10)), votingSummaryReturned)); - EXPECT_EQ(votingSummaryReturned.authorizedVoters, pv->maxVoters); - EXPECT_EQ(votingSummaryReturned.totalVotes, 20+40+100+200); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 20+40+100+200); EXPECT_EQ((int)votingSummaryReturned.optionCount, 4); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), 20); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), 40); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(2), 100); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(3), 200); EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(4), 0); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), 3); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); // manually clear some proposals EXPECT_FALSE(qpi(*pv).clearProposal(qpi(*pv).proposalIndex(qpi.originator()))); @@ -1152,24 +1723,565 @@ void testProposalVotingV1() TEST(TestCoreQPI, ProposalVotingV1proposalOnlyByComputorWithScalarVoteSupport) { - testProposalVotingV1(); + testProposalVotingComputorsV1(); } TEST(TestCoreQPI, ProposalVotingV1proposalOnlyByComputorWithoutScalarVoteSupport) { - testProposalVotingV1(); + testProposalVotingComputorsV1(); } TEST(TestCoreQPI, ProposalVotingV1proposalByAnyoneWithScalarVoteSupport) { - testProposalVotingV1(); + testProposalVotingComputorsV1(); } TEST(TestCoreQPI, ProposalVotingV1proposalByAnyoneWithoutScalarVoteSupport) { - testProposalVotingV1(); + testProposalVotingComputorsV1(); } - // TODO: ProposalVoting YesNo +template +void testProposalVotingShareholdersV1() +{ + ContractTesting test; + test.initEmptyUniverse(); + + system.tick = 123456789; + system.epoch = 12345; + initComputors(0); + + typedef QPI::ProposalAndVotingByShareholders<6, MSVAULT_ASSET_NAME> ProposerAndVoterHandling; + + QpiContextUserProcedureCall qpi(0, QPI::id(1, 2, 3, 4), 123); + auto* pv = new QPI::ProposalVoting< + ProposerAndVoterHandling, + QPI::ProposalDataV1>; + + // Memory must be zeroed to work, which is done in contract states on init + QPI::setMemory(*pv, 0); + + std::vector> shareholderShares{ + {id(100, 20, 3, 4), 200}, + {id(0, 0, 0, 3), 100}, + {id(0, 0, 0, 2), 250}, + {id(0, 0, 0, 1), 50}, + {id(10, 20, 3, 4), 10}, + {id(10, 20, 2, 1), 54}, + {id(10, 20, 3, 1), 12}, + }; + issueContractShares(MSVAULT_CONTRACT_INDEX, shareholderShares); + sortContractShareVector(shareholderShares); + + // fail: get before proposals have been set + QPI::ProposalDataV1 proposalReturned; + QPI::ProposalMultiVoteDataV1 voteDataReturned; + QPI::ProposalSummarizedVotingDataV1 votingSummaryReturned; + for (int i = 0; i < pv->maxProposals; ++i) + { + proposalReturned.type = 42; // test that additional error indicator is set 0 + EXPECT_FALSE(qpi(*pv).getProposal(i, proposalReturned)); + EXPECT_EQ((int)proposalReturned.type, 0); + + voteDataReturned.proposalType = 42; // test that additional error indicator is set 0 + EXPECT_FALSE(qpi(*pv).getVotes(i, shareholderShares[0].first, voteDataReturned)); + EXPECT_EQ((int)voteDataReturned.proposalType, 0); + + votingSummaryReturned.totalVotesAuthorized = 42; // test that additional error indicator is set 0 + EXPECT_FALSE(qpi(*pv).getVotingSummary(i, votingSummaryReturned)); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, 0); + } + EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), -1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(0), -1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(123456), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(-1), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(0), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(123456), -1); + + // fail: get with invalid proposal index + EXPECT_FALSE(qpi(*pv).getProposal(pv->maxProposals, proposalReturned)); + EXPECT_FALSE(qpi(*pv).getProposal(pv->maxProposals + 1, proposalReturned)); + EXPECT_FALSE(qpi(*pv).getVotes(pv->maxProposals, shareholderShares[0].first, voteDataReturned)); + EXPECT_FALSE(qpi(*pv).getVotes(pv->maxProposals + 1, shareholderShares[0].first, voteDataReturned)); + EXPECT_FALSE(qpi(*pv).getVotingSummary(pv->maxProposals, votingSummaryReturned)); + EXPECT_FALSE(qpi(*pv).getVotingSummary(pv->maxProposals + 1, votingSummaryReturned)); + + // fail: no proposals for given IDs and invalid input + EXPECT_EQ((int)qpi(*pv).proposalIndex(QPI::NULL_ID), (int)QPI::INVALID_PROPOSAL_INDEX); // always equal + EXPECT_EQ((int)qpi(*pv).proposalIndex(qpi.originator()), (int)QPI::INVALID_PROPOSAL_INDEX); + EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[0].first), (int)QPI::INVALID_PROPOSAL_INDEX); + EXPECT_EQ(qpi(*pv).proposerId(QPI::INVALID_PROPOSAL_INDEX), QPI::NULL_ID); // always equal + EXPECT_EQ(qpi(*pv).proposerId(pv->maxProposals), QPI::NULL_ID); // always equal + EXPECT_EQ(qpi(*pv).proposerId(0), QPI::NULL_ID); + EXPECT_EQ(qpi(*pv).proposerId(1), QPI::NULL_ID); + + // fail: IDs / indices of non-voters + EXPECT_EQ(qpi(*pv).voteIndex(qpi.originator()), QPI::INVALID_VOTE_INDEX); + EXPECT_EQ(qpi(*pv).voteIndex(QPI::NULL_ID), QPI::INVALID_VOTE_INDEX); + EXPECT_EQ(qpi(*pv).voteCount(QPI::INVALID_VOTE_INDEX), 0); + EXPECT_EQ(qpi(*pv).voteCount(1000), 0); + EXPECT_EQ(qpi(*pv).voterId(pv->maxVotes), QPI::NULL_ID); + EXPECT_EQ(qpi(*pv).voterId(pv->maxVotes + 1), QPI::NULL_ID); + + // okay: set proposal for shareholder 0 + QPI::ProposalDataV1 proposal; + proposal.url.set(0, 0); + proposal.epoch = qpi.epoch(); + proposal.type = QPI::ProposalTypes::YesNo; + setProposalWithSuccessCheck(qpi, pv, shareholderShares[0].first, proposal); + EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[0].first), 0); + EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); + EXPECT_EQ(qpi(*pv).nextProposalIndex(0), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(-1), -1); + + // check that voters match shareholders + for (size_t i = 0; i < shareholderShares.size(); ++i) + { + uint32 voterIdx = qpi(*pv).voteIndex(shareholderShares[i].first); + EXPECT_NE(voterIdx, NO_VOTE_VALUE); + EXPECT_EQ(qpi(*pv).voteCount(voterIdx), shareholderShares[i].second); + EXPECT_EQ(qpi(*pv).voterId(voterIdx), shareholderShares[i].first); + } + + // fail: vote although no proposal is available at proposal index + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 1, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 1, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 12345, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 12345, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + + // fail: vote with wrong type + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::TransferYesNo, qpi.tick(), 0); + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::TransferYesNo, qpi.tick(), 0); + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::VariableScalarMean, qpi.tick(), 0); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::VariableScalarMean, qpi.tick(), 0); + + // fail: vote with non-computor + voteWithInvalidVoter(qpi, *pv, qpi.originator(), 0, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + voteWithInvalidVoter(qpi, *pv, QPI::NULL_ID, 0, QPI::ProposalTypes::YesNo, qpi.tick(), 0); + + // fail: vote with invalid value + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), 2); + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), 2); + + // fail: vote with wrong tick + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick() - 1, 0); + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick() - 1, 0); + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick() + 1, 0); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick() + 1, 0); + + // okay: correct votes in proposalIndex 0 + expectNoVotes(qpi, pv, 0); + int optionProposalVoteCounts[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + for (size_t i = 0; i < shareholderShares.size(); ++i) + { + if (i % 3 != 0) + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), i % 2); + else + voteWithValidVoter(qpi, *pv, shareholderShares[i].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), i % 2); + optionProposalVoteCounts[i % 2] += shareholderShares[i].second; + } + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 0, QPI::ProposalTypes::YesNo, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + EXPECT_TRUE(qpi(*pv).getVotingSummary(0, votingSummaryReturned)); + EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 0); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, pv->maxVotes - shareholderShares[0].second); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 2); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), optionProposalVoteCounts[0] - shareholderShares[0].second); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), optionProposalVoteCounts[1]); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), 1); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), 1); + + // fail: originator id(1,2,3,4) is no shareholder (see custom qpi.computor() above) + setProposalExpectFailure(qpi, pv, qpi.originator(), proposal); + + // fail: invalid type (more options than supported) + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::GeneralOptions, 9); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + + // fail: invalid type (less options than supported) + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::GeneralOptions, 0); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::GeneralOptions, 1); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + + // okay: set proposal for computor 2 (proposal index 1, first use) + QPI::id secondProposer = shareholderShares[2].first; + proposal.type = QPI::ProposalTypes::FourOptions; + proposal.epoch = 1; // non-zero means current epoch + setProposalWithSuccessCheck(qpi, pv, secondProposer, proposal); + EXPECT_EQ((int)qpi(*pv).proposalIndex(secondProposer), 1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); + EXPECT_EQ(qpi(*pv).nextProposalIndex(0), 1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(1), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(-1), -1); + + // fail: vote with invalid values (for yes/no only the values 0 and 1 are valid) + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, 1, proposal.type, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, 1, proposal.type, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[1].first, 1, proposal.type, qpi.tick(), 4); + voteWithValidVoter(qpi, *pv, shareholderShares[1].first, 1, proposal.type, qpi.tick(), 4); + + // fail: vote with non-shareholder + voteWithInvalidVoter(qpi, *pv, qpi.originator(), 1, proposal.type, qpi.tick(), 0); + voteWithInvalidVoter(qpi, *pv, QPI::NULL_ID, 1, proposal.type, qpi.tick(), 0); + + // okay: cast votes in proposalIndex 1 (first use) + expectNoVotes(qpi, pv, 1); + for (int i = 0; i < 8; ++i) + optionProposalVoteCounts[i] = 0; + for (size_t i = 0; i < shareholderShares.size(); ++i) + { + int voteValue = i % 4; + optionProposalVoteCounts[voteValue] += shareholderShares[i].second; + if (i & 1) + voteWithValidVoter(qpi, *pv, shareholderShares[i].first, 1, proposal.type, qpi.tick(), voteValue); + else + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, 1, proposal.type, qpi.tick(), voteValue); + } + EXPECT_TRUE(qpi(*pv).getVotingSummary(1, votingSummaryReturned)); + EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 1); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, pv->maxVotes); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 4); + for (int i = 0; i < 4; ++i) + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(i), optionProposalVoteCounts[i]); + for (int i = 4; i < votingSummaryReturned.optionVoteCount.capacity(); ++i) + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(i), 0); + + // fail: proposal of transfer with wrong address + proposal.type = QPI::ProposalTypes::TransferYesNo; + proposal.transfer.destination = QPI::NULL_ID; + proposal.transfer.amounts.setAll(0); + setProposalExpectFailure(qpi, pv, secondProposer, proposal); + // check that overwrite did not work + EXPECT_TRUE(qpi(*pv).getProposal(qpi(*pv).proposalIndex(secondProposer), proposalReturned)); + EXPECT_FALSE(isReturnedProposalAsExpected(qpi, proposalReturned, proposal)); + + // fail: proposal of transfer with too many or too few options + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::Transfer, 0); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + setProposalExpectFailure(qpi, pv, secondProposer, proposal); + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::Transfer, 1); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + setProposalExpectFailure(qpi, pv, secondProposer, proposal); + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::Transfer, 6); + EXPECT_FALSE(QPI::ProposalTypes::isValid(proposal.type)); + setProposalExpectFailure(qpi, pv, secondProposer, proposal); + + // fail: proposal of revenue distribution with invalid amount + proposal.type = QPI::ProposalTypes::TransferYesNo; + proposal.transfer.destination = qpi.originator(); + proposal.transfer.amounts.set(0, -123456); + setProposalExpectFailure(qpi, pv, secondProposer, proposal); + + // okay: revenue distribution, overwrite existing proposal of comp 2 (proposal index 1, reused) + proposal.transfer.destination = qpi.originator(); + proposal.transfer.amounts.set(0, 1005); + setProposalWithSuccessCheck(qpi, pv, secondProposer, proposal); + EXPECT_EQ((int)qpi(*pv).proposalIndex(secondProposer), 1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); + EXPECT_EQ(qpi(*pv).nextProposalIndex(0), 1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(1), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(-1), -1); + + // fail: vote with invalid values (for yes/no only the values 0 and 1 are valid) + QPI::uint16 secondProposalIdx = qpi(*pv).proposalIndex(secondProposer); + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, secondProposalIdx, proposal.type, qpi.tick(), -1); + voteWithValidVoter(qpi, *pv, shareholderShares[1].first, secondProposalIdx, proposal.type, qpi.tick(), 2); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, secondProposalIdx, proposal.type, qpi.tick(), -1); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[1].first, secondProposalIdx, proposal.type, qpi.tick(), 2); + + // okay: cast votes in proposalIndex 1 (reused) + expectNoVotes(qpi, pv, 1); // checks that setProposal clears previous votes + optionProposalVoteCounts[0] = 0; + optionProposalVoteCounts[1] = 0; + for (size_t i = 0; i < shareholderShares.size(); ++i) + { + int voteValue = i % 2; + optionProposalVoteCounts[voteValue] += shareholderShares[i].second; + if (i & 1) + voteWithValidVoter(qpi, *pv, shareholderShares[i].first, secondProposalIdx, proposal.type, qpi.tick(), voteValue); + else + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, secondProposalIdx, proposal.type, qpi.tick(), voteValue); + } + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[3].first, secondProposalIdx, proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + optionProposalVoteCounts[1] -= shareholderShares[3].second; + voteWithValidVoter(qpi, *pv, shareholderShares[5].first, secondProposalIdx, proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + optionProposalVoteCounts[1] -= shareholderShares[5].second; + EXPECT_TRUE(qpi(*pv).getVotingSummary(1, votingSummaryReturned)); + EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 1); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, optionProposalVoteCounts[0] + optionProposalVoteCounts[1]); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 2); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(0), optionProposalVoteCounts[0]); + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(1), optionProposalVoteCounts[1]); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), 0); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), 0); + + if (!supportScalarVotes) + { + // fail: scalar proposal not supported + proposal.type = QPI::ProposalTypes::VariableScalarMean; + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + } + else + { + // fail: scalar proposal with wrong min/max + proposal.type = QPI::ProposalTypes::VariableScalarMean; + proposal.variableScalar.proposedValue = 10; + proposal.variableScalar.minValue = 11; + proposal.variableScalar.maxValue = 20; + proposal.variableScalar.variable = 123; // not checked, full range usable + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + proposal.variableScalar.minValue = 0; + proposal.variableScalar.maxValue = 9; + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + + // fail: scalar proposal with full range is invalid, because NO_VOTE_VALUE is reserved for no vote + proposal.variableScalar.minValue = proposal.variableScalar.minSupportedValue - 1; + proposal.variableScalar.maxValue = proposal.variableScalar.maxSupportedValue; + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + + // okay: scalar proposal with nearly full range + proposal.variableScalar.minValue = proposal.variableScalar.minSupportedValue; + proposal.variableScalar.maxValue = proposal.variableScalar.maxSupportedValue; + setProposalWithSuccessCheck(qpi, pv, shareholderShares[1].first, proposal); + EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[1].first), 2); + EXPECT_EQ((int)qpi(*pv).proposalIndex(secondProposer), (int)secondProposalIdx); + EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); + EXPECT_EQ(qpi(*pv).nextProposalIndex(0), 1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(1), 2); + EXPECT_EQ(qpi(*pv).nextProposalIndex(2), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(-1), -1); + + // okay: votes in proposalIndex for testing overflow-avoiding summary algorithm for average + expectNoVotes(qpi, pv, qpi(*pv).proposalIndex(shareholderShares[1].first)); + for (int i = 0; i < 3; ++i) + { + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[1].first), + proposal.type, qpi.tick(), + proposal.variableScalar.maxSupportedValue - 2 + i % 3, 5, + proposal.variableScalar.maxSupportedValue - 2 + (i + 1) % 3, 3, + proposal.variableScalar.maxSupportedValue - 2 + (i + 2) % 3, 2); + } + EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(shareholderShares[1].first), votingSummaryReturned)); + EXPECT_EQ((int)votingSummaryReturned.proposalIndex, (int)qpi(*pv).proposalIndex(shareholderShares[1].first)); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 30); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.maxSupportedValue - 1); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), -1); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); + + for (int i = 0; i < 5; ++i) + { + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[1].first), + proposal.type, qpi.tick(), + proposal.variableScalar.minSupportedValue + 4 - i % 5, 4, + proposal.variableScalar.minSupportedValue + 12 - i % 5, 2); + } + EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(shareholderShares[1].first), votingSummaryReturned)); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 30); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, proposal.variableScalar.minSupportedValue + 2 + 3); + + // okay: scalar proposal with limited range + proposal.variableScalar.minValue = -1000; + proposal.variableScalar.maxValue = 1000; + setProposalWithSuccessCheck(qpi, pv, shareholderShares[5].first, proposal); + EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[5].first), 3); + EXPECT_EQ(qpi(*pv).nextProposalIndex(-1), 0); + EXPECT_EQ(qpi(*pv).nextProposalIndex(0), 1); + EXPECT_EQ(qpi(*pv).nextProposalIndex(1), 2); + EXPECT_EQ(qpi(*pv).nextProposalIndex(2), 3); + EXPECT_EQ(qpi(*pv).nextProposalIndex(3), -1); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(-1), -1); + + // fail: vote with invalid values + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, qpi(*pv).proposalIndex(shareholderShares[5].first), proposal.type, qpi.tick(), -1001); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[0].first, qpi(*pv).proposalIndex(shareholderShares[5].first), proposal.type, qpi.tick(), -1001); + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[1].first, qpi(*pv).proposalIndex(shareholderShares[5].first), proposal.type, qpi.tick(), 1001); + voteWithValidVoter(qpi, *pv, shareholderShares[1].first, qpi(*pv).proposalIndex(shareholderShares[5].first), proposal.type, qpi.tick(), 1001); + + // okay: cast votes in proposalIndex of shareholder 5 + expectNoVotes(qpi, pv, qpi(*pv).proposalIndex(shareholderShares[5].first)); + for (int i = 0; i < (int)shareholderShares.size(); ++i) + { + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[5].first), + proposal.type, qpi.tick(), + (i + 1) * 5, 6, + (i + 1) * -10, 3, + 0, shareholderShares[i].second - 9); + } + EXPECT_TRUE(qpi(*pv).getVotingSummary(3, votingSummaryReturned)); + EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 3); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, 676); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, 0); + + // another case for scalar voting summary + for (size_t i = 0; i < shareholderShares.size(); ++i) + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[5].first), proposal.type, qpi.tick(), QPI::NO_VOTE_VALUE); // remove vote + expectNoVotes(qpi, pv, qpi(*pv).proposalIndex(shareholderShares[5].first)); + for (size_t i = 0; i < shareholderShares.size(); ++i) + { + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[5].first), + proposal.type, qpi.tick(), + i * 3, 2, + i * 3 + 1, 3, + i * 3 + 2, 2); + } + EXPECT_TRUE(qpi(*pv).getVotingSummary(3, votingSummaryReturned)); + EXPECT_EQ((int)votingSummaryReturned.proposalIndex, 3); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, shareholderShares.size() * 7); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 0); + EXPECT_EQ(votingSummaryReturned.scalarVotingResult, (shareholderShares.size() / 2) * 3 + 1); + } + + // fail: test multi-option transfer proposal with invalid amounts + proposal.type = QPI::ProposalTypes::TransferThreeAmounts; + proposal.transfer.destination = qpi.originator(); + for (int i = 0; i < 4; ++i) + { + proposal.transfer.amounts.setAll(0); + proposal.transfer.amounts.set(i, -100 * i - 1); + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + } + proposal.transfer.amounts.set(0, 0); + proposal.transfer.amounts.set(1, 10); + proposal.transfer.amounts.set(2, 20); + proposal.transfer.amounts.set(3, 100); // for ProposalTypes::TransferThreeAmounts, fourth must be 0 + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + + // fail: duplicate options + proposal.transfer.amounts.setAll(0); + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + + // fail: options not sorted + for (int i = 0; i < 3; ++i) + proposal.transfer.amounts.set(i, 100 - i); + setProposalExpectFailure(qpi, pv, shareholderShares[1].first, proposal); + + // okay: fill proposal storage + proposal.transfer.amounts.setAll(0); + ASSERT_EQ((int)pv->maxProposals, (int)shareholderShares.size() - 1); + for (int i = 0; i < pv->maxProposals; ++i) + { + proposal.transfer.amounts.set(0, i); + proposal.transfer.amounts.set(1, i * 2 + 1); + proposal.transfer.amounts.set(2, i * 3 + 2); + setProposalWithSuccessCheck(qpi, pv, shareholderShares[i].first, proposal); + } + EXPECT_EQ(countActiveProposals(qpi, pv), (int)pv->maxProposals); + EXPECT_EQ(qpi(*pv).nextFinishedProposalIndex(-1), -1); + + // fail: no space left + setProposalExpectFailure(qpi, pv, shareholderShares[pv->maxProposals].first, proposal); + + // cast some votes before epoch change to test querying voting summary afterwards + for (int i = 0; i < 8; ++i) + optionProposalVoteCounts[i] = 0; + for (size_t i = 0; i < shareholderShares.size(); ++i) + { + voteWithValidVoterMultiVote(qpi, *pv, shareholderShares[i].first, qpi(*pv).proposalIndex(shareholderShares[5].first), proposal.type, qpi.tick(), + i & 1, shareholderShares[i].second / 2, + 2, shareholderShares[i].second / 8, + 3, shareholderShares[i].second / 4); + optionProposalVoteCounts[i & 1] += shareholderShares[i].second / 2; + optionProposalVoteCounts[2] += shareholderShares[i].second / 8; + optionProposalVoteCounts[3] += shareholderShares[i].second / 4; + } + + // simulate epoch change + ++system.epoch; + EXPECT_EQ(countActiveProposals(qpi, pv), 0); + EXPECT_EQ(countFinishedProposals(qpi, pv), (int)pv->maxProposals); + + // okay: same setProposal after epoch change, because the oldest proposal will be deleted + proposal.epoch = qpi.epoch(); + setProposalWithSuccessCheck(qpi, pv, shareholderShares[pv->maxProposals].first, proposal); + EXPECT_EQ(countActiveProposals(qpi, pv), 1); + EXPECT_EQ(countFinishedProposals(qpi, pv), (int)pv->maxProposals - 1); + + // fail: vote in wrong epoch + voteWithValidVoter(qpi, *pv, shareholderShares[0].first, qpi(*pv).proposalIndex(shareholderShares[5].first), proposal.type, qpi.tick(), 0); + + // okay: query voting summary of other epoch + EXPECT_TRUE(qpi(*pv).getVotingSummary(qpi(*pv).proposalIndex(shareholderShares[5].first), votingSummaryReturned)); + EXPECT_EQ(votingSummaryReturned.totalVotesAuthorized, pv->maxVotes); + uint32 voteCountSum = 0; + for (int i = 0; i < 4; ++i) + voteCountSum += optionProposalVoteCounts[i]; + EXPECT_EQ(votingSummaryReturned.totalVotesCasted, voteCountSum); + EXPECT_EQ((int)votingSummaryReturned.optionCount, 4); + for (int i = 0; i < 8; ++i) + EXPECT_EQ(votingSummaryReturned.optionVoteCount.get(i), (i < 4) ? optionProposalVoteCounts[i] : 0); + EXPECT_EQ(votingSummaryReturned.getMostVotedOption(), 0); + EXPECT_EQ(votingSummaryReturned.getAcceptedOption(), -1); + + // manually clear some proposals + EXPECT_FALSE(qpi(*pv).clearProposal(qpi(*pv).proposalIndex(qpi.originator()))); + EXPECT_TRUE(qpi(*pv).clearProposal(qpi(*pv).proposalIndex(shareholderShares[5].first))); + EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[5].first), (int)QPI::INVALID_PROPOSAL_INDEX); + EXPECT_EQ(countActiveProposals(qpi, pv), 1); + EXPECT_EQ(countFinishedProposals(qpi, pv), (int)pv->maxProposals - 2); + proposal.epoch = 0; + setProposalExpectFailure(qpi, pv, qpi.originator(), proposal); + EXPECT_NE((int)qpi(*pv).setProposal(shareholderShares[3].first, proposal), (int)QPI::INVALID_PROPOSAL_INDEX); // success (clear by epoch 0) + EXPECT_EQ((int)qpi(*pv).proposalIndex(shareholderShares[3].first), (int)QPI::INVALID_PROPOSAL_INDEX); + EXPECT_EQ(countActiveProposals(qpi, pv), 1); + EXPECT_EQ(countFinishedProposals(qpi, pv), (int)pv->maxProposals - 3); + + // simulate epoch change + ++system.epoch; + EXPECT_EQ(countActiveProposals(qpi, pv), 0); + EXPECT_EQ(countFinishedProposals(qpi, pv), (int)pv->maxProposals - 2); + + // change of shareholders + AssetPossessionIterator iter({ NULL_ID, MSVAULT_ASSET_NAME }); + int newPossessorCount = 0; + while (!iter.reachedEnd()) + { + if (iter.numberOfPossessedShares()) + { + int destinationOwnershipIndex, destinationPossessionIndex; + EXPECT_TRUE(transferShareOwnershipAndPossession(iter.ownershipIndex(), iter.possessionIndex(), qpi.computor(newPossessorCount), + iter.numberOfPossessedShares(), &destinationOwnershipIndex, &destinationPossessionIndex, true)); + ++newPossessorCount; + } + iter.next(); + } + EXPECT_GE(newPossessorCount, (int)pv->maxProposals); + + // set new proposals by new shareholders + proposal.epoch = qpi.epoch(); + proposal.type = QPI::ProposalTypes::type(QPI::ProposalTypes::Class::GeneralOptions, 6); + for (int i = 0; i < pv->maxProposals; ++i) + setProposalWithSuccessCheck(qpi, pv, qpi.computor(i), proposal); + for (int i = 0; i < pv->maxProposals; ++i) + expectNoVotes(qpi, pv, i); + EXPECT_EQ(countActiveProposals(qpi, pv), (int)pv->maxProposals); + EXPECT_EQ(countFinishedProposals(qpi, pv), 0); + + delete pv; +} + +TEST(TestCoreQPI, ProposalVotingV1shareholderWithScalarVoteSupport) +{ + testProposalVotingShareholdersV1(); +} + +TEST(TestCoreQPI, ProposalVotingV1shareholderWithoutScalarVoteSupport) +{ + testProposalVotingShareholdersV1(); +} diff --git a/test/qpi_collection.cpp b/test/qpi_collection.cpp index d88bf1242..b2b9a6ce5 100644 --- a/test/qpi_collection.cpp +++ b/test/qpi_collection.cpp @@ -2,20 +2,9 @@ #include "gtest/gtest.h" -static void* __scratchpadBuffer = nullptr; -static void* __scratchpad() -{ - return __scratchpadBuffer; -} -namespace QPI -{ - struct QpiContextProcedureCall; - struct QpiContextFunctionCall; -} -typedef void (*USER_FUNCTION)(const QPI::QpiContextFunctionCall&, void* state, void* input, void* output, void* locals); -typedef void (*USER_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); - +#include "../src/contract_core/pre_qpi_def.h" #include "../src/contracts/qpi.h" +#include "../src/common_buffers.h" #include "../src/contract_core/qpi_collection_impl.h" #include "../src/contract_core/qpi_trivial_impl.h" @@ -1522,7 +1511,7 @@ void testCollectionPseudoRandom(int povs, int seed, bool povCollisions, int clea TEST(TestCoreQPI, CollectionInsertRemoveCleanupRandom) { - __scratchpadBuffer = new char[10 * 1024 * 1024]; + reorgBuffer = new char[10 * 1024 * 1024]; constexpr unsigned int numCleanups = 30; for (int i = 0; i < 10; ++i) { @@ -1540,21 +1529,21 @@ TEST(TestCoreQPI, CollectionInsertRemoveCleanupRandom) testCollectionPseudoRandom<16>(10, 12 + i, povCollisions, numCleanups, 55, 45); testCollectionPseudoRandom<4>(4, 42 + i, povCollisions, numCleanups, 52, 48); } - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(TestCoreQPI, CollectionCleanupWithPovCollisions) { // Shows bugs in cleanup() that occur in case of massive pov hash map collisions and in case of capacity < 32 - __scratchpadBuffer = new char[10 * 1024 * 1024]; + reorgBuffer = new char[10 * 1024 * 1024]; bool cleanupAfterEachRemove = true; testCollectionMultiPovOneElement<16>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<32>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<64>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<128>(cleanupAfterEachRemove); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } @@ -1673,7 +1662,7 @@ QPI::uint64 testCollectionPerformance( TEST(TestCoreQPI, CollectionPerformance) { - __scratchpadBuffer = new char[16 * 1024 * 1024]; + reorgBuffer = new char[16 * 1024 * 1024]; std::vector durations; std::vector descriptions; @@ -1702,8 +1691,8 @@ TEST(TestCoreQPI, CollectionPerformance) durations.push_back(testCollectionPerformance<512>(16, 333)); descriptions.push_back("[CollectionPerformance] Collection<512>(16, 333)"); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; bool verbose = true; if (verbose) diff --git a/test/qpi_date_time.cpp b/test/qpi_date_time.cpp index 029458ca4..654d02bd4 100644 --- a/test/qpi_date_time.cpp +++ b/test/qpi_date_time.cpp @@ -8,14 +8,205 @@ #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" +#include "contract_core/qpi_spectrum_impl.h" #include "../src/contract_core/qpi_ticking_impl.h" -TEST(DateAndTimeTest, Equality) { - DateAndTime d1 = { 500, 30, 15, 8, 3, 3, 24 }; - DateAndTime d2 = { 500, 30, 15, 8, 3, 3, 24 }; - DateAndTime d3 = { 501, 30, 15, 8, 3, 3, 24 }; // Different millisecond - DateAndTime d4 = { 500, 30, 15, 8, 3, 3, 25 }; // Different year +#include + +::std::ostream& operator<<(::std::ostream& os, const DateAndTime& dt) +{ + std::ios_base::fmtflags f(os.flags()); + os << std::setfill('0') << dt.getYear() << "-" + << std::setw(2) << (int)dt.getMonth() << "-" + << std::setw(2) << (int)dt.getDay() << " " + << std::setw(2) << (int)dt.getHour() << ":" + << std::setw(2) << (int)dt.getMinute() << ":" + << std::setw(2) << (int)dt.getSecond() << "." + << std::setw(3) << dt.getMillisec() << "'" + << std::setw(3) << dt.getMicrosecDuringMillisec(); + os.flags(f); + return os; +} + +TEST(DateAndTimeTest, IsLeapYear) +{ + EXPECT_TRUE(DateAndTime::isLeapYear(1600)); + EXPECT_FALSE(DateAndTime::isLeapYear(1700)); + EXPECT_FALSE(DateAndTime::isLeapYear(1800)); + EXPECT_FALSE(DateAndTime::isLeapYear(1900)); + EXPECT_TRUE(DateAndTime::isLeapYear(2000)); + EXPECT_FALSE(DateAndTime::isLeapYear(2019)); + EXPECT_TRUE(DateAndTime::isLeapYear(2020)); + EXPECT_FALSE(DateAndTime::isLeapYear(2021)); + EXPECT_FALSE(DateAndTime::isLeapYear(2022)); + EXPECT_FALSE(DateAndTime::isLeapYear(2023)); + EXPECT_TRUE(DateAndTime::isLeapYear(2024)); + EXPECT_FALSE(DateAndTime::isLeapYear(2025)); + EXPECT_FALSE(DateAndTime::isLeapYear(2026)); + EXPECT_FALSE(DateAndTime::isLeapYear(2027)); + EXPECT_TRUE(DateAndTime::isLeapYear(2028)); + EXPECT_FALSE(DateAndTime::isLeapYear(2029)); + EXPECT_FALSE(DateAndTime::isLeapYear(2030)); + EXPECT_FALSE(DateAndTime::isLeapYear(2031)); + EXPECT_TRUE(DateAndTime::isLeapYear(2032)); + EXPECT_FALSE(DateAndTime::isLeapYear(2100)); + EXPECT_FALSE(DateAndTime::isLeapYear(2200)); + EXPECT_TRUE(DateAndTime::isLeapYear(2400)); + EXPECT_FALSE(DateAndTime::isLeapYear(2500)); + EXPECT_TRUE(DateAndTime::isLeapYear(2800)); + EXPECT_TRUE(DateAndTime::isLeapYear(3200)); +} + +TEST(DateAndTimeTest, IsValid) +{ + // Checking year + EXPECT_TRUE(DateAndTime::isValid(0, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2100, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(65535, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(65536, 1, 1, 0, 0, 0, 0, 0)); + + // Checking month + EXPECT_FALSE(DateAndTime::isValid(2025, 0, 1, 0, 0, 0, 0, 0)); + for (int i = 1; i <= 12; ++i) + EXPECT_TRUE(DateAndTime::isValid(2025, i, 1, 0, 0, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 13, 1, 0, 0, 0, 0, 0)); + + // Checking day range + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 0, 0, 0, 0, 0, 0)); + for (int i = 1; i <= 31; ++i) + EXPECT_TRUE(DateAndTime::isValid(2025, 1, i, 0, 0, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 32, 0, 0, 0, 0, 0)); + + // Checking last day of month (except Feb) + int daysPerMonth[] = { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + for (int i = 0; i < 12; ++i) + { + if (daysPerMonth[i]) + { + EXPECT_TRUE(DateAndTime::isValid(2025, i + 1, daysPerMonth[i], 0, 0, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, i + 1, daysPerMonth[i] + 1, 0, 0, 0, 0, 0)); + } + } + + // Checking last day of February + for (int year = 1582; year < 3000; ++year) + { + int daysInFeb = (DateAndTime::isLeapYear(year)) ? 29 : 28; + EXPECT_TRUE(DateAndTime::isValid(year, 2, daysInFeb, 0, 0, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(year, 2, daysInFeb + 1, 0, 0, 0, 0, 0)); + } + + // Checking hour + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 3, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 14, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 23, 0, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 24, 0, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 25, 0, 0, 0, 0)); + + // Checking minute + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 49, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 59, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 60, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 61, 0, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 101, 0, 0, 0)); + + // Checking second + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 49, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 59, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 0, 60, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 0, 61, 0, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 0, 101, 0, 0)); + + // Checking millisec + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 999, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 1000, 0)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 1002, 0)); + + // Checking microsec + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 999)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 1000)); + EXPECT_FALSE(DateAndTime::isValid(2025, 1, 1, 0, 0, 0, 0, 1002)); +} + +TEST(DateAndTimeTest, SetAndGet) +{ + // Default constructor (invalid value) + DateAndTime d1; + EXPECT_FALSE(d1.isValid()); + EXPECT_EQ((int)d1.getYear(), 0); + EXPECT_EQ(d1.getMonth(), 0); + EXPECT_EQ(d1.getDay(), 0); + EXPECT_EQ(d1.getHour(), 0); + EXPECT_EQ(d1.getMinute(), 0); + EXPECT_EQ(d1.getSecond(), 0); + EXPECT_EQ((int)d1.getMillisec(), 0); + EXPECT_EQ((int)d1.getMicrosecDuringMillisec(), 0); + + // Set if valid + EXPECT_FALSE(d1.setIfValid(2025, 0, 0, 0, 0, 0)); + EXPECT_TRUE(d1.setIfValid(2025, 1, 2, 3, 4, 5, 6, 7)); + EXPECT_EQ((int)d1.getYear(), 2025); + EXPECT_EQ(d1.getMonth(), 1); + EXPECT_EQ(d1.getDay(), 2); + EXPECT_EQ(d1.getHour(), 3); + EXPECT_EQ(d1.getMinute(), 4); + EXPECT_EQ(d1.getSecond(), 5); + EXPECT_EQ((int)d1.getMillisec(), 6); + EXPECT_EQ((int)d1.getMicrosecDuringMillisec(), 7); + + // Copy constructor + DateAndTime d2(d1); + EXPECT_EQ((int)d2.getYear(), 2025); + EXPECT_EQ(d2.getMonth(), 1); + EXPECT_EQ(d2.getDay(), 2); + EXPECT_EQ(d2.getHour(), 3); + EXPECT_EQ(d2.getMinute(), 4); + EXPECT_EQ(d2.getSecond(), 5); + EXPECT_EQ((int)d2.getMillisec(), 6); + EXPECT_EQ((int)d2.getMicrosecDuringMillisec(), 7); + + // Set edge case values (invalid as date but good for checking if + // bit-level processing works) + d2.set(65535, 15, 31, 31, 63, 63, 1023, 1023); + EXPECT_FALSE(d2.isValid()); + EXPECT_EQ((int)d2.getYear(), 65535); + EXPECT_EQ(d2.getMonth(), 15); + EXPECT_EQ(d2.getDay(), 31); + EXPECT_EQ(d2.getHour(), 31); + EXPECT_EQ(d2.getMinute(), 63); + EXPECT_EQ(d2.getSecond(), 63); + EXPECT_EQ((int)d2.getMillisec(), 1023); + EXPECT_EQ((int)d2.getMicrosecDuringMillisec(), 1023); + + // Operator = + EXPECT_NE(d1, d2); + d2 = d1; + EXPECT_EQ(d1, d2); + + // Test setTime() and setDate(), which runs without checking validity + EXPECT_EQ(d1, DateAndTime(2025, 1, 2, 3, 4, 5, 6, 7)); + d1.setDate(65535, 15, 31); + EXPECT_EQ(d1, DateAndTime(65535, 15, 31, 3, 4, 5, 6, 7)); + d1.setTime(31, 63, 63, 1023, 1023); + EXPECT_EQ(d1, DateAndTime(65535, 15, 31, 31, 63, 63, 1023, 1023)); + d1.setDate(2030, 7, 10); + EXPECT_EQ(d1, DateAndTime(2030, 7, 10, 31, 63, 63, 1023, 1023)); + d1.setTime(20, 15, 16, 457, 738); + EXPECT_EQ(d1, DateAndTime(2030, 7, 10, 20, 15, 16, 457, 738)); +} + +TEST(DateAndTimeTest, Equality) +{ + DateAndTime d1{ 2024, 3, 3, 8, 15, 30, 500 }; // 2024-03-03 8:15:30.500 + DateAndTime d2{ 2024, 3, 3, 8, 15, 30, 500 }; + DateAndTime d3{ 2024, 3, 3, 8, 15, 30, 501 }; // Different millisecond + DateAndTime d4{ 2025, 3, 3, 8, 15, 30, 500 }; // Different year EXPECT_EQ(d1, d2); EXPECT_EQ(d1, d1); @@ -23,54 +214,376 @@ TEST(DateAndTimeTest, Equality) { EXPECT_NE(d1, d4); } -TEST(DateAndTimeTest, Comparison) { - DateAndTime base = { 10, 1, 1, 1, 1, 1, 25 }; // Jan 1, 2025 +TEST(DateAndTimeTest, Comparison) +{ + DateAndTime sorted[] = { + { 2024, 6, 1, 0, 0, 0 }, // June 1, 2024, 00:00:00.0 + { 2025, 5, 1, 0, 0, 0 }, // May 1, 2025, 00:00:00.0 + { 2025, 5, 29, 12, 0, 0 }, + { 2025, 6, 1, 10, 0, 0 }, + { 2025, 6, 1, 10, 10, 0 }, + { 2025, 6, 1, 10, 10, 10 }, + { 2025, 6, 1, 12, 0, 0 }, // June 1, 2025, 12:00:00.0 + { 2025, 6, 1, 12, 0, 0, 0, 999 }, // June 1, 2025, 12:00:00.000999 + { 2025, 6, 1, 12, 0, 0, 1, 0 }, // June 1, 2025, 12:00:00.001000 + { 2025, 6, 1, 12, 0, 1 }, // June 1, 2025, 12:00:01.0 + { 2025, 6, 1, 12, 1, 0 }, // June 1, 2025, 12:01:00.0 + { 2025, 6, 1, 13, 0, 0 }, // June 1, 2025, 13:01:00.0 + { 2025, 6, 2, 10, 0, 0 }, // June 2, 2025, 10:00:00.0 + { 2025, 7, 1, 10, 0, 0 }, // July 1, 2025, 10:00:00.0 + { 2026, 6, 1, 10, 0, 0 }, // June 1, 2026, 10:00:00.0 + }; + const int count = sizeof(sorted) / sizeof(sorted[0]); - EXPECT_LT((DateAndTime{ 10, 1, 1, 1, 1, 1, 24 }), base); - EXPECT_GT(base, (DateAndTime{ 10, 1, 1, 1, 1, 1, 24 })); - - EXPECT_FALSE(base < base); - EXPECT_FALSE(base > base); - EXPECT_TRUE(base == base); + for (int i = 0; i < count; ++i) + { + for (int j = i; j < count; ++j) + { + if (i == j) + { + EXPECT_EQ(sorted[i], sorted[j]); + } + else + { + EXPECT_LT(sorted[i], sorted[j]); + EXPECT_GT(sorted[j], sorted[i]); + } + } + } } -TEST(DateAndTimeTest, Subtraction) { - DateAndTime base = { 0, 0, 0, 8, 15, 3, 25 }; // Mar 15, 2025, 08:00:00.000 +TEST(DateAndTimeTest, Addition) +{ + // changing date only (using day count) + DateAndTime d1{ 2025, 1, 1 }; + EXPECT_TRUE(d1.add(0, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1)); + EXPECT_TRUE(d1.add(0, 0, 10)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 11)); + EXPECT_TRUE(d1.add(0, 0, -6)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 5)); + EXPECT_TRUE(d1.add(0, 0, 26)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 31)); + EXPECT_TRUE(d1.add(0, 0, 15)); + EXPECT_EQ(d1, DateAndTime(2025, 2, 15)); + EXPECT_TRUE(d1.add(0, 0, 15)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 2)); + EXPECT_TRUE(d1.add(0, 0, -2)); + EXPECT_EQ(d1, DateAndTime(2025, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, -13)); + EXPECT_EQ(d1, DateAndTime(2025, 2, 15)); + EXPECT_TRUE(d1.add(0, 0, -20)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 26)); + EXPECT_TRUE(d1.add(0, 0, -27)); + EXPECT_EQ(d1, DateAndTime(2024, 12, 30)); + EXPECT_TRUE(d1.add(0, 0, 31)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 30)); + EXPECT_TRUE(d1.add(0, 0, 31)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 2)); + EXPECT_TRUE(d1.add(0, 0, -31)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 30)); + EXPECT_TRUE(d1.add(0, 0, 52)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 23)); + EXPECT_TRUE(d1.add(0, 0, 70)); + EXPECT_EQ(d1, DateAndTime(2025, 6, 1)); + EXPECT_TRUE(d1.add(0, 0, -122)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 30)); + EXPECT_TRUE(d1.add(0, 0, 132)); + EXPECT_EQ(d1, DateAndTime(2025, 6, 11)); + EXPECT_TRUE(d1.add(0, 0, -162)); + EXPECT_EQ(d1, DateAndTime(2024, 12, 31)); + EXPECT_TRUE(d1.add(0, 0, -35)); + EXPECT_EQ(d1, DateAndTime(2024, 11, 26)); + EXPECT_TRUE(d1.add(0, 0, -25)); + EXPECT_EQ(d1, DateAndTime(2024, 11, 1)); + EXPECT_TRUE(d1.add(0, 0, 60)); + EXPECT_EQ(d1, DateAndTime(2024, 12, 31)); + EXPECT_TRUE(d1.add(0, 0, -140)); + EXPECT_EQ(d1, DateAndTime(2024, 8, 13)); + EXPECT_TRUE(d1.add(0, 0, -49)); + EXPECT_EQ(d1, DateAndTime(2024, 6, 25)); + EXPECT_TRUE(d1.add(0, 0, 190)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1)); + EXPECT_TRUE(d1.add(0, 0, -200)); + EXPECT_EQ(d1, DateAndTime(2024, 6, 15)); + EXPECT_TRUE(d1.add(0, 0, 220)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 21)); + EXPECT_TRUE(d1.add(0, 0, -220)); + EXPECT_EQ(d1, DateAndTime(2024, 6, 15)); + EXPECT_TRUE(d1.add(0, 0, -21)); + EXPECT_EQ(d1, DateAndTime(2024, 5, 25)); + EXPECT_TRUE(d1.add(0, 0, -50)); + EXPECT_EQ(d1, DateAndTime(2024, 4, 5)); + EXPECT_TRUE(d1.add(0, 0, 70)); + EXPECT_EQ(d1, DateAndTime(2024, 6, 14)); + EXPECT_TRUE(d1.add(0, 0, -80)); + EXPECT_EQ(d1, DateAndTime(2024, 3, 26)); + EXPECT_TRUE(d1.add(0, 0, -26)); + EXPECT_EQ(d1, DateAndTime(2024, 2, 29)); + EXPECT_TRUE(d1.add(0, 0, 1)); + EXPECT_EQ(d1, DateAndTime(2024, 3, 1)); + EXPECT_TRUE(d1.add(0, 0, -2)); + EXPECT_EQ(d1, DateAndTime(2024, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, 366)); + EXPECT_EQ(d1, DateAndTime(2025, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, -366)); + EXPECT_EQ(d1, DateAndTime(2024, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, 366 + 365)); + EXPECT_EQ(d1, DateAndTime(2026, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, 1)); + EXPECT_EQ(d1, DateAndTime(2026, 3, 1)); + EXPECT_TRUE(d1.add(0, 0, -1)); + EXPECT_EQ(d1, DateAndTime(2026, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, -365 - 366 - 365)); + EXPECT_EQ(d1, DateAndTime(2023, 2, 28)); + d1.setDate(2025, 1, 31); + EXPECT_TRUE(d1.add(0, 0, 31)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 3)); + d1.setDate(2025, 12, 31); + EXPECT_TRUE(d1.add(0, 0, 31)); + EXPECT_EQ(d1, DateAndTime(2026, 1, 31)); + d1.setDate(2025, 3, 31); + EXPECT_TRUE(d1.add(0, 0, -31)); + EXPECT_EQ(d1, DateAndTime(2025, 2, 28)); + d1.setDate(2024, 2, 28); + EXPECT_TRUE(d1.add(0, 0, 366)); + EXPECT_EQ(d1, DateAndTime(2025, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, -366)); + EXPECT_EQ(d1, DateAndTime(2024, 2, 28)); + d1.setDate(2024, 2, 29); + EXPECT_TRUE(d1.add(0, 0, 365)); + EXPECT_EQ(d1, DateAndTime(2025, 2, 28)); + EXPECT_TRUE(d1.add(0, 0, -365)); + EXPECT_EQ(d1, DateAndTime(2024, 2, 29)); + d1.setDate(2024, 2, 29); + EXPECT_TRUE(d1.add(0, 0, 366)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 1)); + EXPECT_TRUE(d1.add(0, 0, -366)); + EXPECT_EQ(d1, DateAndTime(2024, 2, 29)); + d1.setDate(2024, 3, 1); + EXPECT_TRUE(d1.add(0, 0, 365)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 1)); + EXPECT_TRUE(d1.add(0, 0, -365)); + EXPECT_EQ(d1, DateAndTime(2024, 3, 1)); + d1.setDate(2024, 3, 1); + EXPECT_TRUE(d1.add(0, 0, 366)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 2)); + EXPECT_TRUE(d1.add(0, 0, -366)); + EXPECT_EQ(d1, DateAndTime(2024, 3, 1)); + + // changing date only using months count + d1.setDate(2025, 10, 31); + EXPECT_TRUE(d1.add(0, 1, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 12, 1)); + EXPECT_TRUE(d1.add(0, -1, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 11, 1)); + EXPECT_TRUE(d1.add(0, 5, 0)); + EXPECT_EQ(d1, DateAndTime(2026, 4, 1)); + EXPECT_TRUE(d1.add(0, -6, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 10, 1)); + EXPECT_TRUE(d1.add(0, 9, 0)); + EXPECT_EQ(d1, DateAndTime(2026, 7, 1)); + EXPECT_TRUE(d1.add(0, -12, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 7, 1)); + EXPECT_TRUE(d1.add(0, 12, 0)); + EXPECT_EQ(d1, DateAndTime(2026, 7, 1)); + EXPECT_TRUE(d1.add(0, -18, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1)); + EXPECT_TRUE(d1.add(0, 18, 0)); + EXPECT_EQ(d1, DateAndTime(2026, 7, 1)); + EXPECT_TRUE(d1.add(0, -24, 0)); + EXPECT_EQ(d1, DateAndTime(2024, 7, 1)); + EXPECT_TRUE(d1.add(0, 36, 0)); + EXPECT_EQ(d1, DateAndTime(2027, 7, 1)); + d1.setDate(2024, 2, 29); + EXPECT_TRUE(d1.add(0, -12, 0)); + EXPECT_EQ(d1, DateAndTime(2023, 3, 1)); + d1.setDate(2024, 2, 29); + EXPECT_TRUE(d1.add(0, 12, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 1)); - EXPECT_EQ(base - base, 0); - EXPECT_EQ((DateAndTime{ 0, 0, 0, 8, 16, 3, 25 }) - base, 86400000LL); + // changing date only using year count + d1.setDate(2025, 10, 31); + EXPECT_TRUE(d1.add(100, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2125, 10, 31)); + EXPECT_TRUE(d1.add(-200, 0, 0)); + EXPECT_EQ(d1, DateAndTime(1925, 10, 31)); + d1.setDate(2024, 2, 29); + EXPECT_TRUE(d1.add(1, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2025, 3, 1)); + d1.setDate(2024, 2, 29); + EXPECT_TRUE(d1.add(-3, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2021, 3, 1)); + + // change time and date + d1 = DateAndTime{2025, 1, 1, 12, 0, 0, 0, 0}; + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 1)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 0, 1)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 3500)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 3, 501)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, -1)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 3, 500)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, -2500)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 1, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, -1)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 0, 999)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 2)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 1, 1)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, -10)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 0, 991)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, -1000)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 11, 59, 59, 999, 991)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 1009)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 0, 0, 1, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, -1001000)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 11, 59, 59, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 61 * 1000000)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 12, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 60 * 60 * 1000000ll)); + EXPECT_EQ(d1, DateAndTime(2025, 1, 1, 13, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, -14 * 60 * 60 * 1000000ll)); + EXPECT_EQ(d1, DateAndTime(2024, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 365 * 24 * 60 * 60 * 1000000ll)); + EXPECT_EQ(d1, DateAndTime(2025, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 365 * 24 * 60 * 60 * 1000ll)); + EXPECT_EQ(d1, DateAndTime(2026, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 365 * 24 * 60 * 60)); + EXPECT_EQ(d1, DateAndTime(2027, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 366 * 24 * 60, 0)); + EXPECT_EQ(d1, DateAndTime(2028, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 365 * 24, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2029, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 365, 0, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2030, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 12, 0, 0, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2031, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(1, 0, 0, 0, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2032, 12, 31, 23, 1, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, -10, -3, 0, -1, 0)); + EXPECT_EQ(d1, DateAndTime(2032, 2, 28, 23, 0, 0, 0, 0)); + EXPECT_TRUE(d1.add(0, 0, 1, 0, 59, 59, 999, 999)); + EXPECT_EQ(d1, DateAndTime(2032, 2, 29, 23, 59, 59, 999, 999)); + EXPECT_TRUE(d1.add(-1, 0, 0, 0, 0, 0, 0, 0)); + EXPECT_EQ(d1, DateAndTime(2031, 3, 1, 23, 59, 59, 999, 999)); + EXPECT_TRUE(d1.add(0, -1, -29, 0, 2, -120, 1, -1999)); + EXPECT_EQ(d1, DateAndTime(2030, 12, 31, 23, 59, 59, 999, 0)); + EXPECT_TRUE(d1.add(0, 0, 0, 0, 0, 0, 0, 1000)); + EXPECT_EQ(d1, DateAndTime(2031, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(d1.add(1, -12, 2, -48, 2, -120, 10, -10000)); + EXPECT_EQ(d1, DateAndTime(2031, 1, 1, 0, 0, 0, 0, 0)); + EXPECT_TRUE(d1.add(-3, 36, -1, 24, -3, 180, -5, 4999)); + EXPECT_EQ(d1, DateAndTime(2030, 12, 31, 23, 59, 59, 999, 999)); + + // test addDays() and addMicrosec() helpers + EXPECT_TRUE(d1.addDays(15)); + EXPECT_EQ(d1, DateAndTime(2031, 1, 15, 23, 59, 59, 999, 999)); + EXPECT_TRUE(d1.addDays(-16)); + EXPECT_EQ(d1, DateAndTime(2030, 12, 30, 23, 59, 59, 999, 999)); + EXPECT_TRUE(d1.addMicrosec(2)); + EXPECT_EQ(d1, DateAndTime(2030, 12, 31, 0, 0, 0, 0, 1)); + EXPECT_TRUE(d1.addMicrosec(-2002)); + EXPECT_EQ(d1, DateAndTime(2030, 12, 30, 23, 59, 59, 997, 999)); + + // test speedup of adding large number of days + d1.set(2000, 1, 1, 0, 0, 0); + EXPECT_TRUE(d1.addDays(366)); + EXPECT_EQ(d1, DateAndTime(2001, 1, 1, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(-366 - 365)); + EXPECT_EQ(d1, DateAndTime(1999, 1, 1, 0, 0, 0)); + d1.set(2000, 3, 1, 0, 0, 0); + EXPECT_TRUE(d1.addDays(365)); + EXPECT_EQ(d1, DateAndTime(2001, 3, 1, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(-365)); + EXPECT_EQ(d1, DateAndTime(2000, 3, 1, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(-366)); + EXPECT_EQ(d1, DateAndTime(1999, 3, 1, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(3 * 366 + 7 * 365)); // leap years: 2000, 2004, 2008 + EXPECT_EQ(d1, DateAndTime(2009, 3, 1, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(7 * 366 + 23 * 365)); // leap years: 2012, 2016, 2020, 2024, 2028, 2032, 2036 + EXPECT_EQ(d1, DateAndTime(2039, 3, 1, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(-(3 * 366 + 12 * 365))); // leap years: 2028, 2032, 2036 (2024 not included due to date after Feb) + EXPECT_EQ(d1, DateAndTime(2024, 3, 1, 0, 0, 0)); + d1.set(2039, 2, 28, 0, 0, 0); + EXPECT_TRUE(d1.addDays(-(4 * 366 + 11 * 365))); // leap years: 2024, 2028, 2032, 2036 + EXPECT_EQ(d1, DateAndTime(2024, 2, 28, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(97 * 366 + 303 * 365)); // 400 years always have the same number of leap years + EXPECT_EQ(d1, DateAndTime(2424, 2, 28, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(2 * 366 + 3 * 365)); // leap years: 2424, 2028 + EXPECT_EQ(d1, DateAndTime(2429, 2, 28, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(97 * 366 + 304 * 365)); // 400 years always have the same number of leap years + 1 year + EXPECT_EQ(d1, DateAndTime(2830, 2, 28, 0, 0, 0)); + EXPECT_TRUE(d1.addDays(-3 * (97 * 366 + 303 * 365))); // 400 years always have the same number of leap years + EXPECT_EQ(d1, DateAndTime(1630, 2, 28, 0, 0, 0)); + EXPECT_TRUE(d1.addDays((97 * 366 + 303 * 365) + 365 + 1)); // + 400 years + 1 year (2031) + 1 day + EXPECT_EQ(d1, DateAndTime(2031, 3, 1, 0, 0, 0)); + + // test some error cases + EXPECT_FALSE(d1.addDays(366 * 66000)); + EXPECT_FALSE(d1.add(INT64_MAX - 1000, 0, 0)); + EXPECT_FALSE(d1.add(0, INT64_MAX, 0)); + d1.setTime(0, 0, 0, 0, 999); + EXPECT_FALSE(d1.add(0, 0, 0, 0, 0, 0, 0, INT64_MAX - 998)); + EXPECT_FALSE(d1.add(0, 0, 0, 0, 0, 0, INT64_MIN, INT64_MIN)); +} - // Test leap year scenarios (2024 is a leap year) - DateAndTime year_2024 = { 0, 0, 0, 0, 1, 1, 24 }; - DateAndTime year_2025 = { 0, 0, 0, 0, 1, 1, 25 }; - long long leap_year_ms = 366LL * 86400000LL; - EXPECT_EQ(year_2025 - year_2024, leap_year_ms); +uint64 microSeconds(uint64 days, uint64 hours, uint64 minutes, uint64 seconds, uint64 milli, uint64 micro) +{ + return (((((((days * 24) + hours) * 60llu) + minutes) * 60 + seconds) * 1000) + milli) * 1000 + micro; } -TEST(DateAndTimeTest, Addition) { - DateAndTime base = { 0, 59, 59, 23, 31, 12, 23 }; // 2023-12-31 23:59:59.000 - - // Add 1 second to roll over everything to the next year - DateAndTime expected_new_year = { 0, 0, 0, 0, 1, 1, 24 }; - EXPECT_EQ(base + 1000LL, expected_new_year); - - // Add 0 - EXPECT_EQ(base + 0LL, base); - - // Add 1 millisecond - DateAndTime expected_1ms = { 1, 59, 59, 23, 31, 12, 23 }; - EXPECT_EQ(base + 1LL, expected_1ms); - - // Test adding across a leap day - // 2024 is a leap year - DateAndTime before_leap_day = { 0, 0, 0, 0, 28, 2, 24 }; - long long two_days_ms = 2 * 86400000LL; - DateAndTime after_leap_day = { 0, 0, 0, 0, 1, 3, 24 }; - EXPECT_EQ(before_leap_day + two_days_ms, after_leap_day); - - // Test adding a full common year's worth of milliseconds - DateAndTime start_of_2025 = { 0, 0, 0, 0, 1, 1, 25 }; - DateAndTime start_of_2026 = { 0, 0, 0, 0, 1, 1, 26 }; - long long common_year_ms = 365LL * 86400000LL; - EXPECT_EQ(start_of_2025 + common_year_ms, start_of_2026); -} \ No newline at end of file +TEST(DateAndTimeTest, Subtraction) +{ + DateAndTime d0; + DateAndTime d1(2030, 12, 31, 5, 4, 3, 2, 1); + EXPECT_EQ(d0.durationMicrosec(d0), UINT64_MAX); + EXPECT_EQ(d0.durationMicrosec(d1), UINT64_MAX); + EXPECT_EQ(d1.durationMicrosec(d0), UINT64_MAX); + + DateAndTime d2(2030, 12, 31, 0, 0, 0, 0, 0); + EXPECT_EQ(d1.durationMicrosec(d1), 0); + EXPECT_EQ(d1.durationMicrosec(d2), d2.durationMicrosec(d1)); + EXPECT_EQ(d1.durationMicrosec(d2), microSeconds(0, 5, 4, 3, 2, 1)); + EXPECT_EQ(d1.durationDays(d2), 0); + + d1.set(2030, 12, 31, 23, 59, 59, 999, 999); + d2.set(2031, 1, 1, 0, 0, 0, 0, 0); + EXPECT_EQ(d1.durationMicrosec(d2), 1); + EXPECT_EQ(d1.durationDays(d2), 0); + + d1.set(2025, 1, 1, 0, 0, 0, 0, 0); + d2.set(2027, 1, 1, 0, 0, 0, 0, 0); + EXPECT_EQ(d1.durationMicrosec(d2), microSeconds(2 * 365, 0, 0, 0, 0, 0)); + EXPECT_EQ(d1.durationDays(d2), 2 * 365); + + d1.set(2027, 1, 1, 12, 15, 43, 123, 456); + d2.set(2025, 1, 1, 11, 10, 34, 101, 412); + EXPECT_EQ(d1.durationMicrosec(d2), microSeconds(2 * 365, 1, 5, 9, 22, 44)); + EXPECT_EQ(d1.durationDays(d2), 2 * 365); + + d1.set(2027, 1, 1, 12, 15, 43, 123, 456); + d2.set(2024, 1, 1, 11, 10, 34, 101, 412); + EXPECT_EQ(d1.durationMicrosec(d2), microSeconds(366 + 2 * 365, 1, 5, 9, 22, 44)); + EXPECT_EQ(d2.durationMicrosec(d1), microSeconds(366 + 2 * 365, 1, 5, 9, 22, 44)); + EXPECT_EQ(d1.durationDays(d2), 366 + 2 * 365); + EXPECT_EQ(d2.durationDays(d1), 366 + 2 * 365); + + d2.set(2024, 1, 1, 0, 0, 0, 0, 0); + d1.set(2024, 4, 1, 0, 0, 0, 0, 42); + EXPECT_EQ(d1.durationMicrosec(d2), microSeconds(31 + 29 + 31, 0, 0, 0, 0, 42)); + EXPECT_EQ(d1.durationDays(d2), 31 + 29 + 31); + + std::mt19937_64 gen64(42); + for (int i = 0; i < 1000; ++i) + { + d1.set((gen64() % 3000) + 1500, (gen64() % 12) + 1, (gen64() % 28) + 1, gen64() % 24, + gen64() % 60, gen64() % 60, gen64() % 999, gen64() % 999); + EXPECT_TRUE(d1.isValid()); + + sint64 microsec = (sint64)gen64() % (1000llu * 365 * 24 * 60 * 60 * 1000 * 1000); + d2 = d1; + EXPECT_TRUE(d2.addMicrosec(microsec)); + EXPECT_TRUE(d2.isValid()); + + EXPECT_EQ(d2.durationMicrosec(d1), microsec); + } +} diff --git a/test/qpi_hash_map.cpp b/test/qpi_hash_map.cpp index 3760c17f1..0f9b7acbc 100644 --- a/test/qpi_hash_map.cpp +++ b/test/qpi_hash_map.cpp @@ -2,20 +2,9 @@ #include "gtest/gtest.h" -static void* __scratchpadBuffer = nullptr; -static void* __scratchpad() -{ - return __scratchpadBuffer; -} -namespace QPI -{ - struct QpiContextProcedureCall; - struct QpiContextFunctionCall; -} -typedef void (*USER_FUNCTION)(const QPI::QpiContextFunctionCall&, void* state, void* input, void* output, void* locals); -typedef void (*USER_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); - +#include "../src/contract_core/pre_qpi_def.h" #include "../src/contracts/qpi.h" +#include "../src/common_buffers.h" #include "../src/contract_core/qpi_hash_map_impl.h" #include #include @@ -374,7 +363,7 @@ TYPED_TEST_P(QPIHashMapTest, TestCleanup) constexpr QPI::uint64 capacity = 4; QPI::HashMap hashMap; - __scratchpadBuffer = new char[2 * sizeof(hashMap)]; + reorgBuffer = new char[2 * sizeof(hashMap)]; std::array keyValuePairs = HashMapTestData::CreateKeyValueTestPairs(); auto ids = std::views::keys(keyValuePairs); @@ -413,8 +402,8 @@ TYPED_TEST_P(QPIHashMapTest, TestCleanup) EXPECT_NE(returnedIndex, QPI::NULL_INDEX); EXPECT_EQ(hashMap.population(), 4); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TYPED_TEST_P(QPIHashMapTest, TestCleanupPerformanceShortcuts) @@ -450,7 +439,7 @@ TEST(NonTypedQPIHashMapTest, TestCleanupLargeMapSameHashes) constexpr QPI::uint64 capacity = 64; QPI::HashMap hashMap; - __scratchpadBuffer = new char[2 * sizeof(hashMap)]; + reorgBuffer = new char[2 * sizeof(hashMap)]; for (QPI::uint64 i = 0; i < 64; ++i) { @@ -463,8 +452,8 @@ TEST(NonTypedQPIHashMapTest, TestCleanupLargeMapSameHashes) // Cleanup will have to iterate through the whole map to find an empty slot for the last element. hashMap.cleanup(); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TYPED_TEST_P(QPIHashMapTest, TestReplace) @@ -623,7 +612,7 @@ void testHashMapPseudoRandom(int seed, int cleanups, int percentAdd, int percent std::map referenceMap; QPI::HashMap map; - __scratchpadBuffer = new char[2 * sizeof(map)]; + reorgBuffer = new char[2 * sizeof(map)]; map.reset(); @@ -685,8 +674,8 @@ void testHashMapPseudoRandom(int seed, int cleanups, int percentAdd, int percent // std::cout << "capacity: " << set.capacity() << ", pupulation:" << set.population() << std::endl; } - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(QPIHashMapTest, HashMapPseudoRandom) @@ -718,7 +707,7 @@ TEST(QPIHashMapTest, HashSet) { constexpr QPI::uint64 capacity = 128; QPI::HashSet hashSet; - __scratchpadBuffer = new char[2 * sizeof(hashSet)]; + reorgBuffer = new char[2 * sizeof(hashSet)]; EXPECT_EQ(hashSet.capacity(), capacity); // Test add() and contains() @@ -816,8 +805,8 @@ TEST(QPIHashMapTest, HashSet) hashSet.reset(); EXPECT_EQ(hashSet.population(), 0); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } template @@ -886,7 +875,7 @@ void testHashSetPseudoRandom(int seed, int cleanups, int percentAdd, int percent std::set referenceSet; QPI::HashSet set; - __scratchpadBuffer = new char[2 * sizeof(set)]; + reorgBuffer = new char[2 * sizeof(set)]; set.reset(); @@ -946,8 +935,8 @@ void testHashSetPseudoRandom(int seed, int cleanups, int percentAdd, int percent // std::cout << "capacity: " << set.capacity() << ", pupulation:" << set.population() << std::endl; } - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(QPIHashMapTest, HashSetPseudoRandom) @@ -983,7 +972,7 @@ static void perfTestCleanup(int seed) std::mt19937_64 gen64(seed); auto* set = new QPI::HashSet(); - __scratchpadBuffer = new char[sizeof(*set)]; + reorgBuffer = new char[sizeof(*set)]; for (QPI::uint64 i = 1; i <= 100; ++i) { @@ -1007,8 +996,8 @@ static void perfTestCleanup(int seed) } delete set; - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(QPIHashMapTest, HashSetPerfTest) diff --git a/test/quorum_value.cpp b/test/quorum_value.cpp new file mode 100644 index 000000000..3698e5d76 --- /dev/null +++ b/test/quorum_value.cpp @@ -0,0 +1,140 @@ +#define NO_UEFI + +#include +#include +#include + +#include "gtest/gtest.h" +#include "../src/platform/quorum_value.h" + +TEST(FixedTypeQuorumTest, CalculateAscendingQuorumSimple) +{ + long long values[10] = {10, 5, 8, 3, 7, 1, 9, 2, 6, 4}; + + long long result = calculateAscendingQuorumValue(values, 10); + + // (10 * 2) / 3 = 6, index 6 after sorting = 7 + EXPECT_EQ(result, 7); +} + +TEST(FixedTypeQuorumTest, Calculate676Quorum) +{ + long long values[676]; + for (int i = 0; i < 676; i++) + { + values[i] = i + 1; + } + + long long quorum = calculateAscendingQuorumValue(values, 676); + + // (676 * 2) / 3 = 450, value at index 450 = 451 + EXPECT_EQ(quorum, 451); +} + +TEST(FixedTypeQuorumTest, EmptyArray) +{ + long long values[1] = {0}; + long long result = calculateAscendingQuorumValue(values, 0); + EXPECT_EQ(result, 0); +} + +TEST(FixedTypeQuorumTest, SingleElement) +{ + long long values[1] = {42}; + long long result = calculateAscendingQuorumValue(values, 1); + EXPECT_EQ(result, 42); +} + +TEST(FixedTypeQuorumTest, PercentileDescending) +{ + int values[10] = {10, 5, 8, 3, 7, 1, 9, 2, 6, 4}; + + int result = calculatePercentileValue(values, 10); + + // (10 * 1) / 3 = 3, descending sort, index 3 = 7 + EXPECT_EQ(result, 7); +} + +TEST(FixedTypeQuorumTest, AllZeros) +{ + long long values[676] = {0}; + + long long quorum = calculateAscendingQuorumValue(values, 676); + + // All values are 0, quorum should be 0 + EXPECT_EQ(quorum, 0); +} + +TEST(FixedTypeQuorumTest, MostlyZeros) +{ + long long values[676] = {0}; + + // Set first 225 elements to non-zero values + for (int i = 0; i < 225; i++) + { + values[i] = i + 1; + } + + long long quorum = calculateAscendingQuorumValue(values, 676); + + // After sorting: 0,0,0,...,0 (451 zeros), 1,2,3,...,225 + // Index 450 will be 0 (since 676-225 = 451 zeros, and index 450 < 451) + EXPECT_EQ(quorum, 0); +} + +template +std::vector prepareData(unsigned int seed, unsigned int numElements) +{ + std::mt19937 gen32(seed); + + unsigned int numberOfBlocks = (sizeof(T) + 3) / 4; + + std::vector vec(numElements, 0); + for (unsigned int i = 0; i < numElements; ++i) + { + for (unsigned int b = 0; b < numberOfBlocks; ++b) + { + vec[i] |= (static_cast(gen32()) << (b * 32)); + } + } + + return vec; +} + +template +void testCalculatePercentile(unsigned int seed) +{ + std::vector vec = prepareData(seed, 676); + + std::vector referenceVec = vec; + std::sort(referenceVec.begin(), referenceVec.end()); + + T result = calculatePercentileValue(vec.data(), static_cast(vec.size())); + + // Calculate expected index: (676 * 2) / 3 = 450 + unsigned int expectedIndex = (676 * 2) / 3; + EXPECT_EQ(result, referenceVec[expectedIndex]); +} + +template +class QuorumValueTest : public testing::Test {}; + +using testing::Types; + +TYPED_TEST_CASE_P(QuorumValueTest); + +TYPED_TEST_P(QuorumValueTest, CalculatePercentile) +{ + unsigned int metaSeed = 98765; + std::mt19937 gen32(metaSeed); + + for (unsigned int t = 0; t < 10; ++t) + testCalculatePercentile(gen32()); +} + +REGISTER_TYPED_TEST_CASE_P(QuorumValueTest, + CalculatePercentile +); + +typedef Types TestTypes; +INSTANTIATE_TYPED_TEST_CASE_P(TypeParamQuorumValueTests, QuorumValueTest, TestTypes); diff --git a/test/score.cpp b/test/score.cpp index f382fb7fa..cc7c19f53 100644 --- a/test/score.cpp +++ b/test/score.cpp @@ -4,9 +4,6 @@ #define ENABLE_PROFILING 0 -// needed for scoring task queue -#define NUMBER_OF_TRANSACTIONS_PER_TICK 1024 - // current optimized implementation #include "../src/score.h" @@ -41,11 +38,14 @@ static constexpr bool PRINT_DETAILED_INFO = false; // set to 0 for run all available samples // For profiling enable, run all available samples -static constexpr unsigned long long COMMON_TEST_NUMBER_OF_SAMPLES = ENABLE_PROFILING ? 0 : 32; +static constexpr unsigned long long COMMON_TEST_NUMBER_OF_SAMPLES = 32; +static constexpr unsigned long long PROFILING_NUMBER_OF_SAMPLES = 32; + // set 0 for run maximum number of threads of the computer. // For profiling enable, set it equal to deployment setting -static constexpr int MAX_NUMBER_OF_THREADS = ENABLE_PROFILING ? 12 : 0; +static constexpr int MAX_NUMBER_OF_THREADS = 0; +static constexpr int MAX_NUMBER_OF_PROFILING_THREADS = 12; static bool gCompareReference = false; // Only run on specific index of samples and setting @@ -65,7 +65,6 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, { return; } - auto pScore = std::make_unique>(); + pScore->initMemory(); pScore->initMiningData(miningSeed); int x = 0; @@ -116,7 +116,7 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, gtIndex = gScoreIndexMap[i]; } - if (ENABLE_PROFILING || PRINT_DETAILED_INFO || gtIndex < 0 || (score_value != gScoresGroundTruth[sampleIndex][gtIndex])) + if (PRINT_DETAILED_INFO || gtIndex < 0 || (score_value != gScoresGroundTruth[sampleIndex][gtIndex])) { if (gScoreProcessingTime.count(i) == 0) { @@ -126,8 +126,6 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, { gScoreProcessingTime[i] += elapsed; } - - if (!ENABLE_PROFILING) { std::cout << "[sample " << sampleIndex << "; setting " << i << ": " @@ -137,7 +135,7 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, << kSettings[i][score_params::POPULATION_THRESHOLD] << ", " << kSettings[i][score_params::NUMBER_OF_MUTATIONS] << ", " << kSettings[i][score_params::SOLUTION_THRESHOLD] - << "]" + << "]" << std::endl; std::cout << " score " << score_value; if (gtIndex >= 0) @@ -151,8 +149,6 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, std::cout << " time " << elapsed << " ms " << std::endl; } } - - if (!ENABLE_PROFILING) { EXPECT_GT(gScoreIndexMap.count(i), 0); if (gtIndex >= 0) @@ -163,27 +159,68 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, } } +// Recursive template to process each element in scoreSettings +template +static void processElementWithPerformance(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex) +{ + auto pScore = std::make_unique>(); + + pScore->initMemory(); + pScore->initMiningData(miningSeed); + int x = 0; + top_of_stack = (unsigned long long)(&x); + auto t0 = std::chrono::high_resolution_clock::now(); + unsigned int score_value = (*pScore)(0, publicKey, miningSeed, nonce); + auto t1 = std::chrono::high_resolution_clock::now(); + auto d = t1 - t0; + auto elapsed = std::chrono::duration_cast(d).count(); + +#pragma omp critical + { + if (gScoreProcessingTime.count(i) == 0) + { + gScoreProcessingTime[i] = elapsed; + } + else + { + gScoreProcessingTime[i] += elapsed; + } + } +} + // Main processing function -template +template static void processHelper(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex, std::index_sequence) { - (processElement(miningSeed, publicKey, nonce, sampleIndex), ...); + if constexpr (profiling) + { + (processElementWithPerformance(miningSeed, publicKey, nonce, sampleIndex), ...); + } + else + { + (processElement(miningSeed, publicKey, nonce, sampleIndex), ...); + } } // Recursive template to process each element in scoreSettings -template +template static void process(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex) { - processHelper(miningSeed, publicKey, nonce, sampleIndex, std::make_index_sequence{}); + processHelper(miningSeed, publicKey, nonce, sampleIndex, std::make_index_sequence{}); } void runCommonTests() { -#ifdef ENABLE_PROFILING - gProfilingDataCollector.init(1024); -#endif - #if defined (__AVX512F__) && !GENERIC_K12 initAVX512KangarooTwelveConstants(); #endif @@ -198,21 +235,23 @@ void runCommonTests() // Convert the raw string and do the data verification unsigned long long numberOfSamplesReadFromFile = sampleString.size(); unsigned long long numberOfSamples = numberOfSamplesReadFromFile; - if (COMMON_TEST_NUMBER_OF_SAMPLES > 0) + unsigned long long requestedNumberOfSamples = COMMON_TEST_NUMBER_OF_SAMPLES; + + if (requestedNumberOfSamples > 0) { - std::cout << "Request testing with " << COMMON_TEST_NUMBER_OF_SAMPLES << " samples." << std::endl; + std::cout << "Request testing with " << requestedNumberOfSamples << " samples." << std::endl; - numberOfSamples = std::min(COMMON_TEST_NUMBER_OF_SAMPLES, numberOfSamples); - if (COMMON_TEST_NUMBER_OF_SAMPLES <= numberOfSamples) + numberOfSamples = std::min(requestedNumberOfSamples, numberOfSamples); + if (requestedNumberOfSamples <= numberOfSamples) { - numberOfSamples = COMMON_TEST_NUMBER_OF_SAMPLES; + numberOfSamples = requestedNumberOfSamples; } else // Request number of samples greater than existed. Only valid for reference score validation only { if (gCompareReference) { - numberOfSamples = COMMON_TEST_NUMBER_OF_SAMPLES; - std::cout << "Refenrece comparison mode: " << numberOfSamples << " samples are read from file for comparision." + numberOfSamples = requestedNumberOfSamples; + std::cout << "Refenrece comparison mode: " << numberOfSamples << " samples are read from file for comparision." << "Remained are generated randomly." << std::endl; } @@ -232,9 +271,9 @@ void runCommonTests() { if (i < numberOfSamplesReadFromFile) { - miningSeeds[i] = hexToByte(sampleString[i][0], 32); - publicKeys[i] = hexToByte(sampleString[i][1], 32); - nonces[i] = hexToByte(sampleString[i][2], 32); + miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); + publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); + nonces[i] = hexTo32Bytes(sampleString[i][2], 32); } else // Samples from files are not enough, randomly generate more { @@ -282,7 +321,7 @@ void runCommonTests() int count = 0; for (unsigned long long j = 0; j < score_params::MAX_PARAM_TYPE; ++j) { - if (scoresSettingHeader[j] == score_params::kSettings[i][j]) + if (scoresSettingHeader[j] == kSettings[i][j]) { count++; } @@ -321,6 +360,7 @@ void runCommonTests() } } + // Run the test unsigned int numberOfThreads = std::thread::hardware_concurrency(); if (MAX_NUMBER_OF_THREADS > 0) @@ -328,23 +368,15 @@ void runCommonTests() numberOfThreads = numberOfThreads > MAX_NUMBER_OF_THREADS ? MAX_NUMBER_OF_THREADS : numberOfThreads; } - if (ENABLE_PROFILING) + if (numberOfThreads > 1) { - std::cout << "Running " << numberOfThreads << " threads for collecting multiple threads performance" << std::endl; + std::cout << "Compare score only. Lauching test with all available " << numberOfThreads << " threads." << std::endl; } else { - if (numberOfThreads > 1) - { - std::cout << "Compare score only. Lauching test with all available " << numberOfThreads << " threads." << std::endl; - } - else - { - std::cout << "Running one sample on one thread for collecting single thread performance." << std::endl; - } + std::cout << "Running one sample on one thread for collecting single thread performance." << std::endl; } - std::vector samples; for (int i = 0; i < numberOfSamples; ++i) { @@ -356,29 +388,26 @@ void runCommonTests() samples.push_back(i); } - std::string compTerm = " and compare with groundtruths from file."; + std::string compTerm = "and compare with groundtruths from file."; if (gCompareReference) { - compTerm = " and compare with reference code."; - } - if (ENABLE_PROFILING) - { - compTerm = "for profiling, without comparing any result (set test case FAILED as default)"; + compTerm = "and compare with reference code."; } std::cout << "Processing " << samples.size() << " samples " << compTerm << "..." << std::endl; + gScoreProcessingTime.clear(); #pragma omp parallel for num_threads(numberOfThreads) for (int i = 0; i < samples.size(); ++i) { int index = samples[i]; - process(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); + process<0, numberOfGeneratedSetting>(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); #pragma omp critical std::cout << i << ", "; } std::cout << std::endl; // Print the average processing time - if (PRINT_DETAILED_INFO || ENABLE_PROFILING) + if (PRINT_DETAILED_INFO) { for (auto scoreTime : gScoreProcessingTime) { @@ -394,18 +423,137 @@ void runCommonTests() << "]: " << processingTime << " ms" << std::endl; } } +} -#ifdef ENABLE_PROFILING - gProfilingDataCollector.writeToFile(); +void runPerformanceTests() +{ + +#if defined (__AVX512F__) && !GENERIC_K12 + initAVX512KangarooTwelveConstants(); #endif -} + constexpr unsigned long long numberOfGeneratedSetting = sizeof(score_params::kProfileSettings) / sizeof(score_params::kProfileSettings[0]); + + // Read the parameters and results + auto sampleString = readCSV(COMMON_TEST_SAMPLES_FILE_NAME); + auto scoresString = readCSV(COMMON_TEST_SCORES_FILE_NAME); + ASSERT_FALSE(sampleString.empty()); + ASSERT_FALSE(scoresString.empty()); + // Convert the raw string and do the data verification + unsigned long long numberOfSamplesReadFromFile = sampleString.size(); + unsigned long long numberOfSamples = numberOfSamplesReadFromFile; + unsigned long long requestedNumberOfSamples = PROFILING_NUMBER_OF_SAMPLES; + + if (requestedNumberOfSamples > 0) + { + std::cout << "Request testing with " << requestedNumberOfSamples << " samples." << std::endl; + + numberOfSamples = std::min(requestedNumberOfSamples, numberOfSamples); + if (requestedNumberOfSamples <= numberOfSamples) + { + numberOfSamples = requestedNumberOfSamples; + } + } + + std::vector miningSeeds(numberOfSamples); + std::vector publicKeys(numberOfSamples); + std::vector nonces(numberOfSamples); + + // Reading the input samples + for (unsigned long long i = 0; i < numberOfSamples; ++i) + { + if (i < numberOfSamplesReadFromFile) + { + miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); + publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); + nonces[i] = hexTo32Bytes(sampleString[i][2], 32); + } + else // Samples from files are not enough, randomly generate more + { + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[24]); + + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[24]); + + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[24]); + + } + } + + std::cout << "Profiling " << numberOfGeneratedSetting << " param combinations. " << std::endl; + + // Run the profiling + unsigned int numberOfThreads = std::thread::hardware_concurrency(); + if (MAX_NUMBER_OF_PROFILING_THREADS > 0) + { + numberOfThreads = numberOfThreads > MAX_NUMBER_OF_PROFILING_THREADS ? MAX_NUMBER_OF_PROFILING_THREADS : numberOfThreads; + } + std::cout << "Running " << numberOfThreads << " threads for collecting multiple threads performance" << std::endl; + + std::vector samples; + for (int i = 0; i < numberOfSamples; ++i) + { + if (!filteredSamples.empty() + && std::find(filteredSamples.begin(), filteredSamples.end(), i) == filteredSamples.end()) + { + continue; + } + samples.push_back(i); + } + + std::string compTerm = "for profiling, don't compare any result."; + + std::cout << "Processing " << samples.size() << " samples " << compTerm << "..." << std::endl; + gScoreProcessingTime.clear(); +#pragma omp parallel for num_threads(numberOfThreads) + for (int i = 0; i < samples.size(); ++i) + { + int index = samples[i]; + process<1, numberOfGeneratedSetting>(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); +#pragma omp critical + std::cout << i << ", "; + } + std::cout << std::endl; + + // Print the average processing time + for (auto scoreTime : gScoreProcessingTime) + { + unsigned long long processingTime = filteredSamples.empty() ? scoreTime.second / numberOfSamples : scoreTime.second / filteredSamples.size(); + std::cout << "Avg processing time [setting " << scoreTime.first << " " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_INPUT_NEURONS] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_OUTPUT_NEURONS] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_TICKS] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_NEIGHBORS] << ", " + << kProfileSettings[scoreTime.first][score_params::POPULATION_THRESHOLD] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_MUTATIONS] << ", " + << kProfileSettings[scoreTime.first][score_params::SOLUTION_THRESHOLD] + << "]: " << processingTime << " ms" << std::endl; + } + gProfilingDataCollector.writeToFile(); +} TEST(TestQubicScoreFunction, CommonTests) { runCommonTests(); } +#if ENABLE_PROFILING + +TEST(TestQubicScoreFunction, PerformanceTests) +{ + runPerformanceTests(); +} +#endif + +#if not ENABLE_PROFILING TEST(TestQubicScoreFunction, TestDeterministic) { constexpr int NUMBER_OF_THREADS = 4; @@ -430,9 +578,9 @@ TEST(TestQubicScoreFunction, TestDeterministic) // Reading the input samples for (unsigned long long i = 0; i < numberOfSamples; ++i) { - miningSeeds[i] = hexToByte(sampleString[i][0], 32); - publicKeys[i] = hexToByte(sampleString[i][1], 32); - nonces[i] = hexToByte(sampleString[i][2], 32); + miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); + publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); + nonces[i] = hexTo32Bytes(sampleString[i][2], 32); } auto pScore = std::make_uniqueinitMemory(); // Run with 4 mining seeds, each run 4 separate threads and the result need to matched - int scores[NUMBER_OF_PHASES][NUMBER_OF_THREADS * NUMBER_OF_SAMPLES] = {0}; + int scores[NUMBER_OF_PHASES][NUMBER_OF_THREADS * NUMBER_OF_SAMPLES] = { 0 }; for (unsigned long long i = 0; i < NUMBER_OF_PHASES; ++i) { pScore->initMiningData(miningSeeds[i]); @@ -485,3 +633,4 @@ TEST(TestQubicScoreFunction, TestDeterministic) } } } +#endif diff --git a/test/score_params.h b/test/score_params.h index 1748db117..7ef8b8910 100644 --- a/test/score_params.h +++ b/test/score_params.h @@ -17,15 +17,14 @@ enum ParamType // Comment out when we want to reduce the number of running test static constexpr unsigned long long kSettings[][MAX_PARAM_TYPE] = { -#if ENABLE_PROFILING - {::NUMBER_OF_INPUT_NEURONS, ::NUMBER_OF_OUTPUT_NEURONS, ::NUMBER_OF_TICKS, ::NUMBER_OF_NEIGHBORS, ::POPULATION_THRESHOLD, ::NUMBER_OF_MUTATIONS, ::SOLUTION_THRESHOLD_DEFAULT}, -#else - //{ ::NUMBER_OF_INPUT_NEURONS, ::NUMBER_OF_OUTPUT_NEURONS, ::NUMBER_OF_TICKS, ::NUMBER_OF_NEIGHBORS, ::POPULATION_THRESHOLD, ::NUMBER_OF_MUTATIONS, ::SOLUTION_THRESHOLD_DEFAULT }, {64, 64, 50, 64, 178, 50, 36}, {256, 256, 120, 256, 612, 100, 171}, {512, 512, 150, 512, 1174, 150, 300}, {1024, 1024, 200, 1024, 3000, 200, 600} - -#endif }; + +static constexpr unsigned long long kProfileSettings[][MAX_PARAM_TYPE] = { + {::NUMBER_OF_INPUT_NEURONS, ::NUMBER_OF_OUTPUT_NEURONS, ::NUMBER_OF_TICKS, ::NUMBER_OF_NEIGHBORS, ::POPULATION_THRESHOLD, ::NUMBER_OF_MUTATIONS, ::SOLUTION_THRESHOLD_DEFAULT}, +}; + } diff --git a/test/sorting.cpp b/test/sorting.cpp new file mode 100644 index 000000000..b6baf77b0 --- /dev/null +++ b/test/sorting.cpp @@ -0,0 +1,118 @@ +#define NO_UEFI + +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/platform_common/sorting.h" + +constexpr unsigned int MAX_NUM_ELEMENTS = 10000U; +constexpr unsigned int MAX_NUM_TESTS_PER_TYPE = 100U; + +TEST(FixedTypeSortingTest, SortAscendingSimple) +{ + int arr[5] = { 3, 6, 1, 9, 2 }; + + quickSort(arr, 0, 4, SortingOrder::SortAscending); + + EXPECT_EQ(arr[0], 1); + EXPECT_EQ(arr[1], 2); + EXPECT_EQ(arr[2], 3); + EXPECT_EQ(arr[3], 6); + EXPECT_EQ(arr[4], 9); +} + +TEST(FixedTypeSortingTest, SortDescendingSimple) +{ + int arr[5] = { 3, 6, 1, 9, 2 }; + + quickSort(arr, 0, 4, SortingOrder::SortDescending); + + EXPECT_EQ(arr[0], 9); + EXPECT_EQ(arr[1], 6); + EXPECT_EQ(arr[2], 3); + EXPECT_EQ(arr[3], 2); + EXPECT_EQ(arr[4], 1); +} + +template +std::vector prepareData(unsigned int seed) +{ + std::mt19937 gen32(seed); + + unsigned int numberOfBlocks = (sizeof(T) + 3) / 4; + + unsigned int numElements = (gen32() % MAX_NUM_ELEMENTS) + 1; + std::vector vec(numElements, 0); + for (unsigned int i = 0; i < numElements; ++i) + { + // generate 4 bytes at a time until the whole number is generated + for (unsigned int b = 0; b < numberOfBlocks; ++b) + { + vec[i] |= (static_cast(gen32()) << (b * 32)); + } + } + + return vec; +} + +template +void testSortAscending(unsigned int seed) +{ + std::vector vec = prepareData(seed); + + std::vector referenceVec = vec; + std::sort(referenceVec.begin(), referenceVec.end(), std::less<>()); + + quickSort(vec.data(), 0, static_cast(vec.size() - 1), SortingOrder::SortAscending); + + EXPECT_EQ(vec, referenceVec); +} + +template +void testSortDescending(unsigned int seed) +{ + std::vector vec = prepareData(seed); + + std::vector referenceVec = vec; + std::sort(referenceVec.begin(), referenceVec.end(), std::greater<>()); + + quickSort(vec.data(), 0, static_cast(vec.size() - 1), SortingOrder::SortDescending); + + EXPECT_EQ(vec, referenceVec); +} + +template +class SortingTest : public testing::Test {}; + +using testing::Types; + +TYPED_TEST_CASE_P(SortingTest); + +TYPED_TEST_P(SortingTest, SortAscending) +{ + unsigned int metaSeed = 32467; + std::mt19937 gen32(metaSeed); + + for (unsigned int t = 0; t < MAX_NUM_TESTS_PER_TYPE; ++t) + testSortAscending(/*seed=*/gen32()); +} + +TYPED_TEST_P(SortingTest, SortDescending) +{ + unsigned int metaSeed = 45787; + std::mt19937 gen32(metaSeed); + + for (unsigned int t = 0; t < MAX_NUM_TESTS_PER_TYPE; ++t) + testSortDescending(/*seed=*/gen32()); +} + +REGISTER_TYPED_TEST_CASE_P(SortingTest, + SortAscending, + SortDescending +); + +// GTest produces a linker error when using `unsigned short` as test type due to unresolved print function - skip for now. +typedef Types TestTypes; +INSTANTIATE_TYPED_TEST_CASE_P(TypeParamSortingTests, SortingTest, TestTypes); \ No newline at end of file diff --git a/test/stable_computor_index.cpp b/test/stable_computor_index.cpp new file mode 100644 index 000000000..3eaab702c --- /dev/null +++ b/test/stable_computor_index.cpp @@ -0,0 +1,214 @@ +#define NO_UEFI + +#include "gtest/gtest.h" +#include "../src/ticking/stable_computor_index.h" + +class StableComputorIndexTest : public ::testing::Test +{ +protected: + m256i futureComputors[NUMBER_OF_COMPUTORS]; + m256i currentComputors[NUMBER_OF_COMPUTORS]; + unsigned char tempBuffer[stableComputorIndexBufferSize()]; + + void SetUp() override + { + memset(futureComputors, 0, sizeof(futureComputors)); + memset(currentComputors, 0, sizeof(currentComputors)); + memset(tempBuffer, 0, sizeof(tempBuffer)); + } + + m256i makeId(int n) + { + m256i id = m256i::zero(); + id.m256i_u64[1] = n; + return id; + } +}; + +// Test: All computors requalify - all should keep their indices +TEST_F(StableComputorIndexTest, AllRequalify) +{ + // Set up current computors with IDs 1 to NUMBER_OF_COMPUTORS + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: Same IDs but reversed order (simulating score reordering) + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(NUMBER_OF_COMPUTORS - i); + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // All should be back to their original indices + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)) << "Index " << i << " mismatch"; + } +} + +// Test: Half computors replaced - requalifying keep index, new fill gaps +TEST_F(StableComputorIndexTest, PartialRequalify) +{ + // Current: ID 1 at idx 0, ID 2 at idx 1, ..., ID 676 at idx 675 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future input (scrambled): odd IDs requalify, even IDs replaced by new (1001+) + unsigned int requalifyingId = 1; + unsigned int newId = 1001; + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + if (i % 2 == 0 && requalifyingId <= NUMBER_OF_COMPUTORS) + { + futureComputors[i] = makeId(requalifyingId); + requalifyingId += 2; + } + else + { + futureComputors[i] = makeId(newId++); + } + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // Odd IDs (1,3,5,...) should be at original indices (0,2,4,...) + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i += 2) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)); + } + + // New IDs (1001,1002,...) should fill gaps at indices (1,3,5,...) + unsigned int expectedNewId = 1001; + for (unsigned int i = 1; i < NUMBER_OF_COMPUTORS; i += 2) + { + EXPECT_EQ(futureComputors[i], makeId(expectedNewId++)); + } +} + +// Test: All computors are new - order preserved +TEST_F(StableComputorIndexTest, AllNew) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: Completely new set (IDs 1000 to 1675) + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1000); + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // New computors should fill slots in order + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1000)); + } +} + +// Test: Single computor requalifies +TEST_F(StableComputorIndexTest, SingleRequalify) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: Only ID 100 requalifies (at position 0), rest are new + futureComputors[0] = makeId(100); // Requalifying, was at idx 99 + for (unsigned int i = 1; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1000); // New IDs + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // ID 100 should be at its original index 99 + EXPECT_EQ(futureComputors[99], makeId(100)); + + // New computors fill remaining slots (0-98, 100-675) + unsigned int newIdx = 0; + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + if (i == 99) continue; // Skip the requalifying slot + EXPECT_EQ(futureComputors[i], makeId(newIdx + 1001)) << "New computor at index " << i; + newIdx++; + } +} + + +// Test: First and last computor swap positions in input +TEST_F(StableComputorIndexTest, FirstLastSwap) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: All same IDs, but first and last swapped in input order + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1); + } + futureComputors[0] = makeId(NUMBER_OF_COMPUTORS); // Last ID at first position + futureComputors[NUMBER_OF_COMPUTORS - 1] = makeId(1); // First ID at last position + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // All should be at their original indices regardless of input order + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)) << "Index " << i << " mismatch"; + } +} + +// Test: Realistic scenario - 225 computors change (max allowed) +TEST_F(StableComputorIndexTest, MaxChange225) +{ + // Current: IDs 1 to 676 + for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) + { + currentComputors[i] = makeId(i + 1); + } + + // Future: First 451 (QUORUM) stay, last 225 are replaced with new IDs + for (unsigned int i = 0; i < 451; i++) + { + futureComputors[i] = makeId(i + 1); // Same IDs, possibly different order + } + for (unsigned int i = 451; i < NUMBER_OF_COMPUTORS; i++) + { + futureComputors[i] = makeId(i + 1000); // New IDs + } + + bool result = calculateStableComputorIndex(futureComputors, currentComputors, tempBuffer); + ASSERT_TRUE(result); + + // First 451 should keep their indices + for (unsigned int i = 0; i < 451; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1)); + } + + // Last 225 slots should have the new IDs + for (unsigned int i = 451; i < NUMBER_OF_COMPUTORS; i++) + { + EXPECT_EQ(futureComputors[i], makeId(i + 1000)); + } +} + diff --git a/test/test.vcxproj b/test/test.vcxproj index b5611ed87..32cf591b4 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -17,7 +17,7 @@ {30e8e249-6b00-4575-bcdf-be2445d5e099} Win32Proj - 10.0.22621.0 + 10.0 Application v143 Unicode @@ -40,7 +40,7 @@ Level3 false stdcpp20 - ../src;$(MSBuildProjectDirectory);$(MSBuildProjectDirectory)\..\;%(AdditionalIncludeDirectories) + ../src;$(MSBuildProjectDirectory);$(MSBuildProjectDirectory)\..\;..\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-dyn.1.8.1.8\build\native\include;%(AdditionalIncludeDirectories) AdvancedVectorExtensions2 true true @@ -66,7 +66,7 @@ true Speed true - ../src;$(MSBuildProjectDirectory);$(MSBuildProjectDirectory)\..\;%(AdditionalIncludeDirectories) + ../src;$(MSBuildProjectDirectory);$(MSBuildProjectDirectory)\..\;..\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-dyn.1.8.1.8\build\native\include;%(AdditionalIncludeDirectories) false true true @@ -95,7 +95,7 @@ true Speed true - ../src;$(MSBuildProjectDirectory);$(MSBuildProjectDirectory)\..\;%(AdditionalIncludeDirectories) + ../src;$(MSBuildProjectDirectory);$(MSBuildProjectDirectory)\..\;..\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-dyn.1.8.1.8\build\native\include;%(AdditionalIncludeDirectories) false true true @@ -120,7 +120,13 @@ + + + + + + @@ -132,8 +138,15 @@ + + + + + + + @@ -155,9 +168,6 @@ - - - @@ -169,15 +179,18 @@ {88b4cda8-8248-44d0-848e-0e938a2aad6d} + + + - + - Dieses Projekt verweist auf mindestens ein NuGet-Paket, das auf diesem Computer fehlt. Verwenden Sie die Wiederherstellung von NuGet-Paketen, um die fehlenden Dateien herunterzuladen. Weitere Informationen finden Sie unter "http://go.microsoft.com/fwlink/?LinkID=322105". Die fehlende Datei ist "{0}". + Este proyecto hace referencia a los paquetes NuGet que faltan en este equipo. Use la restauración de paquetes NuGet para descargarlos. Para obtener más información, consulte http://go.microsoft.com/fwlink/?LinkID=322105. El archivo que falta es {0}. - + \ No newline at end of file diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 73566057a..5580407e8 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -21,21 +21,33 @@ + + + + + + + + + + + + @@ -44,9 +56,6 @@ - - - {b544a744-99a4-4a9d-b445-211aabef63f2} @@ -57,4 +66,7 @@ core + + + \ No newline at end of file diff --git a/test/tx_status_request.cpp b/test/tx_status_request.cpp index 3ba866d3a..b72084010 100644 --- a/test/tx_status_request.cpp +++ b/test/tx_status_request.cpp @@ -57,7 +57,7 @@ static void enqueueResponse(Peer* peer, unsigned int dataSize, unsigned char typ { const RespondTxStatus* txStatus = (const RespondTxStatus*)data; - EXPECT_EQ(type, RESPOND_TX_STATUS); + EXPECT_EQ(type, RespondTxStatus::type()); EXPECT_EQ(dejavu, requestMessage.header.dejavu()); EXPECT_EQ(dataSize, txStatus->size()); @@ -88,7 +88,7 @@ static void checkTick(unsigned int tick, unsigned long long seed, unsigned short // prepare request message requestMessage.header.checkAndSetSize(sizeof(requestMessage)); - requestMessage.header.setType(REQUEST_TX_STATUS); + requestMessage.header.setType(RequestTxStatus::type()); requestMessage.header.setDejavu(seed % UINT_MAX); requestMessage.payload.tick = tick; diff --git a/test/utils.h b/test/utils.h index a48cc9995..734d88933 100644 --- a/test/utils.h +++ b/test/utils.h @@ -9,7 +9,7 @@ namespace test_utils { -std::string byteToHex(const unsigned char* byteArray, size_t sizeInByte) +static std::string byteToHex(const unsigned char* byteArray, size_t sizeInByte) { std::ostringstream oss; for (size_t i = 0; i < sizeInByte; ++i) @@ -19,7 +19,7 @@ std::string byteToHex(const unsigned char* byteArray, size_t sizeInByte) return oss.str(); } -m256i hexToByte(const std::string& hex, const int sizeInByte) +static m256i hexTo32Bytes(const std::string& hex, const int sizeInByte) { if (hex.length() != sizeInByte * 2) { throw std::invalid_argument("Hex string length does not match the expected size"); @@ -34,8 +34,21 @@ m256i hexToByte(const std::string& hex, const int sizeInByte) return byteArray; } +static void hexToByte(const std::string& hex, const int sizeInByte, unsigned char* out) +{ + if (hex.length() != sizeInByte * 2) + { + throw std::invalid_argument("Hex string length does not match the expected size"); + } + + for (size_t i = 0; i < sizeInByte; ++i) + { + out[i] = std::stoi(hex.substr(i * 2, 2), nullptr, 16); + } +} + // Function to read and parse the CSV file -std::vector> readCSV(const std::string& filename) +static std::vector> readCSV(const std::string& filename) { std::vector> data; std::ifstream file(filename); @@ -61,7 +74,7 @@ std::vector> readCSV(const std::string& filename) return data; } -m256i convertFromString(std::string& rStr) +static m256i convertFromString(std::string& rStr) { m256i value; std::stringstream ss(rStr); @@ -74,7 +87,7 @@ m256i convertFromString(std::string& rStr) return value; } -std::vector convertULLFromString(std::string& rStr) +static std::vector convertULLFromString(std::string& rStr) { std::vector values; std::stringstream ss(rStr); diff --git a/tests.md b/tests.md new file mode 100644 index 000000000..4ec59cbe8 --- /dev/null +++ b/tests.md @@ -0,0 +1,22 @@ +## Compliance +- Result: Contract compliance check PASSED +- Tool: Qubic Contract Verification Tool (`qubic-contract-verify`) + +## Test Execution +- Command: `test.exe --gtest_filter=VottunBridge*` +- Result: 14 tests passed (0 failed) +- Tests (scope/expected behavior): + - `CreateOrder_RequiresFee`: rejects creation if the paid fee is below the required amount. + - `TransferToContract_RejectsMissingReward`: with pre-funded contract balance, a call without attached QUs fails and does not change balances or `lockedTokens`. + - `TransferToContract_AcceptsExactReward`: accepts when `invocationReward` matches `amount` and locks tokens. + - `TransferToContract_OrderNotFound`: rejects when `orderId` does not exist. + - `TransferToContract_InvalidAmountMismatch`: rejects when `input.amount` does not match the order amount. + - `TransferToContract_InvalidOrderState`: rejects when the order is not in the created state. + - `CreateOrder_CleansCompletedAndRefundedSlots`: cleans completed/refunded slots to allow a new order. + - `CreateProposal_CleansExecutedProposalsWhenFull`: cleans executed proposals to free slots and create a new one. + - `CreateProposal_InvalidTypeRejected`: rejects out-of-range proposal types. + - `ApproveProposal_NotOwnerRejected`: blocks approval when invocator is not a multisig admin. + - `ApproveProposal_DoubleApprovalRejected`: prevents double approval by the same admin. + - `ApproveProposal_ExecutesChangeThreshold`: executes the proposal once the threshold is reached. + - `ApproveProposal_ProposalNotFound`: rejects approval for a non-existent `proposalId`. + - `ApproveProposal_AlreadyExecuted`: rejects approval for a proposal already executed. \ No newline at end of file