From 38fc9afe41ac8331c1c395e7be22d3ddb79a5bf7 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 12 Oct 2023 10:55:54 -0700 Subject: [PATCH] Monorepo - add protocol rewards (#255) * added protocol-rewards * ignore other packages in coverage report * added protocol rewards coverage * rename coverage tasks * make project specific files to ignore * fix escape chars * try to wrap with single quoates * added protocol rewards to readme * fix protocol rewards url --- .github/workflows/coverage.yml | 6 +- .github/workflows/foundry.yml | 13 +- README.md | 1 + packages/1155-contracts/foundry.toml | 1 + packages/1155-contracts/package.json | 2 +- packages/protocol-rewards/_imagine/Enjoy.sol | 39 + packages/protocol-rewards/foundry.toml | 37 + packages/protocol-rewards/package.json | 26 + packages/protocol-rewards/remappings.txt | 2 + packages/protocol-rewards/script/Deploy.s.sol | 37 + .../protocol-rewards/script/ScriptBase.sol | 24 + .../protocol-rewards/src/ProtocolRewards.sol | 253 +++ .../src/abstract/ERC1155/ERC1155Rewards.sol | 40 + .../ERC1155/ERC1155RewardsStorageV1.sol | 8 + .../src/abstract/ERC721/ERC721Rewards.sol | 38 + .../ERC721/ERC721RewardsStorageV1.sol | 8 + .../src/abstract/RewardSplits.sol | 128 ++ .../src/interfaces/IProtocolRewards.sol | 119 ++ packages/protocol-rewards/src/lib/ECDSA.sol | 217 +++ packages/protocol-rewards/src/lib/EIP712.sol | 142 ++ .../protocol-rewards/src/lib/IERC5267.sol | 28 + packages/protocol-rewards/src/lib/Math.sol | 339 ++++ .../protocol-rewards/src/lib/ShortStrings.sol | 122 ++ .../protocol-rewards/src/lib/SignedMath.sol | 43 + .../protocol-rewards/src/lib/StorageSlot.sol | 138 ++ packages/protocol-rewards/src/lib/Strings.sol | 85 + .../test/ProtocolRewardsTest.sol | 34 + .../test/invariant/Handler.sol | 87 ++ .../invariant/ProtocolRewards.invariant.t.sol | 42 + .../protocol-rewards/test/unit/Deposit.t.sol | 130 ++ .../test/unit/ERC1155Rewards.t.sol | 139 ++ .../test/unit/ERC721Rewards.t.sol | 161 ++ .../protocol-rewards/test/unit/Withdraw.t.sol | 262 ++++ .../protocol-rewards/test/utils/ERC1155.sol | 897 +++++++++++ .../protocol-rewards/test/utils/ERC721.sol | 1364 +++++++++++++++++ .../protocol-rewards/test/utils/MockNFTs.sol | 73 + yarn.lock | 17 +- 37 files changed, 5093 insertions(+), 9 deletions(-) create mode 100644 packages/protocol-rewards/_imagine/Enjoy.sol create mode 100644 packages/protocol-rewards/foundry.toml create mode 100644 packages/protocol-rewards/package.json create mode 100644 packages/protocol-rewards/remappings.txt create mode 100644 packages/protocol-rewards/script/Deploy.s.sol create mode 100644 packages/protocol-rewards/script/ScriptBase.sol create mode 100644 packages/protocol-rewards/src/ProtocolRewards.sol create mode 100644 packages/protocol-rewards/src/abstract/ERC1155/ERC1155Rewards.sol create mode 100644 packages/protocol-rewards/src/abstract/ERC1155/ERC1155RewardsStorageV1.sol create mode 100644 packages/protocol-rewards/src/abstract/ERC721/ERC721Rewards.sol create mode 100644 packages/protocol-rewards/src/abstract/ERC721/ERC721RewardsStorageV1.sol create mode 100644 packages/protocol-rewards/src/abstract/RewardSplits.sol create mode 100644 packages/protocol-rewards/src/interfaces/IProtocolRewards.sol create mode 100644 packages/protocol-rewards/src/lib/ECDSA.sol create mode 100644 packages/protocol-rewards/src/lib/EIP712.sol create mode 100644 packages/protocol-rewards/src/lib/IERC5267.sol create mode 100644 packages/protocol-rewards/src/lib/Math.sol create mode 100644 packages/protocol-rewards/src/lib/ShortStrings.sol create mode 100644 packages/protocol-rewards/src/lib/SignedMath.sol create mode 100644 packages/protocol-rewards/src/lib/StorageSlot.sol create mode 100644 packages/protocol-rewards/src/lib/Strings.sol create mode 100644 packages/protocol-rewards/test/ProtocolRewardsTest.sol create mode 100644 packages/protocol-rewards/test/invariant/Handler.sol create mode 100644 packages/protocol-rewards/test/invariant/ProtocolRewards.invariant.t.sol create mode 100644 packages/protocol-rewards/test/unit/Deposit.t.sol create mode 100644 packages/protocol-rewards/test/unit/ERC1155Rewards.t.sol create mode 100644 packages/protocol-rewards/test/unit/ERC721Rewards.t.sol create mode 100644 packages/protocol-rewards/test/unit/Withdraw.t.sol create mode 100644 packages/protocol-rewards/test/utils/ERC1155.sol create mode 100644 packages/protocol-rewards/test/utils/ERC721.sol create mode 100644 packages/protocol-rewards/test/utils/MockNFTs.sol diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 09747bc24..966b7ba79 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,6 +4,10 @@ on: package: required: true type: string + files_to_ignore: + required: false + type: string + default: "" env: # Setting an environment variable with the value of a configuration variable @@ -35,7 +39,7 @@ jobs: run: | cd ./${{ env.package_folder }} && lcov --rc lcov_branch_coverage=1 \ --remove lcov.info \ - --output-file lcov.info "*node_modules*" "*test*" "*script*" "*DeploymentConfig*" "*Redeem*" "*deployment*" + --output-file lcov.info "*node_modules*" "*test*" "*script*" ${{ inputs.files_to_ignore }} - name: Report code coverage uses: zgosalvez/github-actions-report-lcov@v2 diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 5fc3aa6a9..de72e8234 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -144,6 +144,15 @@ jobs: coverage-1155: uses: ./.github/workflows/coverage.yml - name: "1155 contracts coverage" + name: "Test coverage - 1155" with: - package: "1155-contracts" \ No newline at end of file + package: "1155-contracts" + files_to_ignore: '"*DeploymentConfig*" "*Redeem*" "*deployment*" "*packages*"' + + coverage-protocol-rewards: + uses: ./.github/workflows/coverage.yml + name: "Test coverage - protocol rewards" + with: + package: "protocol-rewards" + files_to_ignore: '"*lib*"' + diff --git a/README.md b/README.md index 37065afa4..e9899aa14 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This repository is a monorepo for the Zora Protocol In it you will find: - [Zora 1155 Contracts](./packages/1155-contracts) +- [Protocol Rewards](./packages/protocol-rewards) ## Official docs diff --git a/packages/1155-contracts/foundry.toml b/packages/1155-contracts/foundry.toml index 53b2757a6..e69cfc703 100644 --- a/packages/1155-contracts/foundry.toml +++ b/packages/1155-contracts/foundry.toml @@ -1,6 +1,7 @@ [profile.default] fs_permissions = [{access = "read", path = "./addresses"}, {access = "read", path = "./chainConfigs"}, {access = "read", path = "./package.json"}, {access = "readwrite", path = "./deterministicConfig"}] libs = ['_imagine', 'node_modules', 'script'] +allow_paths = ["node_modules/@zoralabs/protocol-rewards"] optimizer = true optimizer_runs = 50 out = 'out' diff --git a/packages/1155-contracts/package.json b/packages/1155-contracts/package.json index 00ebd7154..023099258 100644 --- a/packages/1155-contracts/package.json +++ b/packages/1155-contracts/package.json @@ -43,7 +43,7 @@ "@openzeppelin/contracts": "4.9.2", "@wagmi/cli": "^1.0.1", "@zoralabs/openzeppelin-contracts-upgradeable": "4.8.4", - "@zoralabs/protocol-rewards": "1.2.1", + "@zoralabs/protocol-rewards": "*", "abitype": "^0.8.7", "ds-test": "https://github.com/dapphub/ds-test#cd98eff28324bfac652e63a239a60632a761790b", "es-main": "^1.2.0", diff --git a/packages/protocol-rewards/_imagine/Enjoy.sol b/packages/protocol-rewards/_imagine/Enjoy.sol new file mode 100644 index 000000000..a911014a9 --- /dev/null +++ b/packages/protocol-rewards/_imagine/Enjoy.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/* + + + + + + ░░░░░░░░░░░░░░ + ░░▒▒░░░░░░░░░░░░░░░░░░░░ + ░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░ + ░░▒▒▒▒░░░░░░░░░░░░░░ ░░░░░░░░ + ░▓▓▒▒▒▒░░░░░░░░░░░░ ░░░░░░░ + ░▓▓▓▒▒▒▒░░░░░░░░░░░░ ░░░░░░░░ + ░▓▓▓▒▒▒▒░░░░░░░░░░░░░░ ░░░░░░░░░░ + ░▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░▓▓▓▓▓▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░ + ░░▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ + ░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒░░░░░░░░░▒▒▒▒▒░░ + ░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░ + ░░▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░ + + OURS TRULY, + + + + + + + + + + + + */ + + interface Enjoy { } \ No newline at end of file diff --git a/packages/protocol-rewards/foundry.toml b/packages/protocol-rewards/foundry.toml new file mode 100644 index 000000000..62d40400e --- /dev/null +++ b/packages/protocol-rewards/foundry.toml @@ -0,0 +1,37 @@ +[profile.default] +auto_detect_solc = true +fs_permissions = [{access = "read", path = "./addresses"}, {access = "read", path = "./package.json"}] +fuzz_runs = 500 +libs = ['_imagine', 'node_modules', 'script'] +optimizer = true +optimizer_runs = 500000 +out = 'out' +script = 'script' +src = 'src' +via_ir = true + +[profile.optimized] +auto_detect_solc = true +optimizer = true +optimizer_runs = 500000 +out = 'out' +script = 'src' +src = 'src' +test = 'src' +via_ir = true + +[fmt] +bracket_spacing = true +func_attrs_with_params_multiline = true +int_types = "long" +line_length = 120 +quote_style = "double" +tab_width = 4 + +[invariant] +call_override = false +depth = 20 +fail_on_revert = true +runs = 1000 + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/packages/protocol-rewards/package.json b/packages/protocol-rewards/package.json new file mode 100644 index 000000000..77d1279aa --- /dev/null +++ b/packages/protocol-rewards/package.json @@ -0,0 +1,26 @@ +{ + "name": "@zoralabs/protocol-rewards", + "version": "1.2.1", + "repository": "https://github.com/ourzora/zora-protocol.git", + "license": "MIT", + "files": [ + "src/", + "_imagine" + ], + "scripts": { + "build": "forge build", + "build:sizes": "forge build --sizes", + "test": "forge test", + "build:contracts": "forge build", + "prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", + "prettier": "prettier --write 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'", + "lint": "yarn run prettier" + }, + "dependencies": { + "ds-test": "https://github.com/dapphub/ds-test#cd98eff28324bfac652e63a239a60632a761790b", + "forge-std": "https://github.com/foundry-rs/forge-std#705263c95892a906d7af65f0f73ce8a4a0c80b80" + }, + "devDependencies": { + "@types/node": "^20.3.2" + } +} diff --git a/packages/protocol-rewards/remappings.txt b/packages/protocol-rewards/remappings.txt new file mode 100644 index 000000000..50319f624 --- /dev/null +++ b/packages/protocol-rewards/remappings.txt @@ -0,0 +1,2 @@ +ds-test/=node_modules/ds-test/src/ +forge-std/=node_modules/forge-std/src/ \ No newline at end of file diff --git a/packages/protocol-rewards/script/Deploy.s.sol b/packages/protocol-rewards/script/Deploy.s.sol new file mode 100644 index 000000000..8d8abf6e1 --- /dev/null +++ b/packages/protocol-rewards/script/Deploy.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "./ScriptBase.sol"; + +import {console2} from "forge-std/console2.sol"; + +import {ProtocolRewards} from "../src/ProtocolRewards.sol"; + +contract DeployScript is ScriptBase { + function run() public { + vm.startBroadcast(deployer); + + // ProtocolRewards protocolRewards = new ProtocolRewards(); + + bytes memory creationCode = type(ProtocolRewards).creationCode; + + bytes32 salt = bytes32(0x0000000000000000000000000000000000000000668d7f9eb18e35000dbaaa0f); + + console2.log("creation code hash"); + bytes32 creationCodeHash = keccak256(creationCode); + console2.logBytes32(creationCodeHash); + + // Assert to ensure bytecode has not changed + assert(bytes32(0xfa8c14fa41eb1f11f85062d699fe173e7ae3c1e988f0fa4c1846ac7948b6c471) == creationCodeHash); + + // Sanity check for address + assert(IMMUTABLE_CREATE2_FACTORY.findCreate2Address(salt, creationCode) == address(0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B)); + + address result = IMMUTABLE_CREATE2_FACTORY.safeCreate2(salt, creationCode); + + console2.log("PROTOCOL REWARDS DEPLOYED:"); + console2.logAddress(address(result)); + + vm.stopBroadcast(); + } +} diff --git a/packages/protocol-rewards/script/ScriptBase.sol b/packages/protocol-rewards/script/ScriptBase.sol new file mode 100644 index 000000000..6daa6f229 --- /dev/null +++ b/packages/protocol-rewards/script/ScriptBase.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Script} from "forge-std/Script.sol"; + +interface ImmutableCreate2Factory { + function findCreate2Address(bytes32 salt, bytes memory initCode) external view returns (address deploymentAddress); + + function findCreate2AddressViaHash(bytes32 salt, bytes32 initCodeHash) external view returns (address deploymentAddress); + + function hasBeenDeployed(address deploymentAddress) external view returns (bool); + + function safeCreate2(bytes32 salt, bytes memory initializationCode) external payable returns (address deploymentAddress); +} + +contract ScriptBase is Script { + address deployer; + + ImmutableCreate2Factory constant IMMUTABLE_CREATE2_FACTORY = ImmutableCreate2Factory(0x0000000000FFe8B47B3e2130213B802212439497); + + function setUp() public { + deployer = vm.envAddress("deployer"); + } +} diff --git a/packages/protocol-rewards/src/ProtocolRewards.sol b/packages/protocol-rewards/src/ProtocolRewards.sol new file mode 100644 index 000000000..ca2b1c2bb --- /dev/null +++ b/packages/protocol-rewards/src/ProtocolRewards.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {Enjoy} from "../_imagine/Enjoy.sol"; +import {EIP712} from "./lib/EIP712.sol"; +import {IProtocolRewards} from "./interfaces/IProtocolRewards.sol"; + +/// @title ProtocolRewards +/// @notice Manager of deposits & withdrawals for protocol rewards +contract ProtocolRewards is Enjoy, IProtocolRewards, EIP712 { + /// @notice The EIP-712 typehash for gasless withdraws + bytes32 public constant WITHDRAW_TYPEHASH = keccak256("Withdraw(address from,address to,uint256 amount,uint256 nonce,uint256 deadline)"); + + /// @notice An account's balance + mapping(address => uint256) public balanceOf; + + /// @notice An account's nonce for gasless withdraws + mapping(address => uint256) public nonces; + + constructor() payable EIP712("ProtocolRewards", "1") {} + + /// @notice The total amount of ETH held in the contract + function totalSupply() external view returns (uint256) { + return address(this).balance; + } + + /// @notice Generic function to deposit ETH for a recipient, with an optional comment + /// @param to Address to deposit to + /// @param to Reason system reason for deposit (used for indexing) + /// @param comment Optional comment as reason for deposit + function deposit(address to, bytes4 reason, string calldata comment) external payable { + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + balanceOf[to] += msg.value; + + emit Deposit(msg.sender, to, reason, msg.value, comment); + } + + /// @notice Generic function to deposit ETH for multiple recipients, with an optional comment + /// @param recipients recipients to send the amount to, array aligns with amounts + /// @param amounts amounts to send to each recipient, array aligns with recipients + /// @param reasons optional bytes4 hash for indexing + /// @param comment Optional comment to include with mint + function depositBatch(address[] calldata recipients, uint256[] calldata amounts, bytes4[] calldata reasons, string calldata comment) external payable { + uint256 numRecipients = recipients.length; + + if (numRecipients != amounts.length || numRecipients != reasons.length) { + revert ARRAY_LENGTH_MISMATCH(); + } + + uint256 expectedTotalValue; + + for (uint256 i; i < numRecipients; ) { + expectedTotalValue += amounts[i]; + + unchecked { + ++i; + } + } + + if (msg.value != expectedTotalValue) { + revert INVALID_DEPOSIT(); + } + + address currentRecipient; + uint256 currentAmount; + + for (uint256 i; i < numRecipients; ) { + currentRecipient = recipients[i]; + currentAmount = amounts[i]; + + if (currentRecipient == address(0)) { + revert ADDRESS_ZERO(); + } + + balanceOf[currentRecipient] += currentAmount; + + emit Deposit(msg.sender, currentRecipient, reasons[i], currentAmount, comment); + + unchecked { + ++i; + } + } + } + + /// @notice Used by Zora ERC-721 & ERC-1155 contracts to deposit protocol rewards + /// @param creator Creator for NFT rewards + /// @param creatorReward Creator reward amount + /// @param createReferral Creator referral + /// @param createReferralReward Creator referral reward + /// @param mintReferral Mint referral user + /// @param mintReferralReward Mint referral amount + /// @param firstMinter First minter reward + /// @param firstMinterReward First minter reward amount + /// @param zora ZORA recipient + /// @param zoraReward ZORA amount + function depositRewards( + address creator, + uint256 creatorReward, + address createReferral, + uint256 createReferralReward, + address mintReferral, + uint256 mintReferralReward, + address firstMinter, + uint256 firstMinterReward, + address zora, + uint256 zoraReward + ) external payable { + if (msg.value != (creatorReward + createReferralReward + mintReferralReward + firstMinterReward + zoraReward)) { + revert INVALID_DEPOSIT(); + } + + unchecked { + if (creator != address(0)) { + balanceOf[creator] += creatorReward; + } + if (createReferral != address(0)) { + balanceOf[createReferral] += createReferralReward; + } + if (mintReferral != address(0)) { + balanceOf[mintReferral] += mintReferralReward; + } + if (firstMinter != address(0)) { + balanceOf[firstMinter] += firstMinterReward; + } + if (zora != address(0)) { + balanceOf[zora] += zoraReward; + } + } + + emit RewardsDeposit( + creator, + createReferral, + mintReferral, + firstMinter, + zora, + msg.sender, + creatorReward, + createReferralReward, + mintReferralReward, + firstMinterReward, + zoraReward + ); + } + + /// @notice Withdraw protocol rewards + /// @param to Withdraws from msg.sender to this address + /// @param amount Amount to withdraw (0 for total balance) + function withdraw(address to, uint256 amount) external { + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + address owner = msg.sender; + + if (amount > balanceOf[owner]) { + revert INVALID_WITHDRAW(); + } + + if (amount == 0) { + amount = balanceOf[owner]; + } + + balanceOf[owner] -= amount; + + emit Withdraw(owner, to, amount); + + (bool success, ) = to.call{value: amount}(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } + + /// @notice Withdraw rewards on behalf of an address + /// @param to The address to withdraw for + /// @param amount The amount to withdraw (0 for total balance) + function withdrawFor(address to, uint256 amount) external { + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + if (amount > balanceOf[to]) { + revert INVALID_WITHDRAW(); + } + + if (amount == 0) { + amount = balanceOf[to]; + } + + balanceOf[to] -= amount; + + emit Withdraw(to, to, amount); + + (bool success, ) = to.call{value: amount}(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } + + /// @notice Execute a withdraw of protocol rewards via signature + /// @param from Withdraw from this address + /// @param to Withdraw to this address + /// @param amount Amount to withdraw (0 for total balance) + /// @param deadline Deadline for the signature to be valid + /// @param v V component of signature + /// @param r R component of signature + /// @param s S component of signature + function withdrawWithSig(address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { + if (block.timestamp > deadline) { + revert SIGNATURE_DEADLINE_EXPIRED(); + } + + bytes32 withdrawHash; + + unchecked { + withdrawHash = keccak256(abi.encode(WITHDRAW_TYPEHASH, from, to, amount, nonces[from]++, deadline)); + } + + bytes32 digest = _hashTypedDataV4(withdrawHash); + + address recoveredAddress = ecrecover(digest, v, r, s); + + if (recoveredAddress == address(0) || recoveredAddress != from) { + revert INVALID_SIGNATURE(); + } + + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + if (amount > balanceOf[from]) { + revert INVALID_WITHDRAW(); + } + + if (amount == 0) { + amount = balanceOf[from]; + } + + balanceOf[from] -= amount; + + emit Withdraw(from, to, amount); + + (bool success, ) = to.call{value: amount}(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } +} diff --git a/packages/protocol-rewards/src/abstract/ERC1155/ERC1155Rewards.sol b/packages/protocol-rewards/src/abstract/ERC1155/ERC1155Rewards.sol new file mode 100644 index 000000000..422f1c14c --- /dev/null +++ b/packages/protocol-rewards/src/abstract/ERC1155/ERC1155Rewards.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {RewardSplits} from "../RewardSplits.sol"; + +/// @notice The base logic for handling Zora ERC-1155 protocol rewards +/// @dev Used in https://github.com/ourzora/zora-1155-contracts/blob/main/src/nft/ZoraCreator1155Impl.sol +abstract contract ERC1155Rewards is RewardSplits { + constructor(address _protocolRewards, address _zoraRewardRecipient) payable RewardSplits(_protocolRewards, _zoraRewardRecipient) {} + + function _handleRewardsAndGetValueSent( + uint256 msgValue, + uint256 numTokens, + address creator, + address createReferral, + address mintReferral, + address firstMinter + ) internal returns (uint256) { + uint256 totalReward = computeTotalReward(numTokens); + + // If we have no first minter, first minter should be the creator. + if (firstMinter == address(0)) { + firstMinter = creator; + } + + if (msgValue < totalReward) { + revert INVALID_ETH_AMOUNT(); + } else if (msgValue == totalReward) { + _depositFreeMintRewards(totalReward, numTokens, creator, createReferral, mintReferral, firstMinter); + + return 0; + } else { + _depositPaidMintRewards(totalReward, numTokens, createReferral, mintReferral, firstMinter); + + unchecked { + return msgValue - totalReward; + } + } + } +} diff --git a/packages/protocol-rewards/src/abstract/ERC1155/ERC1155RewardsStorageV1.sol b/packages/protocol-rewards/src/abstract/ERC1155/ERC1155RewardsStorageV1.sol new file mode 100644 index 000000000..4fbdbc61b --- /dev/null +++ b/packages/protocol-rewards/src/abstract/ERC1155/ERC1155RewardsStorageV1.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract ERC1155RewardsStorageV1 { + mapping(uint256 => address) public createReferrals; + + mapping(uint256 => address) public firstMinters; +} diff --git a/packages/protocol-rewards/src/abstract/ERC721/ERC721Rewards.sol b/packages/protocol-rewards/src/abstract/ERC721/ERC721Rewards.sol new file mode 100644 index 000000000..f35591200 --- /dev/null +++ b/packages/protocol-rewards/src/abstract/ERC721/ERC721Rewards.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {RewardSplits} from "../RewardSplits.sol"; + +/// @notice The base logic for handling Zora ERC-721 protocol rewards +/// @dev Used in https://github.com/ourzora/zora-drops-contracts/blob/main/src/ERC721Drop.sol +abstract contract ERC721Rewards is RewardSplits { + constructor(address _protocolRewards, address _zoraRewardRecipient) payable RewardSplits(_protocolRewards, _zoraRewardRecipient) {} + + function _handleRewards( + uint256 msgValue, + uint256 numTokens, + uint256 salePrice, + address creator, + address createReferral, + address mintReferral, + address firstMinter + ) internal { + uint256 totalReward = computeTotalReward(numTokens); + + if (salePrice == 0) { + if (msgValue != totalReward) { + revert INVALID_ETH_AMOUNT(); + } + + _depositFreeMintRewards(totalReward, numTokens, creator, createReferral, mintReferral, firstMinter); + } else { + uint256 totalSale = numTokens * salePrice; + + if (msgValue != (totalReward + totalSale)) { + revert INVALID_ETH_AMOUNT(); + } + + _depositPaidMintRewards(totalReward, numTokens, createReferral, mintReferral, firstMinter); + } + } +} diff --git a/packages/protocol-rewards/src/abstract/ERC721/ERC721RewardsStorageV1.sol b/packages/protocol-rewards/src/abstract/ERC721/ERC721RewardsStorageV1.sol new file mode 100644 index 000000000..4136dd69b --- /dev/null +++ b/packages/protocol-rewards/src/abstract/ERC721/ERC721RewardsStorageV1.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +abstract contract ERC721RewardsStorageV1 { + address public createReferral; + + address public firstMinter; +} diff --git a/packages/protocol-rewards/src/abstract/RewardSplits.sol b/packages/protocol-rewards/src/abstract/RewardSplits.sol new file mode 100644 index 000000000..514f7ee3f --- /dev/null +++ b/packages/protocol-rewards/src/abstract/RewardSplits.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {IProtocolRewards} from "../interfaces/IProtocolRewards.sol"; + +struct RewardsSettings { + uint256 creatorReward; + uint256 createReferralReward; + uint256 mintReferralReward; + uint256 firstMinterReward; + uint256 zoraReward; +} + +/// @notice Common logic for between Zora ERC-721 & ERC-1155 contracts for protocol reward splits & deposits +abstract contract RewardSplits { + error CREATOR_FUNDS_RECIPIENT_NOT_SET(); + error INVALID_ADDRESS_ZERO(); + error INVALID_ETH_AMOUNT(); + error ONLY_CREATE_REFERRAL(); + + uint256 internal constant TOTAL_REWARD_PER_MINT = 0.000777 ether; + + uint256 internal constant CREATOR_REWARD = 0.000333 ether; + uint256 internal constant FIRST_MINTER_REWARD = 0.000111 ether; + + uint256 internal constant CREATE_REFERRAL_FREE_MINT_REWARD = 0.000111 ether; + uint256 internal constant MINT_REFERRAL_FREE_MINT_REWARD = 0.000111 ether; + uint256 internal constant ZORA_FREE_MINT_REWARD = 0.000111 ether; + + uint256 internal constant MINT_REFERRAL_PAID_MINT_REWARD = 0.000222 ether; + uint256 internal constant CREATE_REFERRAL_PAID_MINT_REWARD = 0.000222 ether; + uint256 internal constant ZORA_PAID_MINT_REWARD = 0.000222 ether; + + address internal immutable zoraRewardRecipient; + IProtocolRewards internal immutable protocolRewards; + + constructor(address _protocolRewards, address _zoraRewardRecipient) payable { + if (_protocolRewards == address(0) || _zoraRewardRecipient == address(0)) { + revert INVALID_ADDRESS_ZERO(); + } + + protocolRewards = IProtocolRewards(_protocolRewards); + zoraRewardRecipient = _zoraRewardRecipient; + } + + function computeTotalReward(uint256 numTokens) public pure returns (uint256) { + return numTokens * TOTAL_REWARD_PER_MINT; + } + + function computeFreeMintRewards(uint256 numTokens) public pure returns (RewardsSettings memory) { + return + RewardsSettings({ + creatorReward: numTokens * CREATOR_REWARD, + createReferralReward: numTokens * CREATE_REFERRAL_FREE_MINT_REWARD, + mintReferralReward: numTokens * MINT_REFERRAL_FREE_MINT_REWARD, + firstMinterReward: numTokens * FIRST_MINTER_REWARD, + zoraReward: numTokens * ZORA_FREE_MINT_REWARD + }); + } + + function computePaidMintRewards(uint256 numTokens) public pure returns (RewardsSettings memory) { + return + RewardsSettings({ + creatorReward: 0, + createReferralReward: numTokens * CREATE_REFERRAL_PAID_MINT_REWARD, + mintReferralReward: numTokens * MINT_REFERRAL_PAID_MINT_REWARD, + firstMinterReward: numTokens * FIRST_MINTER_REWARD, + zoraReward: numTokens * ZORA_PAID_MINT_REWARD + }); + } + + function _depositFreeMintRewards( + uint256 totalReward, + uint256 numTokens, + address creator, + address createReferral, + address mintReferral, + address firstMinter + ) internal { + RewardsSettings memory settings = computeFreeMintRewards(numTokens); + + if (createReferral == address(0)) { + createReferral = zoraRewardRecipient; + } + + if (mintReferral == address(0)) { + mintReferral = zoraRewardRecipient; + } + + protocolRewards.depositRewards{value: totalReward}( + creator, + settings.creatorReward, + createReferral, + settings.createReferralReward, + mintReferral, + settings.mintReferralReward, + firstMinter, + settings.firstMinterReward, + zoraRewardRecipient, + settings.zoraReward + ); + } + + function _depositPaidMintRewards(uint256 totalReward, uint256 numTokens, address createReferral, address mintReferral, address firstMinter) internal { + RewardsSettings memory settings = computePaidMintRewards(numTokens); + + if (createReferral == address(0)) { + createReferral = zoraRewardRecipient; + } + + if (mintReferral == address(0)) { + mintReferral = zoraRewardRecipient; + } + + protocolRewards.depositRewards{value: totalReward}( + address(0), + 0, + createReferral, + settings.createReferralReward, + mintReferral, + settings.mintReferralReward, + firstMinter, + settings.firstMinterReward, + zoraRewardRecipient, + settings.zoraReward + ); + } +} diff --git a/packages/protocol-rewards/src/interfaces/IProtocolRewards.sol b/packages/protocol-rewards/src/interfaces/IProtocolRewards.sol new file mode 100644 index 000000000..c68ba9afe --- /dev/null +++ b/packages/protocol-rewards/src/interfaces/IProtocolRewards.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/// @title IProtocolRewards +/// @notice The interface for deposits & withdrawals for Protocol Rewards +interface IProtocolRewards { + /// @notice Rewards Deposit Event + /// @param creator Creator for NFT rewards + /// @param createReferral Creator referral + /// @param mintReferral Mint referral user + /// @param firstMinter First minter reward recipient + /// @param zora ZORA recipient + /// @param from The caller of the deposit + /// @param creatorReward Creator reward amount + /// @param createReferralReward Creator referral reward + /// @param mintReferralReward Mint referral amount + /// @param firstMinterReward First minter reward amount + /// @param zoraReward ZORA amount + event RewardsDeposit( + address indexed creator, + address indexed createReferral, + address indexed mintReferral, + address firstMinter, + address zora, + address from, + uint256 creatorReward, + uint256 createReferralReward, + uint256 mintReferralReward, + uint256 firstMinterReward, + uint256 zoraReward + ); + + /// @notice Deposit Event + /// @param from From user + /// @param to To user (within contract) + /// @param reason Optional bytes4 reason for indexing + /// @param amount Amount of deposit + /// @param comment Optional user comment + event Deposit(address indexed from, address indexed to, bytes4 indexed reason, uint256 amount, string comment); + + /// @notice Withdraw Event + /// @param from From user + /// @param to To user (within contract) + /// @param amount Amount of deposit + event Withdraw(address indexed from, address indexed to, uint256 amount); + + /// @notice Cannot send to address zero + error ADDRESS_ZERO(); + + /// @notice Function argument array length mismatch + error ARRAY_LENGTH_MISMATCH(); + + /// @notice Invalid deposit + error INVALID_DEPOSIT(); + + /// @notice Invalid signature for deposit + error INVALID_SIGNATURE(); + + /// @notice Invalid withdraw + error INVALID_WITHDRAW(); + + /// @notice Signature for withdraw is too old and has expired + error SIGNATURE_DEADLINE_EXPIRED(); + + /// @notice Low-level ETH transfer has failed + error TRANSFER_FAILED(); + + /// @notice Generic function to deposit ETH for a recipient, with an optional comment + /// @param to Address to deposit to + /// @param to Reason system reason for deposit (used for indexing) + /// @param comment Optional comment as reason for deposit + function deposit(address to, bytes4 why, string calldata comment) external payable; + + /// @notice Generic function to deposit ETH for multiple recipients, with an optional comment + /// @param recipients recipients to send the amount to, array aligns with amounts + /// @param amounts amounts to send to each recipient, array aligns with recipients + /// @param reasons optional bytes4 hash for indexing + /// @param comment Optional comment to include with mint + function depositBatch(address[] calldata recipients, uint256[] calldata amounts, bytes4[] calldata reasons, string calldata comment) external payable; + + /// @notice Used by Zora ERC-721 & ERC-1155 contracts to deposit protocol rewards + /// @param creator Creator for NFT rewards + /// @param creatorReward Creator reward amount + /// @param createReferral Creator referral + /// @param createReferralReward Creator referral reward + /// @param mintReferral Mint referral user + /// @param mintReferralReward Mint referral amount + /// @param firstMinter First minter reward + /// @param firstMinterReward First minter reward amount + /// @param zora ZORA recipient + /// @param zoraReward ZORA amount + function depositRewards( + address creator, + uint256 creatorReward, + address createReferral, + uint256 createReferralReward, + address mintReferral, + uint256 mintReferralReward, + address firstMinter, + uint256 firstMinterReward, + address zora, + uint256 zoraReward + ) external payable; + + /// @notice Withdraw protocol rewards + /// @param to Withdraws from msg.sender to this address + /// @param amount amount to withdraw + function withdraw(address to, uint256 amount) external; + + /// @notice Execute a withdraw of protocol rewards via signature + /// @param from Withdraw from this address + /// @param to Withdraw to this address + /// @param amount Amount to withdraw + /// @param deadline Deadline for the signature to be valid + /// @param v V component of signature + /// @param r R component of signature + /// @param s S component of signature + function withdrawWithSig(address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; +} diff --git a/packages/protocol-rewards/src/lib/ECDSA.sol b/packages/protocol-rewards/src/lib/ECDSA.sol new file mode 100644 index 000000000..9d0eecc79 --- /dev/null +++ b/packages/protocol-rewards/src/lib/ECDSA.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.8; + +import "./Strings.sol"; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS, + InvalidSignatureV // Deprecated in v4.8 + } + + function _throwError(RecoverError error) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert("ECDSA: invalid signature"); + } else if (error == RecoverError.InvalidSignatureLength) { + revert("ECDSA: invalid signature length"); + } else if (error == RecoverError.InvalidSignatureS) { + revert("ECDSA: invalid signature 's' value"); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature` or error string. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, signature); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + * + * _Available since v4.2._ + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, r, vs); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature); + } + + return (signer, RecoverError.NoError); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, v, r, s); + _throwError(error); + return recovered; + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") + mstore(0x1c, hash) + message := keccak256(0x00, 0x3c) + } + } + + /** + * @dev Returns an Ethereum Signed Message, created from `s`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s)); + } + + /** + * @dev Returns an Ethereum Signed Typed Data, created from a + * `domainSeparator` and a `structHash`. This produces hash corresponding + * to the one signed with the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] + * JSON-RPC method as part of EIP-712. + * + * See {recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, "\x19\x01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + data := keccak256(ptr, 0x42) + } + } + + /** + * @dev Returns an Ethereum Signed Data with intended validator, created from a + * `validator` and `data` according to the version 0 of EIP-191. + * + * See {recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x00", validator, data)); + } +} diff --git a/packages/protocol-rewards/src/lib/EIP712.sol b/packages/protocol-rewards/src/lib/EIP712.sol new file mode 100644 index 000000000..fc46fae66 --- /dev/null +++ b/packages/protocol-rewards/src/lib/EIP712.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/EIP712.sol) + +pragma solidity ^0.8.8; + +import "./ECDSA.sol"; +import "./IERC5267.sol"; +import "./ShortStrings.sol"; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible, + * thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding + * they need in their contracts using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain + * separator of the implementation contract. This will cause the `_domainSeparatorV4` function to always rebuild the + * separator from the immutable values, which is cheaper than accessing a cached version in cold storage. + * + * _Available since v3.4._ + * + * @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment + */ +abstract contract EIP712 is IERC5267 { + using ShortStrings for *; + + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; + + bytes32 private immutable _hashedName; + bytes32 private immutable _hashedVersion; + + ShortString private immutable _name; + ShortString private immutable _version; + string private _nameFallback; + string private _versionFallback; + + /** + * @dev Initializes the domain separator and parameter caches. + * + * The meaning of `name` and `version` is specified in + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: + * + * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. + * - `version`: the current major version of the signing domain. + * + * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart + * contract upgrade]. + */ + constructor(string memory name, string memory version) { + _name = name.toShortStringWithFallback(_nameFallback); + _version = version.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(name)); + _hashedVersion = keccak256(bytes(version)); + + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev See {EIP-5267}. + * + * _Available since v4.9._ + */ + function eip712Domain() + public + view + virtual + override + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _name.toStringWithFallback(_nameFallback), + _version.toStringWithFallback(_versionFallback), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } +} diff --git a/packages/protocol-rewards/src/lib/IERC5267.sol b/packages/protocol-rewards/src/lib/IERC5267.sol new file mode 100644 index 000000000..c76d11373 --- /dev/null +++ b/packages/protocol-rewards/src/lib/IERC5267.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5267.sol) + +pragma solidity ^0.8.8; + +interface IERC5267 { + /** + * @dev MAY be emitted to signal that the domain could have changed. + */ + event EIP712DomainChanged(); + + /** + * @dev returns the fields and values that describe the domain separator used by this contract for EIP-712 + * signature. + */ + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); +} diff --git a/packages/protocol-rewards/src/lib/Math.sol b/packages/protocol-rewards/src/lib/Math.sol new file mode 100644 index 000000000..3134948e7 --- /dev/null +++ b/packages/protocol-rewards/src/lib/Math.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) + +pragma solidity ^0.8.8; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + enum Rounding { + Down, // Toward negative infinity + Up, // Toward infinity + Zero // Toward zero + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds up instead + * of rounding down. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b - 1) / b can overflow on addition, so we distribute. + return a == 0 ? 0 : (a - 1) / b + 1; + } + + /** + * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) + * with further edits by Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use + // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2^256 + prod0. + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod0 := mul(x, y) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return prod0 / denominator; + } + + // Make sure the result is less than 2^256. Also prevents denominator == 0. + require(denominator > prod1, "Math: mulDiv overflow"); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1. + // See https://cs.stackexchange.com/q/138556/92363. + + // Does not overflow because the denominator cannot be zero at this stage in the function. + uint256 twos = denominator & (~denominator + 1); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such + // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv = 1 mod 2^4. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works + // in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2^8 + inverse *= 2 - denominator * inverse; // inverse mod 2^16 + inverse *= 2 - denominator * inverse; // inverse mod 2^32 + inverse *= 2 - denominator * inverse; // inverse mod 2^64 + inverse *= 2 - denominator * inverse; // inverse mod 2^128 + inverse *= 2 - denominator * inverse; // inverse mod 2^256 + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is + // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inverse; + return result; + } + } + + /** + * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + uint256 result = mulDiv(x, y, denominator); + if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) { + result += 1; + } + return result; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down. + * + * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). + */ + function sqrt(uint256 a) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. + // + // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have + // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. + // + // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` + // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` + // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` + // + // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. + uint256 result = 1 << (log2(a) >> 1); + + // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision + // into the expected uint128 result. + unchecked { + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + return min(result, a / result); + } + } + + /** + * @notice Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + (rounding == Rounding.Up && result * result < a ? 1 : 0); + } + } + + /** + * @dev Return the log in base 2, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 128; + } + if (value >> 64 > 0) { + value >>= 64; + result += 64; + } + if (value >> 32 > 0) { + value >>= 32; + result += 32; + } + if (value >> 16 > 0) { + value >>= 16; + result += 16; + } + if (value >> 8 > 0) { + value >>= 8; + result += 8; + } + if (value >> 4 > 0) { + value >>= 4; + result += 4; + } + if (value >> 2 > 0) { + value >>= 2; + result += 2; + } + if (value >> 1 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 10, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10 ** 64) { + value /= 10 ** 64; + result += 64; + } + if (value >= 10 ** 32) { + value /= 10 ** 32; + result += 32; + } + if (value >= 10 ** 16) { + value /= 10 ** 16; + result += 16; + } + if (value >= 10 ** 8) { + value /= 10 ** 8; + result += 8; + } + if (value >= 10 ** 4) { + value /= 10 ** 4; + result += 4; + } + if (value >= 10 ** 2) { + value /= 10 ** 2; + result += 2; + } + if (value >= 10 ** 1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 256, rounded down, of a positive value. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 16; + } + if (value >> 64 > 0) { + value >>= 64; + result += 8; + } + if (value >> 32 > 0) { + value >>= 32; + result += 4; + } + if (value >> 16 > 0) { + value >>= 16; + result += 2; + } + if (value >> 8 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 256, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0); + } + } +} diff --git a/packages/protocol-rewards/src/lib/ShortStrings.sol b/packages/protocol-rewards/src/lib/ShortStrings.sol new file mode 100644 index 000000000..90a054999 --- /dev/null +++ b/packages/protocol-rewards/src/lib/ShortStrings.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/ShortStrings.sol) + +pragma solidity ^0.8.8; + +import "./StorageSlot.sol"; + +// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | +// | length | 0x BB | +type ShortString is bytes32; + +/** + * @dev This library provides functions to convert short memory strings + * into a `ShortString` type that can be used as an immutable variable. + * + * Strings of arbitrary length can be optimized using this library if + * they are short enough (up to 31 bytes) by packing them with their + * length (1 byte) in a single EVM word (32 bytes). Additionally, a + * fallback mechanism can be used for every other case. + * + * Usage example: + * + * ```solidity + * contract Named { + * using ShortStrings for *; + * + * ShortString private immutable _name; + * string private _nameFallback; + * + * constructor(string memory contractName) { + * _name = contractName.toShortStringWithFallback(_nameFallback); + * } + * + * function name() external view returns (string memory) { + * return _name.toStringWithFallback(_nameFallback); + * } + * } + * ``` + */ +library ShortStrings { + // Used as an identifier for strings longer than 31 bytes. + bytes32 private constant _FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF; + + error StringTooLong(string str); + error InvalidShortString(); + + /** + * @dev Encode a string of at most 31 chars into a `ShortString`. + * + * This will trigger a `StringTooLong` error is the input string is too long. + */ + function toShortString(string memory str) internal pure returns (ShortString) { + bytes memory bstr = bytes(str); + if (bstr.length > 31) { + revert StringTooLong(str); + } + return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length)); + } + + /** + * @dev Decode a `ShortString` back to a "normal" string. + */ + function toString(ShortString sstr) internal pure returns (string memory) { + uint256 len = byteLength(sstr); + // using `new string(len)` would work locally but is not memory safe. + string memory str = new string(32); + /// @solidity memory-safe-assembly + assembly { + mstore(str, len) + mstore(add(str, 0x20), sstr) + } + return str; + } + + /** + * @dev Return the length of a `ShortString`. + */ + function byteLength(ShortString sstr) internal pure returns (uint256) { + uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; + if (result > 31) { + revert InvalidShortString(); + } + return result; + } + + /** + * @dev Encode a string into a `ShortString`, or write it to storage if it is too long. + */ + function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) { + if (bytes(value).length < 32) { + return toShortString(value); + } else { + StorageSlot.getStringSlot(store).value = value; + return ShortString.wrap(_FALLBACK_SENTINEL); + } + } + + /** + * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + */ + function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { + if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) { + return toString(value); + } else { + return store; + } + } + + /** + * @dev Return the length of a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + * + * WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of + * actual characters as the UTF-8 encoding of a single character can span over multiple bytes. + */ + function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) { + if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) { + return byteLength(value); + } else { + return bytes(store).length; + } + } +} diff --git a/packages/protocol-rewards/src/lib/SignedMath.sol b/packages/protocol-rewards/src/lib/SignedMath.sol new file mode 100644 index 000000000..b1940c777 --- /dev/null +++ b/packages/protocol-rewards/src/lib/SignedMath.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol) + +pragma solidity ^0.8.8; + +/** + * @dev Standard signed math utilities missing in the Solidity language. + */ +library SignedMath { + /** + * @dev Returns the largest of two signed numbers. + */ + function max(int256 a, int256 b) internal pure returns (int256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two signed numbers. + */ + function min(int256 a, int256 b) internal pure returns (int256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two signed numbers without overflow. + * The result is rounded towards zero. + */ + function average(int256 a, int256 b) internal pure returns (int256) { + // Formula from the book "Hacker's Delight" + int256 x = (a & b) + ((a ^ b) >> 1); + return x + (int256(uint256(x) >> 255) & (a ^ b)); + } + + /** + * @dev Returns the absolute unsigned value of a signed value. + */ + function abs(int256 n) internal pure returns (uint256) { + unchecked { + // must be unchecked in order to support `n = type(int256).min` + return uint256(n >= 0 ? n : -n); + } + } +} diff --git a/packages/protocol-rewards/src/lib/StorageSlot.sol b/packages/protocol-rewards/src/lib/StorageSlot.sol new file mode 100644 index 000000000..0dd1c0573 --- /dev/null +++ b/packages/protocol-rewards/src/lib/StorageSlot.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +pragma solidity ^0.8.8; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * ```solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + * + * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ + * _Available since v4.9 for `string`, `bytes`._ + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } +} diff --git a/packages/protocol-rewards/src/lib/Strings.sol b/packages/protocol-rewards/src/lib/Strings.sol new file mode 100644 index 000000000..f0e909111 --- /dev/null +++ b/packages/protocol-rewards/src/lib/Strings.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) + +pragma solidity ^0.8.8; + +import "./Math.sol"; +import "./SignedMath.sol"; + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant _SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + unchecked { + uint256 length = Math.log10(value) + 1; + string memory buffer = new string(length); + uint256 ptr; + /// @solidity memory-safe-assembly + assembly { + ptr := add(buffer, add(32, length)) + } + while (true) { + ptr--; + /// @solidity memory-safe-assembly + assembly { + mstore8(ptr, byte(mod(value, 10), _SYMBOLS)) + } + value /= 10; + if (value == 0) break; + } + return buffer; + } + } + + /** + * @dev Converts a `int256` to its ASCII `string` decimal representation. + */ + function toString(int256 value) internal pure returns (string memory) { + return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value)))); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + unchecked { + return toHexString(value, Math.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } + + /** + * @dev Returns true if the two strings are equal. + */ + function equal(string memory a, string memory b) internal pure returns (bool) { + return keccak256(bytes(a)) == keccak256(bytes(b)); + } +} diff --git a/packages/protocol-rewards/test/ProtocolRewardsTest.sol b/packages/protocol-rewards/test/ProtocolRewardsTest.sol new file mode 100644 index 000000000..1e1ddcdc2 --- /dev/null +++ b/packages/protocol-rewards/test/ProtocolRewardsTest.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; + +import "../src/ProtocolRewards.sol"; + +import "./utils/MockNFTs.sol"; + +contract ProtocolRewardsTest is Test { + uint256 internal constant ETH_SUPPLY = 120_200_000 ether; + + ProtocolRewards internal protocolRewards; + + address internal collector; + address internal creator; + address internal createReferral; + address internal mintReferral; + address internal firstMinter; + address internal zora; + + function setUp() public virtual { + protocolRewards = new ProtocolRewards(); + + vm.label(address(protocolRewards), "protocolRewards"); + + collector = makeAddr("collector"); + creator = makeAddr("creator"); + createReferral = makeAddr("createReferral"); + mintReferral = makeAddr("mintReferral"); + firstMinter = makeAddr("firstMinter"); + zora = makeAddr("zora"); + } +} diff --git a/packages/protocol-rewards/test/invariant/Handler.sol b/packages/protocol-rewards/test/invariant/Handler.sol new file mode 100644 index 000000000..4c57eb0f7 --- /dev/null +++ b/packages/protocol-rewards/test/invariant/Handler.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "forge-std/Base.sol"; + +import "../ProtocolRewardsTest.sol"; + +contract Handler is CommonBase, StdCheats, StdUtils { + uint256 internal constant ETH_SUPPLY = 120_200_000 ether; + ProtocolRewards internal immutable rewards; + + uint256 public ghost_depositSum; + uint256 public ghost_withdrawSum; + + address internal currentActor; + uint256 public numActors; + mapping(uint256 => address) public actors; + + constructor(ProtocolRewards _rewards) { + rewards = _rewards; + + vm.deal(address(this), ETH_SUPPLY); + } + + modifier validateActor(address actor) { + if (actor == address(0) || actor <= address(0x9)) { + return; + } + + _; + } + + modifier createActor(address actor) { + currentActor = msg.sender; + + actors[++numActors] = currentActor; + + _; + } + + modifier useActor(uint256 actorSeed) { + if (numActors == 0) { + return; + } + + currentActor = actors[(actorSeed % numActors) + 1]; + + _; + } + + modifier validateWithdraw() { + if (rewards.balanceOf(currentActor) == 0) { + return; + } + + _; + } + + function deposit(uint256 amount) public validateActor(msg.sender) createActor(msg.sender) { + amount = bound(amount, 0, address(this).balance); + + (bool success, ) = currentActor.call{value: amount}(""); + if (!success) { + return; + } + + vm.prank(currentActor); + rewards.deposit{value: amount}(currentActor, "", ""); + + ghost_depositSum += amount; + } + + function withdraw(uint256 actorSeed, uint256 amount) public validateActor(msg.sender) useActor(actorSeed) validateWithdraw { + amount = bound(amount, 0, rewards.balanceOf(currentActor)); + + amount == 0 ? ghost_withdrawSum += rewards.balanceOf(currentActor) : ghost_withdrawSum += amount; + + vm.prank(currentActor); + rewards.withdraw(currentActor, amount); + } + + function forEachActor(function(address) external func) public { + for (uint256 i = 1; i < numActors; ++i) { + func(actors[i]); + } + } +} diff --git a/packages/protocol-rewards/test/invariant/ProtocolRewards.invariant.t.sol b/packages/protocol-rewards/test/invariant/ProtocolRewards.invariant.t.sol new file mode 100644 index 000000000..1d59791f9 --- /dev/null +++ b/packages/protocol-rewards/test/invariant/ProtocolRewards.invariant.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../ProtocolRewardsTest.sol"; +import "./Handler.sol"; + +contract ProtocolRewardsInvariantTest is ProtocolRewardsTest { + Handler internal handler; + + function setUp() public override { + super.setUp(); + + handler = new Handler(protocolRewards); + + vm.label(address(handler), "HANDLER"); + + targetContract(address(handler)); + + bytes4[] memory targetSelectors = new bytes4[](2); + + targetSelectors[0] = Handler.deposit.selector; + targetSelectors[1] = Handler.withdraw.selector; + + targetSelector(FuzzSelector({addr: address(handler), selectors: targetSelectors})); + + excludeSender(address(handler)); + excludeSender(address(protocolRewards)); + excludeSender(address(this)); + } + + function invariant_TotalSupplyMatchesTotalDeposits() public { + assertEq(protocolRewards.totalSupply(), handler.ghost_depositSum() - handler.ghost_withdrawSum()); + } + + function invariant_UserBalanceCannotExceedTotalSupply() public { + handler.forEachActor(this.ensureActorBalanceDoesNotExceedTotalSupply); + } + + function ensureActorBalanceDoesNotExceedTotalSupply(address actor) external { + assertLe(protocolRewards.balanceOf(actor), protocolRewards.totalSupply()); + } +} diff --git a/packages/protocol-rewards/test/unit/Deposit.t.sol b/packages/protocol-rewards/test/unit/Deposit.t.sol new file mode 100644 index 000000000..b9cb2eb6f --- /dev/null +++ b/packages/protocol-rewards/test/unit/Deposit.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../ProtocolRewardsTest.sol"; +import "../../src/abstract/RewardSplits.sol"; + +contract DepositTest is ProtocolRewardsTest { + function setUp() public override { + super.setUp(); + } + + function testDeposit(uint256 amount, address to) public { + vm.assume(amount < ETH_SUPPLY); + vm.assume(to != address(0)); + + vm.deal(collector, amount); + + vm.prank(collector); + protocolRewards.deposit{value: amount}(to, bytes4(0), "test"); + + assertEq(protocolRewards.balanceOf(to), amount); + } + + function testRevert_CannotDepositToAddressZero(uint256 amount) public { + vm.assume(amount < ETH_SUPPLY); + + vm.deal(collector, amount); + + vm.prank(collector); + vm.expectRevert(abi.encodeWithSignature("ADDRESS_ZERO()")); + protocolRewards.deposit{value: amount}(address(0), bytes4(0), "test"); + } + + function testDepositBatch(uint8 numRecipients) public { + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + bytes4[] memory reasons = new bytes4[](numRecipients); + + uint256 totalValue; + + for (uint256 i; i < numRecipients; ++i) { + recipients[i] = makeAddr(vm.toString(i + 1)); + amounts[i] = i + 1 ether; + + totalValue += amounts[i]; + } + + vm.deal(collector, totalValue); + vm.prank(collector); + protocolRewards.depositBatch{value: totalValue}(recipients, amounts, reasons, "test"); + + for (uint256 i; i < numRecipients; ++i) { + assertEq(protocolRewards.balanceOf(recipients[i]), amounts[i]); + } + } + + function testRevert_RecipientsAndAmountsLengthMismatch(uint8 numRecipients, uint8 numAmounts) public { + vm.assume(numRecipients != numAmounts); + + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numAmounts); + bytes4[] memory reasons = new bytes4[](numAmounts); + + uint256 totalValue; + + for (uint256 i; i < numAmounts; ++i) { + amounts[i] = i + 1 ether; + + totalValue += amounts[i]; + } + + for (uint256 i; i < numRecipients; ++i) { + recipients[i] = makeAddr(vm.toString(i + 1)); + } + + vm.deal(collector, totalValue); + + vm.prank(collector); + vm.expectRevert(abi.encodeWithSignature("ARRAY_LENGTH_MISMATCH()")); + protocolRewards.depositBatch{value: totalValue}(recipients, amounts, reasons, "test"); + } + + function testRevert_InvalidDepositMsgValue(uint8 numRecipients) public { + vm.assume(numRecipients > 0); + + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + bytes4[] memory reasons = new bytes4[](numRecipients); + + uint256 totalValue; + + for (uint256 i; i < numRecipients; ++i) { + recipients[i] = makeAddr(vm.toString(i + 1)); + amounts[i] = i + 1 ether; + + totalValue += amounts[i]; + } + + vm.deal(collector, totalValue); + + vm.prank(collector); + vm.expectRevert(abi.encodeWithSignature("INVALID_DEPOSIT()")); + protocolRewards.depositBatch{value: 0}(recipients, amounts, reasons, "test"); + } + + function testRevert_RecipientCannotBeAddressZero(uint8 numRecipients) public { + vm.assume(numRecipients > 0); + + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + bytes4[] memory reasons = new bytes4[](numRecipients); + + uint256 totalValue; + + for (uint256 i; i < numRecipients; ++i) { + recipients[i] = makeAddr(vm.toString(i + 1)); + amounts[i] = i + 1 ether; + + totalValue += amounts[i]; + } + + recipients[0] = address(0); + + vm.deal(collector, totalValue); + + vm.prank(collector); + vm.expectRevert(abi.encodeWithSignature("ADDRESS_ZERO()")); + protocolRewards.depositBatch{value: totalValue}(recipients, amounts, reasons, "test"); + } +} diff --git a/packages/protocol-rewards/test/unit/ERC1155Rewards.t.sol b/packages/protocol-rewards/test/unit/ERC1155Rewards.t.sol new file mode 100644 index 000000000..cbbeca67a --- /dev/null +++ b/packages/protocol-rewards/test/unit/ERC1155Rewards.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../ProtocolRewardsTest.sol"; +import {RewardsSettings} from "../../src/abstract/RewardSplits.sol"; + +contract ERC1155RewardsTest is ProtocolRewardsTest { + MockERC1155 internal mockERC1155; + + function setUp() public override { + super.setUp(); + + mockERC1155 = new MockERC1155(creator, createReferral, address(protocolRewards), zora); + + vm.label(address(mockERC1155), "MOCK_ERC1155"); + } + + function test1155FreeMintDeposit(uint16 numTokens) public { + vm.assume(numTokens > 0); + + uint256 totalReward = mockERC1155.computeTotalReward(numTokens); + + vm.deal(collector, totalReward); + + vm.prank(collector); + mockERC1155.mintWithRewards{value: totalReward}(collector, 0, numTokens, mintReferral); + + RewardsSettings memory settings = mockERC1155.computeFreeMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(creator), settings.creatorReward); + assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); + assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward); + } + + function test1155PaidMintDeposit(uint16 numTokens, uint256 pricePerToken) public { + vm.assume(numTokens > 0); + vm.assume(pricePerToken > 0 && pricePerToken < 100 ether); + + mockERC1155.setSalePrice(pricePerToken); + + uint256 totalReward = mockERC1155.computeTotalReward(numTokens); + uint256 totalSale = numTokens * pricePerToken; + uint256 totalValue = totalReward + totalSale; + + vm.deal(collector, totalValue); + + vm.prank(collector); + mockERC1155.mintWithRewards{value: totalValue}(collector, 0, numTokens, mintReferral); + + RewardsSettings memory settings = mockERC1155.computePaidMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); + assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward); + } + + function test1155FreeMintNullReferralRecipients(uint16 numTokens) public { + vm.assume(numTokens > 0); + + mockERC1155 = new MockERC1155(creator, address(0), address(protocolRewards), zora); + + uint256 totalReward = mockERC1155.computeTotalReward(numTokens); + + vm.deal(collector, totalReward); + + vm.prank(collector); + mockERC1155.mintWithRewards{value: totalReward}(collector, 0, numTokens, address(0)); + + RewardsSettings memory settings = mockERC1155.computeFreeMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(creator), settings.creatorReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward + settings.createReferralReward); + } + + function test1155PaidMintNullReferralRecipient(uint16 numTokens, uint256 pricePerToken) public { + vm.assume(numTokens > 0); + vm.assume(pricePerToken > 0 && pricePerToken < 100 ether); + + mockERC1155 = new MockERC1155(creator, address(0), address(protocolRewards), zora); + + mockERC1155.setSalePrice(pricePerToken); + + uint256 totalReward = mockERC1155.computeTotalReward(numTokens); + uint256 totalSale = numTokens * pricePerToken; + uint256 totalValue = totalReward + totalSale; + + vm.deal(collector, totalValue); + + vm.prank(collector); + mockERC1155.mintWithRewards{value: totalValue}(collector, 0, numTokens, address(0)); + + RewardsSettings memory settings = mockERC1155.computePaidMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward + settings.createReferralReward); + } + + function testRevert1155FreeMintInvalidEth(uint16 numTokens) public { + vm.assume(numTokens > 0); + + vm.expectRevert(abi.encodeWithSignature("INVALID_ETH_AMOUNT()")); + mockERC1155.mintWithRewards(collector, 0, numTokens, mintReferral); + } + + function testRevert1155PaidMintInvalidEth(uint16 numTokens, uint256 pricePerToken) public { + vm.assume(numTokens > 0); + vm.assume(pricePerToken > 0 && pricePerToken < 100 ether); + + mockERC1155.setSalePrice(pricePerToken); + + vm.expectRevert(abi.encodeWithSignature("INVALID_ETH_AMOUNT()")); + mockERC1155.mintWithRewards(collector, 0, numTokens, mintReferral); + } + + function testRevert1155PaidMintInvalidEthRemaining(uint16 numTokens, uint256 pricePerToken) public { + vm.assume(numTokens > 0); + vm.assume(pricePerToken > 0 && pricePerToken < 100 ether); + + mockERC1155.setSalePrice(pricePerToken); + + uint256 totalReward = mockERC1155.computeTotalReward(numTokens); + uint256 totalSale = numTokens * pricePerToken; + uint256 totalValue = totalReward + totalSale; + + vm.deal(collector, totalValue); + + vm.prank(collector); + vm.expectRevert(abi.encodeWithSignature("MOCK_ERC1155_INVALID_REMAINING_VALUE()")); + mockERC1155.mintWithRewards{value: totalValue - 1}(collector, 0, numTokens, mintReferral); + } +} diff --git a/packages/protocol-rewards/test/unit/ERC721Rewards.t.sol b/packages/protocol-rewards/test/unit/ERC721Rewards.t.sol new file mode 100644 index 000000000..204b49336 --- /dev/null +++ b/packages/protocol-rewards/test/unit/ERC721Rewards.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../ProtocolRewardsTest.sol"; +import {RewardsSettings} from "../../src/abstract/RewardSplits.sol"; + +contract ERC721RewardsTest is ProtocolRewardsTest { + MockERC721 internal mockERC721; + + function setUp() public override { + super.setUp(); + + mockERC721 = new MockERC721(creator, createReferral, address(protocolRewards), zora); + + vm.label(address(mockERC721), "MOCK_ERC721"); + } + + function testValidateFreeMintTotalComputation(uint16 numTokens) public { + uint256 expectedTotal = mockERC721.computeTotalReward(numTokens); + + RewardsSettings memory settings = mockERC721.computeFreeMintRewards(numTokens); + + uint256 actualTotal = settings.creatorReward + + settings.createReferralReward + + settings.mintReferralReward + + settings.firstMinterReward + + settings.zoraReward; + + assertEq(expectedTotal, actualTotal); + } + + function testValidatePaidMintTotalComputation(uint32 numTokens) public { + uint256 expectedTotal = mockERC721.computeTotalReward(numTokens); + + RewardsSettings memory settings = mockERC721.computePaidMintRewards(numTokens); + + uint256 actualTotal = settings.mintReferralReward + settings.createReferralReward + settings.firstMinterReward + settings.zoraReward; + + assertEq(expectedTotal, actualTotal); + } + + function test721FreeMintDeposit(uint16 numTokens) public { + vm.assume(numTokens > 0 && numTokens < 10_000); + + uint256 totalReward = mockERC721.computeTotalReward(numTokens); + + vm.deal(collector, totalReward); + + vm.prank(collector); + mockERC721.mintWithRewards{value: totalReward}(collector, numTokens, mintReferral); + + RewardsSettings memory settings = mockERC721.computeFreeMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(creator), settings.creatorReward); + assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); + assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward); + } + + function test721PaidMintDeposit(uint16 numTokens, uint256 pricePerToken) public { + vm.assume(numTokens > 0 && numTokens < 10_000); + vm.assume(pricePerToken > 0 && pricePerToken < 100 ether); + + mockERC721.setSalePrice(pricePerToken); + + uint256 totalReward = mockERC721.computeTotalReward(numTokens); + uint256 totalSale = numTokens * pricePerToken; + uint256 totalValue = totalReward + totalSale; + + vm.deal(collector, totalValue); + + vm.prank(collector); + mockERC721.mintWithRewards{value: totalValue}(collector, numTokens, mintReferral); + + RewardsSettings memory settings = mockERC721.computePaidMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); + assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward); + } + + function test721FreeMintNullReferralRecipients(uint16 numTokens) public { + vm.assume(numTokens > 0 && numTokens < 10_000); + + mockERC721 = new MockERC721(creator, address(0), address(protocolRewards), zora); + + uint256 totalReward = mockERC721.computeTotalReward(numTokens); + + vm.deal(collector, totalReward); + + vm.prank(collector); + mockERC721.mintWithRewards{value: totalReward}(collector, numTokens, address(0)); + + RewardsSettings memory settings = mockERC721.computeFreeMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(creator), settings.creatorReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward + settings.createReferralReward); + } + + function test721PaidMintNullReferralRecipient(uint16 numTokens, uint256 pricePerToken) public { + vm.assume(numTokens > 0 && numTokens < 10_000); + vm.assume(pricePerToken > 0 && pricePerToken < 100 ether); + + mockERC721 = new MockERC721(creator, address(0), address(protocolRewards), zora); + + mockERC721.setSalePrice(pricePerToken); + + uint256 totalReward = mockERC721.computeTotalReward(numTokens); + uint256 totalSale = numTokens * pricePerToken; + uint256 totalValue = totalReward + totalSale; + + vm.deal(collector, totalValue); + + vm.prank(collector); + mockERC721.mintWithRewards{value: totalValue}(collector, numTokens, address(0)); + + RewardsSettings memory settings = mockERC721.computePaidMintRewards(numTokens); + + assertEq(protocolRewards.totalSupply(), totalReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward + settings.createReferralReward); + } + + function testSet721CreatorFundsRecipientAsContractIfNotSet(uint16 numTokens) public { + vm.assume(numTokens > 0); + + mockERC721 = new MockERC721(address(0), createReferral, address(protocolRewards), zora); + + uint256 totalValue = mockERC721.computeTotalReward(numTokens); + + RewardsSettings memory settings = mockERC721.computeFreeMintRewards(numTokens); + + mockERC721.mintWithRewards{value: totalValue}(collector, numTokens, mintReferral); + + assertEq(protocolRewards.balanceOf(address(mockERC721)), settings.creatorReward); + assertEq(protocolRewards.balanceOf(collector), settings.firstMinterReward); + } + + function testRevert721FreeMintInvalidEth(uint16 numTokens) public { + vm.assume(numTokens > 0); + + vm.expectRevert(abi.encodeWithSignature("INVALID_ETH_AMOUNT()")); + mockERC721.mintWithRewards(collector, numTokens, mintReferral); + } + + function testRevert721PaidMintInvalidEth(uint16 numTokens, uint256 pricePerToken) public { + vm.assume(numTokens > 0); + vm.assume(pricePerToken > 0 && pricePerToken < 100 ether); + + mockERC721.setSalePrice(pricePerToken); + + vm.expectRevert(abi.encodeWithSignature("INVALID_ETH_AMOUNT()")); + mockERC721.mintWithRewards(collector, numTokens, mintReferral); + } +} diff --git a/packages/protocol-rewards/test/unit/Withdraw.t.sol b/packages/protocol-rewards/test/unit/Withdraw.t.sol new file mode 100644 index 000000000..99c906ff9 --- /dev/null +++ b/packages/protocol-rewards/test/unit/Withdraw.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../ProtocolRewardsTest.sol"; +import "../../src/abstract/RewardSplits.sol"; + +contract WithdrawTest is ProtocolRewardsTest { + function setUp() public override { + super.setUp(); + + vm.deal(collector, 10 ether); + + vm.prank(collector); + protocolRewards.deposit{value: 10 ether}(creator, "", ""); + } + + function getDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes("ProtocolRewards")), + keccak256(bytes("1")), + block.chainid, + address(protocolRewards) + ) + ); + } + + function testWithdraw() public { + uint256 beforeCreatorBalance = creator.balance; + uint256 beforeTotalSupply = protocolRewards.totalSupply(); + + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + vm.prank(creator); + protocolRewards.withdraw(creator, creatorRewardsBalance); + + assertEq(creator.balance, beforeCreatorBalance + creatorRewardsBalance); + assertEq(protocolRewards.totalSupply(), beforeTotalSupply - creatorRewardsBalance); + } + + function testWithdrawFullBalance() public { + uint256 beforeCreatorBalance = creator.balance; + uint256 beforeTotalSupply = protocolRewards.totalSupply(); + + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + vm.prank(creator); + protocolRewards.withdraw(creator, 0); + + assertEq(creator.balance, beforeCreatorBalance + creatorRewardsBalance); + assertEq(protocolRewards.totalSupply(), beforeTotalSupply - creatorRewardsBalance); + } + + function testRevert_InvalidWithdrawToAddress() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + vm.expectRevert(abi.encodeWithSignature("ADDRESS_ZERO()")); + vm.prank(creator); + protocolRewards.withdraw(address(0), creatorRewardsBalance); + } + + function testRevert_WithdrawInvalidAmount() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + vm.expectRevert(abi.encodeWithSignature("INVALID_WITHDRAW()")); + vm.prank(creator); + protocolRewards.withdraw(creator, creatorRewardsBalance + 1); + } + + function testWithdrawFor() public { + uint256 beforeCreatorBalance = creator.balance; + uint256 beforeTotalSupply = protocolRewards.totalSupply(); + + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + protocolRewards.withdrawFor(creator, creatorRewardsBalance); + + assertEq(creator.balance, beforeCreatorBalance + creatorRewardsBalance); + assertEq(protocolRewards.totalSupply(), beforeTotalSupply - creatorRewardsBalance); + } + + function testWithdrawForFullBalance() public { + uint256 beforeCreatorBalance = creator.balance; + uint256 beforeTotalSupply = protocolRewards.totalSupply(); + + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + protocolRewards.withdrawFor(creator, 0); + + assertEq(creator.balance, beforeCreatorBalance + creatorRewardsBalance); + assertEq(protocolRewards.totalSupply(), beforeTotalSupply - creatorRewardsBalance); + } + + function testRevert_WithdrawForInvalidAmount() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + vm.expectRevert(abi.encodeWithSignature("INVALID_WITHDRAW()")); + protocolRewards.withdrawFor(creator, creatorRewardsBalance + 1); + } + + function testRevert_WithdrawForInvalidToAddress() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + vm.expectRevert(abi.encodeWithSignature("ADDRESS_ZERO()")); + protocolRewards.withdrawFor(address(0), creatorRewardsBalance); + } + + function testWithdrawWithSig() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + (, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); + + uint256 nonce = protocolRewards.nonces(creator); + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, creator, creatorRewardsBalance, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + uint256 beforeCreatorBalance = creator.balance; + uint256 beforeTotalSupply = protocolRewards.totalSupply(); + + protocolRewards.withdrawWithSig(creator, creator, creatorRewardsBalance, deadline, v, r, s); + + assertEq(creator.balance, beforeCreatorBalance + creatorRewardsBalance); + assertEq(protocolRewards.totalSupply(), beforeTotalSupply - creatorRewardsBalance); + } + + function testWithdrawWithSigFullBalance() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + (, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); + + uint256 nonce = protocolRewards.nonces(creator); + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, creator, 0, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + uint256 beforeCreatorBalance = creator.balance; + uint256 beforeTotalSupply = protocolRewards.totalSupply(); + + protocolRewards.withdrawWithSig(creator, creator, 0, deadline, v, r, s); + + assertEq(creator.balance, beforeCreatorBalance + creatorRewardsBalance); + assertEq(protocolRewards.totalSupply(), beforeTotalSupply - creatorRewardsBalance); + } + + function testRevert_SigExpired() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + (, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); + + uint256 nonce = protocolRewards.nonces(creator); + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, creator, creatorRewardsBalance, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + vm.warp(deadline + 1); + + vm.expectRevert(abi.encodeWithSignature("SIGNATURE_DEADLINE_EXPIRED()")); + protocolRewards.withdrawWithSig(creator, creator, creatorRewardsBalance, deadline, v, r, s); + } + + function testRevert_InvalidWithdrawWithSigToAddress() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + + (, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); + + uint256 nonce = protocolRewards.nonces(creator); + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, address(0), creatorRewardsBalance, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + vm.expectRevert(abi.encodeWithSignature("ADDRESS_ZERO()")); + protocolRewards.withdrawWithSig(creator, address(0), creatorRewardsBalance, deadline, v, r, s); + } + + function testRevert_InvalidNonce() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + (, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); + + uint256 nonce = protocolRewards.nonces(creator) + 1; + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, creator, creatorRewardsBalance, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); + protocolRewards.withdrawWithSig(creator, creator, creatorRewardsBalance, deadline, v, r, s); + } + + function testRevert_InvalidSigner() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + (address notCreator, uint256 notCreatorPrivateKey) = makeAddrAndKey("notCreator"); + + uint256 nonce = protocolRewards.nonces(creator); + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, notCreator, creatorRewardsBalance, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(notCreatorPrivateKey, digest); + + vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); + protocolRewards.withdrawWithSig(creator, notCreator, creatorRewardsBalance, deadline, v, r, s); + } + + function testRevert_InvalidWithdrawAmount() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + (, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); + + uint256 nonce = protocolRewards.nonces(creator); + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, creator, creatorRewardsBalance + 1, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + vm.expectRevert(abi.encodeWithSignature("INVALID_WITHDRAW()")); + protocolRewards.withdrawWithSig(creator, creator, creatorRewardsBalance + 1, deadline, v, r, s); + } + + function testRevert_InvalidReplay() public { + uint256 creatorRewardsBalance = protocolRewards.balanceOf(creator); + (, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); + + uint256 nonce = protocolRewards.nonces(creator); + uint256 deadline = block.timestamp + 1 days; + + bytes32 withdrawHash = keccak256(abi.encode(protocolRewards.WITHDRAW_TYPEHASH(), creator, creator, creatorRewardsBalance, nonce, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", getDomainSeparator(), withdrawHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + + protocolRewards.withdrawWithSig(creator, creator, creatorRewardsBalance, deadline, v, r, s); + + vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); + protocolRewards.withdrawWithSig(creator, creator, creatorRewardsBalance, deadline, v, r, s); + } +} diff --git a/packages/protocol-rewards/test/utils/ERC1155.sol b/packages/protocol-rewards/test/utils/ERC1155.sol new file mode 100644 index 000000000..9eb8223ea --- /dev/null +++ b/packages/protocol-rewards/test/utils/ERC1155.sol @@ -0,0 +1,897 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/ERC1155.sol) + +pragma solidity ^0.8.0; + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/IERC1155.sol) + +// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +/** + * @dev Required interface of an ERC1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155 is IERC165 { + /** + * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + /** + * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all + * transfers. + */ + event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); + + /** + * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to + * `approved`. + */ + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + + /** + * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. + * + * If an {URI} event was emitted for `id`, the standard + * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value + * returned by {IERC1155MetadataURI-uri}. + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the amount of tokens of token type `id` owned by `account`. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) external view returns (uint256); + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); + + /** + * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, + * + * Emits an {ApprovalForAll} event. + * + * Requirements: + * + * - `operator` cannot be the caller. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address account, address operator) external view returns (bool); + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external; +} + +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) + +/** + * @dev _Available since v3.1._ + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev Handles the receipt of a single ERC1155 token type. This function is + * called at the end of a `safeTransferFrom` after the balance has been updated. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param operator The address which initiated the transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param id The ID of the token being transferred + * @param value The amount of tokens being transferred + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) external returns (bytes4); + + /** + * @dev Handles the receipt of a multiple ERC1155 token types. This function + * is called at the end of a `safeBatchTransferFrom` after the balances have + * been updated. + * + * NOTE: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param operator The address which initiated the batch transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param ids An array containing ids of each token being transferred (order and length must match values array) + * @param values An array containing amounts of each token being transferred (order and length must match ids array) + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} + +// OpenZeppelin Contracts v4.4.1 (token/ERC1155/extensions/IERC1155MetadataURI.sol) + +/** + * @dev Interface of the optional ERC1155MetadataExtension interface, as defined + * in the https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155MetadataURI is IERC1155 { + /** + * @dev Returns the URI for token type `id`. + * + * If the `\{id\}` substring is present in the URI, it must be replaced by + * clients with the actual token type ID. + */ + function uri(uint256 id) external view returns (string memory); +} + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * + * Furthermore, `isContract` will also return true if the target contract within + * the same transaction is already scheduled for destruction by `SELFDESTRUCT`, + * which only has an effect at the end of a transaction. + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success, ) = recipient.call{value: amount}(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling + * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * + * _Available since v4.8._ + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata, + string memory errorMessage + ) internal view returns (bytes memory) { + if (success) { + if (returndata.length == 0) { + // only check isContract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + require(isContract(target), "Address: call to non-contract"); + } + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + /** + * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason or using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + function _revert(bytes memory returndata, string memory errorMessage) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } +} + +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + * + * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. + */ +abstract contract ERC165 is IERC165 { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} + +/** + * @dev Implementation of the basic standard multi-token. + * See https://eips.ethereum.org/EIPS/eip-1155 + * Originally based on code by Enjin: https://github.com/enjin/erc-1155 + * + * _Available since v3.1._ + */ +contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { + using Address for address; + + // Mapping from token ID to account balances + mapping(uint256 => mapping(address => uint256)) private _balances; + + // Mapping from account to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json + string private _uri; + + /** + * @dev See {_setURI}. + */ + constructor(string memory uri_) { + _setURI(uri_); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1155).interfaceId || interfaceId == type(IERC1155MetadataURI).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC1155MetadataURI-uri}. + * + * This implementation returns the same URI for *all* token types. It relies + * on the token type ID substitution mechanism + * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. + * + * Clients calling this function must replace the `\{id\}` substring with the + * actual token type ID. + */ + function uri(uint256) public view virtual override returns (string memory) { + return _uri; + } + + /** + * @dev See {IERC1155-balanceOf}. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { + require(account != address(0), "ERC1155: address zero is not a valid owner"); + return _balances[id][account]; + } + + /** + * @dev See {IERC1155-balanceOfBatch}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) public view virtual override returns (uint256[] memory) { + require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); + + uint256[] memory batchBalances = new uint256[](accounts.length); + + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balanceOf(accounts[i], ids[i]); + } + + return batchBalances; + } + + /** + * @dev See {IERC1155-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + _setApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC1155-isApprovedForAll}. + */ + function isApprovedForAll(address account, address operator) public view virtual override returns (bool) { + return _operatorApprovals[account][operator]; + } + + /** + * @dev See {IERC1155-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) public virtual override { + require(from == _msgSender() || isApprovedForAll(from, _msgSender()), "ERC1155: caller is not token owner or approved"); + _safeTransferFrom(from, to, id, amount, data); + } + + /** + * @dev See {IERC1155-safeBatchTransferFrom}. + */ + function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) public virtual override { + require(from == _msgSender() || isApprovedForAll(from, _msgSender()), "ERC1155: caller is not token owner or approved"); + _safeBatchTransferFrom(from, to, ids, amounts, data); + } + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function _safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) internal virtual { + require(to != address(0), "ERC1155: transfer to the zero address"); + + address operator = _msgSender(); + uint256[] memory ids = _asSingletonArray(id); + uint256[] memory amounts = _asSingletonArray(amount); + + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + + emit TransferSingle(operator, from, to, id, amount); + + _afterTokenTransfer(operator, from, to, ids, amounts, data); + + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function _safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual { + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + require(to != address(0), "ERC1155: transfer to the zero address"); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + } + + emit TransferBatch(operator, from, to, ids, amounts); + + _afterTokenTransfer(operator, from, to, ids, amounts, data); + + _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data); + } + + /** + * @dev Sets a new URI for all token types, by relying on the token type ID + * substitution mechanism + * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. + * + * By this mechanism, any occurrence of the `\{id\}` substring in either the + * URI or any of the amounts in the JSON file at said URI will be replaced by + * clients with the token type ID. + * + * For example, the `https://token-cdn-domain/\{id\}.json` URI would be + * interpreted by clients as + * `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` + * for token type ID 0x4cce0. + * + * See {uri}. + * + * Because these URIs cannot be meaningfully represented by the {URI} event, + * this function emits no events. + */ + function _setURI(string memory newuri) internal virtual { + _uri = newuri; + } + + /** + * @dev Creates `amount` tokens of token type `id`, and assigns them to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function _mint(address to, uint256 id, uint256 amount, bytes memory data) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + + address operator = _msgSender(); + uint256[] memory ids = _asSingletonArray(id); + uint256[] memory amounts = _asSingletonArray(amount); + + _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); + + _balances[id][to] += amount; + emit TransferSingle(operator, address(0), to, id, amount); + + _afterTokenTransfer(operator, address(0), to, ids, amounts, data); + + _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_mint}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); + + for (uint256 i = 0; i < ids.length; i++) { + _balances[ids[i]][to] += amounts[i]; + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _afterTokenTransfer(operator, address(0), to, ids, amounts, data); + + _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data); + } + + /** + * @dev Destroys `amount` tokens of token type `id` from `from` + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `from` must have at least `amount` tokens of token type `id`. + */ + function _burn(address from, uint256 id, uint256 amount) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + + address operator = _msgSender(); + uint256[] memory ids = _asSingletonArray(id); + uint256[] memory amounts = _asSingletonArray(amount); + + _beforeTokenTransfer(operator, from, address(0), ids, amounts, ""); + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + + emit TransferSingle(operator, from, address(0), id, amount); + + _afterTokenTransfer(operator, from, address(0), ids, amounts, ""); + } + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_burn}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + */ + function _burnBatch(address from, uint256[] memory ids, uint256[] memory amounts) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + + address operator = _msgSender(); + + _beforeTokenTransfer(operator, from, address(0), ids, amounts, ""); + + for (uint256 i = 0; i < ids.length; i++) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + } + + emit TransferBatch(operator, from, address(0), ids, amounts); + + _afterTokenTransfer(operator, from, address(0), ids, amounts, ""); + } + + /** + * @dev Approve `operator` to operate on all of `owner` tokens + * + * Emits an {ApprovalForAll} event. + */ + function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { + require(owner != operator, "ERC1155: setting approval status for self"); + _operatorApprovals[owner][operator] = approved; + emit ApprovalForAll(owner, operator, approved); + } + + /** + * @dev Hook that is called before any token transfer. This includes minting + * and burning, as well as batched variants. + * + * The same hook is called on both single and batched variants. For single + * transfers, the length of the `ids` and `amounts` arrays will be 1. + * + * Calling conditions (for each `id` and `amount` pair): + * + * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * of token type `id` will be transferred to `to`. + * - When `from` is zero, `amount` tokens of token type `id` will be minted + * for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens of token type `id` + * will be burned. + * - `from` and `to` are never both zero. + * - `ids` and `amounts` have the same, non-zero length. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual {} + + /** + * @dev Hook that is called after any token transfer. This includes minting + * and burning, as well as batched variants. + * + * The same hook is called on both single and batched variants. For single + * transfers, the length of the `id` and `amount` arrays will be 1. + * + * Calling conditions (for each `id` and `amount` pair): + * + * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * of token type `id` will be transferred to `to`. + * - When `from` is zero, `amount` tokens of token type `id` will be minted + * for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens of token type `id` + * will be burned. + * - `from` and `to` are never both zero. + * - `ids` and `amounts` have the same, non-zero length. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual {} + + function _doSafeTransferAcceptanceCheck(address operator, address from, address to, uint256 id, uint256 amount, bytes memory data) private { + if (to.isContract()) { + try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non-ERC1155Receiver implementer"); + } + } + } + + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.isContract()) { + try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non-ERC1155Receiver implementer"); + } + } + } + + function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) { + uint256[] memory array = new uint256[](1); + array[0] = element; + + return array; + } +} diff --git a/packages/protocol-rewards/test/utils/ERC721.sol b/packages/protocol-rewards/test/utils/ERC721.sol new file mode 100644 index 000000000..775d09b48 --- /dev/null +++ b/packages/protocol-rewards/test/utils/ERC721.sol @@ -0,0 +1,1364 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/ERC721.sol) + +pragma solidity ^0.8.0; + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol) + +// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 is IERC165 { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721 + * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must + * understand this adds an external call which potentially creates a reentrancy vulnerability. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); +} + +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol) + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721Receiver { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + * + * The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); +} + +// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol) + +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721Metadata is IERC721 { + /** + * @dev Returns the token collection name. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the token collection symbol. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * + * Furthermore, `isContract` will also return true if the target contract within + * the same transaction is already scheduled for destruction by `SELFDESTRUCT`, + * which only has an effect at the end of a transaction. + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success, ) = recipient.call{value: amount}(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling + * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * + * _Available since v4.8._ + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata, + string memory errorMessage + ) internal view returns (bytes memory) { + if (success) { + if (returndata.length == 0) { + // only check isContract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + require(isContract(target), "Address: call to non-contract"); + } + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + /** + * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason or using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + function _revert(bytes memory returndata, string memory errorMessage) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } +} + +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) + +// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + enum Rounding { + Down, // Toward negative infinity + Up, // Toward infinity + Zero // Toward zero + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds up instead + * of rounding down. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b - 1) / b can overflow on addition, so we distribute. + return a == 0 ? 0 : (a - 1) / b + 1; + } + + /** + * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) + * with further edits by Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use + // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2^256 + prod0. + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod0 := mul(x, y) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return prod0 / denominator; + } + + // Make sure the result is less than 2^256. Also prevents denominator == 0. + require(denominator > prod1, "Math: mulDiv overflow"); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1. + // See https://cs.stackexchange.com/q/138556/92363. + + // Does not overflow because the denominator cannot be zero at this stage in the function. + uint256 twos = denominator & (~denominator + 1); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such + // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv = 1 mod 2^4. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works + // in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2^8 + inverse *= 2 - denominator * inverse; // inverse mod 2^16 + inverse *= 2 - denominator * inverse; // inverse mod 2^32 + inverse *= 2 - denominator * inverse; // inverse mod 2^64 + inverse *= 2 - denominator * inverse; // inverse mod 2^128 + inverse *= 2 - denominator * inverse; // inverse mod 2^256 + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is + // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inverse; + return result; + } + } + + /** + * @notice Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + uint256 result = mulDiv(x, y, denominator); + if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) { + result += 1; + } + return result; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down. + * + * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11). + */ + function sqrt(uint256 a) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target. + // + // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have + // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`. + // + // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)` + // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))` + // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)` + // + // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit. + uint256 result = 1 << (log2(a) >> 1); + + // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128, + // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at + // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision + // into the expected uint128 result. + unchecked { + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + result = (result + a / result) >> 1; + return min(result, a / result); + } + } + + /** + * @notice Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + (rounding == Rounding.Up && result * result < a ? 1 : 0); + } + } + + /** + * @dev Return the log in base 2, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 128; + } + if (value >> 64 > 0) { + value >>= 64; + result += 64; + } + if (value >> 32 > 0) { + value >>= 32; + result += 32; + } + if (value >> 16 > 0) { + value >>= 16; + result += 16; + } + if (value >> 8 > 0) { + value >>= 8; + result += 8; + } + if (value >> 4 > 0) { + value >>= 4; + result += 4; + } + if (value >> 2 > 0) { + value >>= 2; + result += 2; + } + if (value >> 1 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 10, rounded down, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10 ** 64) { + value /= 10 ** 64; + result += 64; + } + if (value >= 10 ** 32) { + value /= 10 ** 32; + result += 32; + } + if (value >= 10 ** 16) { + value /= 10 ** 16; + result += 16; + } + if (value >= 10 ** 8) { + value /= 10 ** 8; + result += 8; + } + if (value >= 10 ** 4) { + value /= 10 ** 4; + result += 4; + } + if (value >= 10 ** 2) { + value /= 10 ** 2; + result += 2; + } + if (value >= 10 ** 1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 256, rounded down, of a positive value. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >> 128 > 0) { + value >>= 128; + result += 16; + } + if (value >> 64 > 0) { + value >>= 64; + result += 8; + } + if (value >> 32 > 0) { + value >>= 32; + result += 4; + } + if (value >> 16 > 0) { + value >>= 16; + result += 2; + } + if (value >> 8 > 0) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 256, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0); + } + } +} + +// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol) + +/** + * @dev Standard signed math utilities missing in the Solidity language. + */ +library SignedMath { + /** + * @dev Returns the largest of two signed numbers. + */ + function max(int256 a, int256 b) internal pure returns (int256) { + return a > b ? a : b; + } + + /** + * @dev Returns the smallest of two signed numbers. + */ + function min(int256 a, int256 b) internal pure returns (int256) { + return a < b ? a : b; + } + + /** + * @dev Returns the average of two signed numbers without overflow. + * The result is rounded towards zero. + */ + function average(int256 a, int256 b) internal pure returns (int256) { + // Formula from the book "Hacker's Delight" + int256 x = (a & b) + ((a ^ b) >> 1); + return x + (int256(uint256(x) >> 255) & (a ^ b)); + } + + /** + * @dev Returns the absolute unsigned value of a signed value. + */ + function abs(int256 n) internal pure returns (uint256) { + unchecked { + // must be unchecked in order to support `n = type(int256).min` + return uint256(n >= 0 ? n : -n); + } + } +} + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant _SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + unchecked { + uint256 length = Math.log10(value) + 1; + string memory buffer = new string(length); + uint256 ptr; + /// @solidity memory-safe-assembly + assembly { + ptr := add(buffer, add(32, length)) + } + while (true) { + ptr--; + /// @solidity memory-safe-assembly + assembly { + mstore8(ptr, byte(mod(value, 10), _SYMBOLS)) + } + value /= 10; + if (value == 0) break; + } + return buffer; + } + } + + /** + * @dev Converts a `int256` to its ASCII `string` decimal representation. + */ + function toString(int256 value) internal pure returns (string memory) { + return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value)))); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + unchecked { + return toHexString(value, Math.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } + + /** + * @dev Returns true if the two strings are equal. + */ + function equal(string memory a, string memory b) internal pure returns (bool) { + return keccak256(bytes(a)) == keccak256(bytes(b)); + } +} + +// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + * + * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. + */ +abstract contract ERC165 is IERC165 { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata extension, but not including the Enumerable extension, which is available separately as + * {ERC721Enumerable}. + */ +contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { + using Address for address; + using Strings for uint256; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to owner address + mapping(uint256 => address) private _owners; + + // Mapping owner address to token count + mapping(address => uint256) private _balances; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC721Metadata).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view virtual override returns (uint256) { + require(owner != address(0), "ERC721: address zero is not a valid owner"); + return _balances[owner]; + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + address owner = _ownerOf(tokenId); + require(owner != address(0), "ERC721: invalid token ID"); + return owner; + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + _requireMinted(tokenId); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overridden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual override { + address owner = ERC721.ownerOf(tokenId); + require(to != owner, "ERC721: approval to current owner"); + + require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()), "ERC721: approve caller is not token owner or approved for all"); + + _approve(to, tokenId); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view virtual override returns (address) { + _requireMinted(tokenId); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + _setApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual override { + //solhint-disable-next-line max-line-length + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + + _transfer(from, to, tokenId); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { + require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + _safeTransfer(from, to, tokenId, data); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * `data` is additional data, it has no specified format and it is sent in call to `to`. + * + * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. + * implement alternative mechanisms to perform token transfer, such as signature-based. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual { + _transfer(from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + /** + * @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist + */ + function _ownerOf(uint256 tokenId) internal view virtual returns (address) { + return _owners[tokenId]; + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + * and stop existing when they are burned (`_burn`). + */ + function _exists(uint256 tokenId) internal view virtual returns (bool) { + return _ownerOf(tokenId) != address(0); + } + + /** + * @dev Returns whether `spender` is allowed to manage `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { + address owner = ERC721.ownerOf(tokenId); + return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual { + _mint(to, tokenId); + require(_checkOnERC721Received(address(0), to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 tokenId) internal virtual { + require(to != address(0), "ERC721: mint to the zero address"); + require(!_exists(tokenId), "ERC721: token already minted"); + + _beforeTokenTransfer(address(0), to, tokenId, 1); + + // Check that tokenId was not minted by `_beforeTokenTransfer` hook + require(!_exists(tokenId), "ERC721: token already minted"); + + unchecked { + // Will not overflow unless all 2**256 token ids are minted to the same owner. + // Given that tokens are minted one by one, it is impossible in practice that + // this ever happens. Might change if we allow batch minting. + // The ERC fails to describe this case. + _balances[to] += 1; + } + + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + + _afterTokenTransfer(address(0), to, tokenId, 1); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * This is an internal function that does not check if the sender is authorized to operate on the token. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal virtual { + address owner = ERC721.ownerOf(tokenId); + + _beforeTokenTransfer(owner, address(0), tokenId, 1); + + // Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook + owner = ERC721.ownerOf(tokenId); + + // Clear approvals + delete _tokenApprovals[tokenId]; + + unchecked { + // Cannot overflow, as that would require more tokens to be burned/transferred + // out than the owner initially received through minting and transferring in. + _balances[owner] -= 1; + } + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + + _afterTokenTransfer(owner, address(0), tokenId, 1); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) internal virtual { + require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + require(to != address(0), "ERC721: transfer to the zero address"); + + _beforeTokenTransfer(from, to, tokenId, 1); + + // Check that tokenId was not transferred by `_beforeTokenTransfer` hook + require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + + // Clear approvals from the previous owner + delete _tokenApprovals[tokenId]; + + unchecked { + // `_balances[from]` cannot overflow for the same reason as described in `_burn`: + // `from`'s balance is the number of token held, which is at least one before the current + // transfer. + // `_balances[to]` could overflow in the conditions described in `_mint`. That would require + // all 2**256 token ids to be minted, which in practice is impossible. + _balances[from] -= 1; + _balances[to] += 1; + } + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId, 1); + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits an {Approval} event. + */ + function _approve(address to, uint256 tokenId) internal virtual { + _tokenApprovals[tokenId] = to; + emit Approval(ERC721.ownerOf(tokenId), to, tokenId); + } + + /** + * @dev Approve `operator` to operate on all of `owner` tokens + * + * Emits an {ApprovalForAll} event. + */ + function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { + require(owner != operator, "ERC721: approve to caller"); + _operatorApprovals[owner][operator] = approved; + emit ApprovalForAll(owner, operator, approved); + } + + /** + * @dev Reverts if the `tokenId` has not been minted yet. + */ + function _requireMinted(uint256 tokenId) internal view virtual { + require(_exists(tokenId), "ERC721: invalid token ID"); + } + + /** + * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. + * The call is not executed if the target address is not a contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private returns (bool) { + if (to.isContract()) { + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) { + return retval == IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC721: transfer to non ERC721Receiver implementer"); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @dev Hook that is called before any token transfer. This includes minting and burning. If {ERC721Consecutive} is + * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s tokens will be transferred to `to`. + * - When `from` is zero, the tokens will be minted for `to`. + * - When `to` is zero, ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * - `batchSize` is non-zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {} + + /** + * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is + * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s tokens were transferred to `to`. + * - When `from` is zero, the tokens were minted for `to`. + * - When `to` is zero, ``from``'s tokens were burned. + * - `from` and `to` are never both zero. + * - `batchSize` is non-zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {} + + /** + * @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override. + * + * WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant + * being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such + * that `ownerOf(tokenId)` is `a`. + */ + // solhint-disable-next-line func-name-mixedcase + function __unsafe_increaseBalance(address account, uint256 amount) internal { + _balances[account] += amount; + } +} diff --git a/packages/protocol-rewards/test/utils/MockNFTs.sol b/packages/protocol-rewards/test/utils/MockNFTs.sol new file mode 100644 index 000000000..74e447d91 --- /dev/null +++ b/packages/protocol-rewards/test/utils/MockNFTs.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ERC721} from "./ERC721.sol"; +import {ERC1155} from "./ERC1155.sol"; + +import {ERC721RewardsStorageV1} from "../../src/abstract/ERC721/ERC721RewardsStorageV1.sol"; +import {ERC721Rewards} from "../../src/abstract/ERC721/ERC721Rewards.sol"; +import {ERC1155Rewards} from "../../src/abstract/ERC1155/ERC1155Rewards.sol"; +import {ERC1155RewardsStorageV1} from "../../src/abstract/ERC1155/ERC1155RewardsStorageV1.sol"; + +contract MockERC721 is ERC721, ERC721Rewards, ERC721RewardsStorageV1 { + address public creator; + uint256 public salePrice; + uint256 public currentTokenId; + + constructor( + address _creator, + address _createReferral, + address _protocolRewards, + address _zoraRewardRecipient + ) ERC721("Mock ERC721", "MOCK") ERC721Rewards(_protocolRewards, _zoraRewardRecipient) { + creator = _creator; + createReferral = _createReferral; + } + + function setSalePrice(uint256 _salePrice) external { + salePrice = _salePrice; + } + + function mintWithRewards(address to, uint256 numTokens, address mintReferral) external payable { + if (firstMinter == address(0)) firstMinter = to; + + _handleRewards(msg.value, numTokens, salePrice, creator != address(0) ? creator : address(this), createReferral, mintReferral, firstMinter); + + for (uint256 i; i < numTokens; ++i) { + _mint(to, currentTokenId++); + } + } +} + +contract MockERC1155 is ERC1155, ERC1155Rewards, ERC1155RewardsStorageV1 { + error MOCK_ERC1155_INVALID_REMAINING_VALUE(); + + address public creator; + uint256 public salePrice; + + constructor( + address _creator, + address _createReferral, + address _protocolRewards, + address _zoraRewardRecipient + ) ERC1155("Mock ERC1155 URI") ERC1155Rewards(_protocolRewards, _zoraRewardRecipient) { + creator = _creator; + createReferrals[0] = _createReferral; + } + + function setSalePrice(uint256 _salePrice) external { + salePrice = _salePrice; + } + + function mintWithRewards(address to, uint256 tokenId, uint256 numTokens, address mintReferral) external payable { + if (firstMinters[tokenId] == address(0)) firstMinters[tokenId] = to; + + uint256 remainingValue = _handleRewardsAndGetValueSent(msg.value, numTokens, creator, createReferrals[tokenId], mintReferral, firstMinters[tokenId]); + + uint256 expectedRemainingValue = salePrice * numTokens; + + if (remainingValue != expectedRemainingValue) revert MOCK_ERC1155_INVALID_REMAINING_VALUE(); + + _mint(to, tokenId, numTokens, ""); + } +} diff --git a/yarn.lock b/yarn.lock index ab76b3e99..f4a28329b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -712,6 +712,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@^20.3.2": + version "20.8.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.4.tgz#0e9ebb2ff29d5c3302fc84477d066fa7c6b441aa" + integrity sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A== + dependencies: + undici-types "~5.25.1" + "@types/normalize-package-data@^2.4.0": version "2.4.2" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz#9b0e3e8533fe5024ad32d6637eb9589988b6fdca" @@ -816,11 +823,6 @@ resolved "https://registry.yarnpkg.com/@zoralabs/openzeppelin-contracts-upgradeable/-/openzeppelin-contracts-upgradeable-4.8.4.tgz#130b69cd5ff70b1f67da11fe53fe8b2323464b84" integrity sha512-5vhL88tz00Gv2+NUhLdYBRqb9RRekfyQAodXTQxJU2LYxxy6jr1mPycTZempQ1kmw5wIwFbSIoYzpaxOx6UK6Q== -"@zoralabs/protocol-rewards@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@zoralabs/protocol-rewards/-/protocol-rewards-1.2.1.tgz#fe7f011748b036a1a3c8f3b43811307794d1c89a" - integrity sha512-Jf2aIHhyAsybCCv1byV5uP/YiwA/ZB3zTywDO6d15796Bf58zzC3D1ptKuh+z1Nba3dU2Hzqz0K7EEQOjoq+1A== - abitype@0.8.7: version "0.8.7" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.7.tgz#e4b3f051febd08111f486c0cc6a98fa72d033622" @@ -4039,6 +4041,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"