From efdb5e70513b25bda5dad2d8b77bfd985349dafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?txb=C3=AC?= <46839250+0xTxbi@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:31:50 +0100 Subject: [PATCH] feature(smart-contracts): `Airdrops` (#15550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add contract * linting and cleanup * apply suggestions * blocklist feature * cleanup * update contract * add comprehensive tests * linting * more cleanup * more cleanup * more cleanup * more cleanup * refactored contracts * using the campaign name * adding the chainalisys oracle * adding more tests * fixed tests * linting * moved file * updated version * make contract upgradeable * move released contract to UP folder using `yarn hardhat release --contract contracts/tokens/UP/Airdrops.sol --dest-folder UP` from `smart-contracts` folder * add airdrops deployment script --------- Co-authored-by: Julien Genestoux Co-authored-by: Clément Renaud --- governance/scripts/deployments/airdrop.js | 46 + packages/contracts/CHANGELOG.md | 4 + packages/contracts/package.json | 2 +- packages/contracts/src/abis/UP/Airdrops.json | 642 ++++++ .../contracts/src/contracts/UP/Airdrops.sol | 2018 +++++++++++++++++ packages/hardhat-helpers/src/deploy.js | 1 + packages/hardhat-helpers/src/tasks/deploy.js | 10 +- .../contracts/tokens/UP/Airdrops.sol | 235 ++ .../contracts/utils/SanctionsList.sol | 16 + smart-contracts/test/UPToken/airdrops.js | 330 +++ 10 files changed, 3298 insertions(+), 6 deletions(-) create mode 100644 governance/scripts/deployments/airdrop.js create mode 100644 packages/contracts/src/abis/UP/Airdrops.json create mode 100644 packages/contracts/src/contracts/UP/Airdrops.sol create mode 100644 smart-contracts/contracts/tokens/UP/Airdrops.sol create mode 100644 smart-contracts/contracts/utils/SanctionsList.sol create mode 100644 smart-contracts/test/UPToken/airdrops.js diff --git a/governance/scripts/deployments/airdrop.js b/governance/scripts/deployments/airdrop.js new file mode 100644 index 00000000000..13cba2e788f --- /dev/null +++ b/governance/scripts/deployments/airdrop.js @@ -0,0 +1,46 @@ +const { + getNetwork, + deployUpgradeableContract, + copyAndBuildContractsAtVersion, +} = require('@unlock-protocol/hardhat-helpers') + +// from https://go.chainalysis.com/chainalysis-oracle-docs.html +const chainalysisOracle = '0x3A91A31cB3dC49b4db9Ce721F50a9D076c8D739B' + +async function main() { + // fetch chain info + const base = await getNetwork(8453) + const { address: upAddress } = base.tokens.find( + (token) => token.symbol === 'UP' + ) + const { id: chainId } = await getNetwork() + + console.log(`Deploying UP Airdrops to ${chainId}`) + + const [qualifiedPath] = await copyAndBuildContractsAtVersion(__dirname, [ + { contractName: 'Airdrops', subfolder: 'UP' }, + ]) + + console.log(` waiting for tx to be mined for contract verification...`) + const { address: airdropAddress } = await deployUpgradeableContract( + qualifiedPath, + [upAddress, chainalysisOracle], + { + wait: 5, + } + ) + + console.log(`UP Airdrops deployed at ${airdropAddress}`) +} + +// execute as standalone +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +} + +module.exports = main diff --git a/packages/contracts/CHANGELOG.md b/packages/contracts/CHANGELOG.md index b220549e235..c46caf3cbbd 100644 --- a/packages/contracts/CHANGELOG.md +++ b/packages/contracts/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 0.0.33 + +- Adding Airdrops contract + ## 0.0.32 - PublicLock v15 : Minor changes to reduce contract size (see (https://github.com/unlock-protocol/unlock/pull/15242)[#15242]) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index f277f75b7e0..3338cb263ba 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@unlock-protocol/contracts", - "version": "0.0.32", + "version": "0.0.33", "main": "dist/index.js", "scripts": { "clean": "rm -rf dist docs src/index.ts", diff --git a/packages/contracts/src/abis/UP/Airdrops.json b/packages/contracts/src/abis/UP/Airdrops.json new file mode 100644 index 00000000000..dc516baf091 --- /dev/null +++ b/packages/contracts/src/abis/UP/Airdrops.json @@ -0,0 +1,642 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "Airdrops", + "sourceName": "contracts/Airdrops.flatten.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "campaignHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "AlreadyClaimed", + "type": "error" + }, + { + "inputs": [], + "name": "ECDSAInvalidSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "ECDSAInvalidSignatureLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "ECDSAInvalidSignatureS", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "campaignHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + } + ], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "campaignHash", + "type": "bytes32" + } + ], + "name": "MissingCampaign", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "UserBlocked", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "campaignHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "expected", + "type": "address" + } + ], + "name": "WrongSigner", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "AddedToBlockList", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "campaign", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + } + ], + "name": "CampaignSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "chainalysisOracle", + "type": "address" + } + ], + "name": "ChainalysisOracleSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "RemovedFromBlockList", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "campaign", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensClaimed", + "type": "event" + }, + { + "inputs": [], + "name": "EIP712Name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EIP712Version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "addToBlocklist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "blocklist", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "campaigns", + "outputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "chainalysisOracle", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "campaignName", + "type": "string" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + }, + { + "internalType": "bytes", + "name": "tosSignature", + "type": "bytes" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "claimedLeafs", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_chainalysisOracle", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isBlocked", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "removeFromBlocklist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_chainalysisOracle", + "type": "address" + } + ], + "name": "setChainalysisOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "campaignName", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "name": "setMerkleRootForCampaign", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "string", + "name": "campaignName", + "type": "string" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "verifySignature", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x608060405234801561000f575f80fd5b5061001861001d565b6100cf565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff161561006d5760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b03908116146100cc5780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b61186c806100dc5f395ff3fe608060405234801561000f575f80fd5b50600436106100fe575f3560e01c8063a0e5db951161009a578063a0e5db95146101c7578063c83fae91146101ea578063cd3bffbb146101fd578063cea57fe814610235578063d6b9850d14610248578063ddcf23b014610250578063e5c7160b14610263578063f2fde38b14610285578063fbac395114610298578063fc0c546a146102ab575f80fd5b80632fff5f0b14610102578063485cc95514610120578063592ca85014610135578063715018a61461014857806377962f5e1461015057806384b0196e146101635780638da5cb5b1461017e5780639bd6fe77146101935780639eca2f1e146101a6575b5f80fd5b61010a6102bd565b604051610117919061125a565b60405180910390f35b61013361012e366004611287565b6102cc565b005b6101336101433660046112b8565b61044f565b61013361049f565b61013361015e366004611315565b6104b2565b61016b610587565b604051610117979695949392919061135c565b610186610630565b60405161011791906113f0565b6101336101a1366004611418565b61065e565b6101b96101b436600461153e565b6108e0565b604051610117929190611555565b6101da6101d5366004611576565b610981565b6040519015158152602001610117565b6101336101f83660046112b8565b6109f2565b61022761020b3660046115f9565b600260209081525f928352604080842090915290825290205481565b604051908152602001610117565b600454610186906001600160a01b031681565b61010a610a45565b61013361025e3660046112b8565b610a4f565b6101da6102713660046112b8565b60036020525f908152604090205460ff1681565b6101336102933660046112b8565b610aa0565b6101da6102a63660046112b8565b610add565b5f54610186906001600160a01b031681565b60606102c7610b72565b905090565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff1615906001600160401b03165f811580156103105750825b90505f826001600160401b0316600114801561032b5750303b155b905081158015610339575080155b156103575760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561038157845460ff60401b1916600160401b1785555b61038a33610c10565b6103dd60405180604001604052806018815260200177556e6c6f636b2050726f746f636f6c2041697264726f707360401b815250604051806040016040528060018152602001603160f81b815250610c21565b5f80546001600160a01b0319166001600160a01b03891617905561040086610a4f565b831561044657845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b610457610c37565b6001600160a01b0381165f81815260036020526040808220805460ff19169055517fcdabb021e917f9d861cb523bbce1b91834fdd7e51a00cc016b762f94a322017a9190a250565b6104a7610c37565b6104b05f610c69565b565b6104ba610c37565b5f83836040516020016104ce929190611619565b60408051601f1981840301815282825280516020918201206060601f880183900490920284018201835291830186815291935082919087908790819085018382808284375f92018290525093855250505060209182018590528381526001909152604090208151819061054190826116ae565b5060209182015160019091015560405183815282917f89c3e26d7bcdf317ac634ea41b5a797210026588da523dfbc2f0cd7cb397f144910160405180910390a250505050565b5f6060805f805f60605f610599610cd9565b80549091501580156105ad57506001810154155b6105f65760405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b60448201526064015b60405180910390fd5b6105fe610b72565b610606610cfd565b604080515f80825260208201909252600f60f81b9c939b5091995046985030975095509350915050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b5f8888604051602001610672929190611619565b60405160208183030381529060405280519060200120905061069386610add565b156106b3578560405163ca86d8c960e01b81526004016105ed91906113f0565b5f818152600160208190526040909120908101546106e75760405163a855b33b60e01b8152600481018390526024016105ed565b6106f5878b8b8b8888610981565b61072457604051630fa3cb9d60e01b8152600481018390526001600160a01b03881660248201526044016105ed565b604080516001600160a01b03891660208201529081018790525f9060600160408051601f19818403018152828252805160209182012090830152016040516020818303038152906040528051906020012090505f61078787846001015484610d19565b9050806107af57838989896040516335d6488d60e11b81526004016105ed9493929190611769565b5f848152600260209081526040808320858452909152902054156107ff57604051637cf5c21d60e11b8152600481018590526001600160a01b038a166024820152604481018990526064016105ed565b5f8481526002602090815260408083208584529091528082204290559054905163a9059cbb60e01b81526001600160a01b038b81166004830152602482018b90529091169063a9059cbb906044016020604051808303815f875af1158015610869573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061088d91906117cb565b50886001600160a01b0316847fb6a3782a350d2b8520b7fe13e8e97b61f22902243ad29c4c3a9a8f714935718c8a6040516108ca91815260200190565b60405180910390a3505050505050505050505050565b60016020525f90815260409020805481906108fa90611628565b80601f016020809104026020016040519081016040528092919081815260200182805461092690611628565b80156109715780601f1061094857610100808354040283529160200191610971565b820191905f5260205f20905b81548152906001019060200180831161095457829003601f168201915b5050505050908060010154905082565b5f8061098f88888888610d2e565b9050876001600160a01b03166109dc85858080601f0160208091040260200160405190810160405280939291908181526020018383808284375f920191909152508693925050610dd99050565b6001600160a01b03161498975050505050505050565b6109fa610c37565b6001600160a01b0381165f81815260036020526040808220805460ff19166001179055517fe195c9fe4a65c54ea36d99055c62d29b7eb7ae93c2302230768051410763aeea9190a250565b60606102c7610cfd565b610a57610c37565b600480546001600160a01b0319166001600160a01b0383169081179091556040517f945608d9c0482277a161758f06cc06330d471d606d6bf4f5eb2c2a94e97cc523905f90a250565b610aa8610c37565b6001600160a01b038116610ad1575f604051631e4fbdf760e01b81526004016105ed91906113f0565b610ada81610c69565b50565b6001600160a01b0381165f9081526003602052604081205460ff1680610b6c57506004805460405163df592f7d60e01b81526001600160a01b039091169163df592f7d91610b2d918691016113f0565b602060405180830381865afa158015610b48573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610b6c91906117cb565b92915050565b60605f610b7d610cd9565b9050806002018054610b8e90611628565b80601f0160208091040260200160405190810160405280929190818152602001828054610bba90611628565b8015610c055780601f10610bdc57610100808354040283529160200191610c05565b820191905f5260205f20905b815481529060010190602001808311610be857829003601f168201915b505050505091505090565b610c18610e01565b610ada81610e4a565b610c29610e01565b610c338282610e52565b5050565b33610c40610630565b6001600160a01b0316146104b0573360405163118cdaa760e01b81526004016105ed91906113f0565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d10090565b60605f610d08610cd9565b9050806003018054610b8e90611628565b5f82610d258584610e91565b14949350505050565b5f807f7b90df25d76042650a088e472aa3dc43eddd25ac84381bbee2fdfc9735868cb8868686604051602001610d65929190611619565b6040516020818303038152906040528051906020012085604051602001610dae94939291909384526001600160a01b039290921660208401526040830152606082015260800190565b604051602081830303815290604052805190602001209050610dcf81610edd565b9695505050505050565b5f805f80610de78686610f09565b925092509250610df78282610f52565b5090949350505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff166104b057604051631afcd79f60e31b815260040160405180910390fd5b610aa8610e01565b610e5a610e01565b5f610e63610cd9565b905060028101610e7384826116ae565b5060038101610e8283826116ae565b505f8082556001909101555050565b5f81815b8451811015610ed557610ec182868381518110610eb457610eb46117ea565b602002602001015161100a565b915080610ecd816117fe565b915050610e95565b509392505050565b5f610b6c610ee9611039565b8360405161190160f01b8152600281019290925260228201526042902090565b5f805f8351604103610f40576020840151604085015160608601515f1a610f3288828585611042565b955095509550505050610f4b565b505081515f91506002905b9250925092565b5f826003811115610f6557610f65611822565b03610f6e575050565b6001826003811115610f8257610f82611822565b03610fa05760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610fb457610fb4611822565b03610fd55760405163fce698f760e01b8152600481018290526024016105ed565b6003826003811115610fe957610fe9611822565b03610c33576040516335e2f38360e21b8152600481018290526024016105ed565b5f818310611024575f828152602084905260409020611032565b5f8381526020839052604090205b9392505050565b5f6102c7611100565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b0384111561107157505f915060039050826110f6565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156110c2573d5f803e3d5ffd5b5050604051601f1901519150506001600160a01b0381166110ed57505f9250600191508290506110f6565b92505f91508190505b9450945094915050565b5f7f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f61112a611173565b6111326111d8565b60408051602081019490945283019190915260608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b5f8061117d610cd9565b90505f611188610b72565b8051909150156111a057805160209091012092915050565b815480156111af579392505050565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470935050505090565b5f806111e2610cd9565b90505f6111ed610cfd565b80519091501561120557805160209091012092915050565b600182015480156111af579392505050565b5f81518084525f5b8181101561123b5760208185018101518683018201520161121f565b505f602082860101526020601f19601f83011685010191505092915050565b602081525f6110326020830184611217565b80356001600160a01b0381168114611282575f80fd5b919050565b5f8060408385031215611298575f80fd5b6112a18361126c565b91506112af6020840161126c565b90509250929050565b5f602082840312156112c8575f80fd5b6110328261126c565b5f8083601f8401126112e1575f80fd5b5081356001600160401b038111156112f7575f80fd5b60208301915083602082850101111561130e575f80fd5b9250929050565b5f805f60408486031215611327575f80fd5b83356001600160401b0381111561133c575f80fd5b611348868287016112d1565b909790965060209590950135949350505050565b60ff60f81b881681525f602060e08184015261137b60e084018a611217565b838103604085015261138d818a611217565b606085018990526001600160a01b038816608086015260a0850187905284810360c086015285518082528387019250908301905f5b818110156113de578351835292840192918401916001016113c2565b50909c9b505050505050505050505050565b6001600160a01b0391909116815260200190565b634e487b7160e01b5f52604160045260245ffd5b5f805f805f805f8060c0898b03121561142f575f80fd5b6001600160401b03808a351115611444575f80fd5b6114518b8b358c016112d1565b909950975060208a0135965061146960408b0161126c565b955060608a0135945060808a013581811115611483575f80fd5b8a01601f81018c13611493575f80fd5b8035828111156114a5576114a5611404565b8060051b604051601f19603f830116810181811086821117156114ca576114ca611404565b60405291825260208184018101929081018f8411156114e7575f80fd5b6020850194505b838510156115065784358152602094850194016114ee565b50965050505060a08a01358181111561151d575f80fd5b6115298c828d016112d1565b9a9d999c50979a509598949793969550505050565b5f6020828403121561154e575f80fd5b5035919050565b604081525f6115676040830185611217565b90508260208301529392505050565b5f805f805f806080878903121561158b575f80fd5b6115948761126c565b955060208701356001600160401b03808211156115af575f80fd5b6115bb8a838b016112d1565b90975095506040890135945060608901359150808211156115da575f80fd5b506115e789828a016112d1565b979a9699509497509295939492505050565b5f806040838503121561160a575f80fd5b50508035926020909101359150565b818382375f9101908152919050565b600181811c9082168061163c57607f821691505b60208210810361165a57634e487b7160e01b5f52602260045260245ffd5b50919050565b601f8211156116a9575f81815260208120601f850160051c810160208610156116865750805b601f850160051c820191505b818110156116a557828155600101611692565b5050505b505050565b81516001600160401b038111156116c7576116c7611404565b6116db816116d58454611628565b84611660565b602080601f83116001811461170e575f84156116f75750858301515b5f19600386901b1c1916600185901b1785556116a5565b5f85815260208120601f198616915b8281101561173c5788860151825594840194600190910190840161171d565b508582101561175957878501515f19600388901b60f8161c191681555b5050505050600190811b01905550565b5f60808201868352602060018060a01b038716818501528560408501526080606085015281855180845260a08601915082870193505f5b818110156117bc578451835293830193918301916001016117a0565b50909998505050505050505050565b5f602082840312156117db575f80fd5b81518015158114611032575f80fd5b634e487b7160e01b5f52603260045260245ffd5b5f6001820161181b57634e487b7160e01b5f52601160045260245ffd5b5060010190565b634e487b7160e01b5f52602160045260245ffdfea2646970667358221220744210f65398685ab999c86c1eca2fdc2e8ea2defa580440b7d852564d70cb4864736f6c63430008150033", + "deployedBytecode": "0x608060405234801561000f575f80fd5b50600436106100fe575f3560e01c8063a0e5db951161009a578063a0e5db95146101c7578063c83fae91146101ea578063cd3bffbb146101fd578063cea57fe814610235578063d6b9850d14610248578063ddcf23b014610250578063e5c7160b14610263578063f2fde38b14610285578063fbac395114610298578063fc0c546a146102ab575f80fd5b80632fff5f0b14610102578063485cc95514610120578063592ca85014610135578063715018a61461014857806377962f5e1461015057806384b0196e146101635780638da5cb5b1461017e5780639bd6fe77146101935780639eca2f1e146101a6575b5f80fd5b61010a6102bd565b604051610117919061125a565b60405180910390f35b61013361012e366004611287565b6102cc565b005b6101336101433660046112b8565b61044f565b61013361049f565b61013361015e366004611315565b6104b2565b61016b610587565b604051610117979695949392919061135c565b610186610630565b60405161011791906113f0565b6101336101a1366004611418565b61065e565b6101b96101b436600461153e565b6108e0565b604051610117929190611555565b6101da6101d5366004611576565b610981565b6040519015158152602001610117565b6101336101f83660046112b8565b6109f2565b61022761020b3660046115f9565b600260209081525f928352604080842090915290825290205481565b604051908152602001610117565b600454610186906001600160a01b031681565b61010a610a45565b61013361025e3660046112b8565b610a4f565b6101da6102713660046112b8565b60036020525f908152604090205460ff1681565b6101336102933660046112b8565b610aa0565b6101da6102a63660046112b8565b610add565b5f54610186906001600160a01b031681565b60606102c7610b72565b905090565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff1615906001600160401b03165f811580156103105750825b90505f826001600160401b0316600114801561032b5750303b155b905081158015610339575080155b156103575760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561038157845460ff60401b1916600160401b1785555b61038a33610c10565b6103dd60405180604001604052806018815260200177556e6c6f636b2050726f746f636f6c2041697264726f707360401b815250604051806040016040528060018152602001603160f81b815250610c21565b5f80546001600160a01b0319166001600160a01b03891617905561040086610a4f565b831561044657845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b610457610c37565b6001600160a01b0381165f81815260036020526040808220805460ff19169055517fcdabb021e917f9d861cb523bbce1b91834fdd7e51a00cc016b762f94a322017a9190a250565b6104a7610c37565b6104b05f610c69565b565b6104ba610c37565b5f83836040516020016104ce929190611619565b60408051601f1981840301815282825280516020918201206060601f880183900490920284018201835291830186815291935082919087908790819085018382808284375f92018290525093855250505060209182018590528381526001909152604090208151819061054190826116ae565b5060209182015160019091015560405183815282917f89c3e26d7bcdf317ac634ea41b5a797210026588da523dfbc2f0cd7cb397f144910160405180910390a250505050565b5f6060805f805f60605f610599610cd9565b80549091501580156105ad57506001810154155b6105f65760405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b60448201526064015b60405180910390fd5b6105fe610b72565b610606610cfd565b604080515f80825260208201909252600f60f81b9c939b5091995046985030975095509350915050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b5f8888604051602001610672929190611619565b60405160208183030381529060405280519060200120905061069386610add565b156106b3578560405163ca86d8c960e01b81526004016105ed91906113f0565b5f818152600160208190526040909120908101546106e75760405163a855b33b60e01b8152600481018390526024016105ed565b6106f5878b8b8b8888610981565b61072457604051630fa3cb9d60e01b8152600481018390526001600160a01b03881660248201526044016105ed565b604080516001600160a01b03891660208201529081018790525f9060600160408051601f19818403018152828252805160209182012090830152016040516020818303038152906040528051906020012090505f61078787846001015484610d19565b9050806107af57838989896040516335d6488d60e11b81526004016105ed9493929190611769565b5f848152600260209081526040808320858452909152902054156107ff57604051637cf5c21d60e11b8152600481018590526001600160a01b038a166024820152604481018990526064016105ed565b5f8481526002602090815260408083208584529091528082204290559054905163a9059cbb60e01b81526001600160a01b038b81166004830152602482018b90529091169063a9059cbb906044016020604051808303815f875af1158015610869573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061088d91906117cb565b50886001600160a01b0316847fb6a3782a350d2b8520b7fe13e8e97b61f22902243ad29c4c3a9a8f714935718c8a6040516108ca91815260200190565b60405180910390a3505050505050505050505050565b60016020525f90815260409020805481906108fa90611628565b80601f016020809104026020016040519081016040528092919081815260200182805461092690611628565b80156109715780601f1061094857610100808354040283529160200191610971565b820191905f5260205f20905b81548152906001019060200180831161095457829003601f168201915b5050505050908060010154905082565b5f8061098f88888888610d2e565b9050876001600160a01b03166109dc85858080601f0160208091040260200160405190810160405280939291908181526020018383808284375f920191909152508693925050610dd99050565b6001600160a01b03161498975050505050505050565b6109fa610c37565b6001600160a01b0381165f81815260036020526040808220805460ff19166001179055517fe195c9fe4a65c54ea36d99055c62d29b7eb7ae93c2302230768051410763aeea9190a250565b60606102c7610cfd565b610a57610c37565b600480546001600160a01b0319166001600160a01b0383169081179091556040517f945608d9c0482277a161758f06cc06330d471d606d6bf4f5eb2c2a94e97cc523905f90a250565b610aa8610c37565b6001600160a01b038116610ad1575f604051631e4fbdf760e01b81526004016105ed91906113f0565b610ada81610c69565b50565b6001600160a01b0381165f9081526003602052604081205460ff1680610b6c57506004805460405163df592f7d60e01b81526001600160a01b039091169163df592f7d91610b2d918691016113f0565b602060405180830381865afa158015610b48573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610b6c91906117cb565b92915050565b60605f610b7d610cd9565b9050806002018054610b8e90611628565b80601f0160208091040260200160405190810160405280929190818152602001828054610bba90611628565b8015610c055780601f10610bdc57610100808354040283529160200191610c05565b820191905f5260205f20905b815481529060010190602001808311610be857829003601f168201915b505050505091505090565b610c18610e01565b610ada81610e4a565b610c29610e01565b610c338282610e52565b5050565b33610c40610630565b6001600160a01b0316146104b0573360405163118cdaa760e01b81526004016105ed91906113f0565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d10090565b60605f610d08610cd9565b9050806003018054610b8e90611628565b5f82610d258584610e91565b14949350505050565b5f807f7b90df25d76042650a088e472aa3dc43eddd25ac84381bbee2fdfc9735868cb8868686604051602001610d65929190611619565b6040516020818303038152906040528051906020012085604051602001610dae94939291909384526001600160a01b039290921660208401526040830152606082015260800190565b604051602081830303815290604052805190602001209050610dcf81610edd565b9695505050505050565b5f805f80610de78686610f09565b925092509250610df78282610f52565b5090949350505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff166104b057604051631afcd79f60e31b815260040160405180910390fd5b610aa8610e01565b610e5a610e01565b5f610e63610cd9565b905060028101610e7384826116ae565b5060038101610e8283826116ae565b505f8082556001909101555050565b5f81815b8451811015610ed557610ec182868381518110610eb457610eb46117ea565b602002602001015161100a565b915080610ecd816117fe565b915050610e95565b509392505050565b5f610b6c610ee9611039565b8360405161190160f01b8152600281019290925260228201526042902090565b5f805f8351604103610f40576020840151604085015160608601515f1a610f3288828585611042565b955095509550505050610f4b565b505081515f91506002905b9250925092565b5f826003811115610f6557610f65611822565b03610f6e575050565b6001826003811115610f8257610f82611822565b03610fa05760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610fb457610fb4611822565b03610fd55760405163fce698f760e01b8152600481018290526024016105ed565b6003826003811115610fe957610fe9611822565b03610c33576040516335e2f38360e21b8152600481018290526024016105ed565b5f818310611024575f828152602084905260409020611032565b5f8381526020839052604090205b9392505050565b5f6102c7611100565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b0384111561107157505f915060039050826110f6565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156110c2573d5f803e3d5ffd5b5050604051601f1901519150506001600160a01b0381166110ed57505f9250600191508290506110f6565b92505f91508190505b9450945094915050565b5f7f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f61112a611173565b6111326111d8565b60408051602081019490945283019190915260608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b5f8061117d610cd9565b90505f611188610b72565b8051909150156111a057805160209091012092915050565b815480156111af579392505050565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470935050505090565b5f806111e2610cd9565b90505f6111ed610cfd565b80519091501561120557805160209091012092915050565b600182015480156111af579392505050565b5f81518084525f5b8181101561123b5760208185018101518683018201520161121f565b505f602082860101526020601f19601f83011685010191505092915050565b602081525f6110326020830184611217565b80356001600160a01b0381168114611282575f80fd5b919050565b5f8060408385031215611298575f80fd5b6112a18361126c565b91506112af6020840161126c565b90509250929050565b5f602082840312156112c8575f80fd5b6110328261126c565b5f8083601f8401126112e1575f80fd5b5081356001600160401b038111156112f7575f80fd5b60208301915083602082850101111561130e575f80fd5b9250929050565b5f805f60408486031215611327575f80fd5b83356001600160401b0381111561133c575f80fd5b611348868287016112d1565b909790965060209590950135949350505050565b60ff60f81b881681525f602060e08184015261137b60e084018a611217565b838103604085015261138d818a611217565b606085018990526001600160a01b038816608086015260a0850187905284810360c086015285518082528387019250908301905f5b818110156113de578351835292840192918401916001016113c2565b50909c9b505050505050505050505050565b6001600160a01b0391909116815260200190565b634e487b7160e01b5f52604160045260245ffd5b5f805f805f805f8060c0898b03121561142f575f80fd5b6001600160401b03808a351115611444575f80fd5b6114518b8b358c016112d1565b909950975060208a0135965061146960408b0161126c565b955060608a0135945060808a013581811115611483575f80fd5b8a01601f81018c13611493575f80fd5b8035828111156114a5576114a5611404565b8060051b604051601f19603f830116810181811086821117156114ca576114ca611404565b60405291825260208184018101929081018f8411156114e7575f80fd5b6020850194505b838510156115065784358152602094850194016114ee565b50965050505060a08a01358181111561151d575f80fd5b6115298c828d016112d1565b9a9d999c50979a509598949793969550505050565b5f6020828403121561154e575f80fd5b5035919050565b604081525f6115676040830185611217565b90508260208301529392505050565b5f805f805f806080878903121561158b575f80fd5b6115948761126c565b955060208701356001600160401b03808211156115af575f80fd5b6115bb8a838b016112d1565b90975095506040890135945060608901359150808211156115da575f80fd5b506115e789828a016112d1565b979a9699509497509295939492505050565b5f806040838503121561160a575f80fd5b50508035926020909101359150565b818382375f9101908152919050565b600181811c9082168061163c57607f821691505b60208210810361165a57634e487b7160e01b5f52602260045260245ffd5b50919050565b601f8211156116a9575f81815260208120601f850160051c810160208610156116865750805b601f850160051c820191505b818110156116a557828155600101611692565b5050505b505050565b81516001600160401b038111156116c7576116c7611404565b6116db816116d58454611628565b84611660565b602080601f83116001811461170e575f84156116f75750858301515b5f19600386901b1c1916600185901b1785556116a5565b5f85815260208120601f198616915b8281101561173c5788860151825594840194600190910190840161171d565b508582101561175957878501515f19600388901b60f8161c191681555b5050505050600190811b01905550565b5f60808201868352602060018060a01b038716818501528560408501526080606085015281855180845260a08601915082870193505f5b818110156117bc578451835293830193918301916001016117a0565b50909998505050505050505050565b5f602082840312156117db575f80fd5b81518015158114611032575f80fd5b634e487b7160e01b5f52603260045260245ffd5b5f6001820161181b57634e487b7160e01b5f52601160045260245ffd5b5060010190565b634e487b7160e01b5f52602160045260245ffdfea2646970667358221220744210f65398685ab999c86c1eca2fdc2e8ea2defa580440b7d852564d70cb4864736f6c63430008150033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/packages/contracts/src/contracts/UP/Airdrops.sol b/packages/contracts/src/contracts/UP/Airdrops.sol new file mode 100644 index 00000000000..18723614990 --- /dev/null +++ b/packages/contracts/src/contracts/UP/Airdrops.sol @@ -0,0 +1,2018 @@ +// Sources flattened with hardhat v2.22.18 https://hardhat.org + +// SPDX-License-Identifier: MIT + +// File @openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol) + +pragma solidity ^0.8.20; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ```solidity + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Storage of the initializable contract. + * + * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions + * when using with upgradeable contracts. + * + * @custom:storage-location erc7201:openzeppelin.storage.Initializable + */ + struct InitializableStorage { + /** + * @dev Indicates that the contract has been initialized. + */ + uint64 _initialized; + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool _initializing; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + /** + * @dev The contract is already initialized. + */ + error InvalidInitialization(); + + /** + * @dev The contract is not initializing. + */ + error NotInitializing(); + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint64 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. + * + * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any + * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in + * production. + * + * Emits an {Initialized} event. + */ + modifier initializer() { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // Cache values to avoid duplicated sloads + bool isTopLevelCall = !$._initializing; + uint64 initialized = $._initialized; + + // Allowed calls: + // - initialSetup: the contract is not in the initializing state and no previous version was + // initialized + // - construction: the contract is initialized at version 1 (no reininitialization) and the + // current contract is just being deployed + bool initialSetup = initialized == 0 && isTopLevelCall; + bool construction = initialized == 1 && address(this).code.length == 0; + + if (!initialSetup && !construction) { + revert InvalidInitialization(); + } + $._initialized = 1; + if (isTopLevelCall) { + $._initializing = true; + } + _; + if (isTopLevelCall) { + $._initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * A reinitializer may be used after the original initialization step. This is essential to configure modules that + * are added through upgrades and that require initialization. + * + * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` + * cannot be nested. If one is invoked in the context of another, execution will revert. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + * + * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. + * + * Emits an {Initialized} event. + */ + modifier reinitializer(uint64 version) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing || $._initialized >= version) { + revert InvalidInitialization(); + } + $._initialized = version; + $._initializing = true; + _; + $._initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + _checkInitializing(); + _; + } + + /** + * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. + */ + function _checkInitializing() internal view virtual { + if (!_isInitializing()) { + revert NotInitializing(); + } + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + * + * Emits an {Initialized} event the first time it is successfully executed. + */ + function _disableInitializers() internal virtual { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing) { + revert InvalidInitialization(); + } + if ($._initialized != type(uint64).max) { + $._initialized = type(uint64).max; + emit Initialized(type(uint64).max); + } + } + + /** + * @dev Returns the highest version that has been initialized. See {reinitializer}. + */ + function _getInitializedVersion() internal view returns (uint64) { + return _getInitializableStorage()._initialized; + } + + /** + * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. + */ + function _isInitializing() internal view returns (bool) { + return _getInitializableStorage()._initializing; + } + + /** + * @dev Returns a pointer to the storage namespace. + */ + // solhint-disable-next-line var-name-mixedcase + function _getInitializableStorage() private pure returns (InitializableStorage storage $) { + assembly { + $.slot := INITIALIZABLE_STORAGE + } + } +} + + +// File @openzeppelin/contracts-upgradeable5/utils/ContextUpgradeable.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) + +pragma solidity ^0.8.20; + +/** + * @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 ContextUpgradeable is Initializable { + function __Context_init() internal onlyInitializing { + } + + function __Context_init_unchained() internal onlyInitializing { + } + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } +} + + +// File @openzeppelin/contracts-upgradeable5/access/OwnableUpgradeable.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol) + +pragma solidity ^0.8.20; + + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * The initial owner is set to the address provided by the deployer. This can + * later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable { + /// @custom:storage-location erc7201:openzeppelin.storage.Ownable + struct OwnableStorage { + address _owner; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; + + function _getOwnableStorage() private pure returns (OwnableStorage storage $) { + assembly { + $.slot := OwnableStorageLocation + } + } + + /** + * @dev The caller account is not authorized to perform an operation. + */ + error OwnableUnauthorizedAccount(address account); + + /** + * @dev The owner is not a valid owner account. (eg. `address(0)`) + */ + error OwnableInvalidOwner(address owner); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the address provided by the deployer as the initial owner. + */ + function __Ownable_init(address initialOwner) internal onlyInitializing { + __Ownable_init_unchained(initialOwner); + } + + function __Ownable_init_unchained(address initialOwner) internal onlyInitializing { + if (initialOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(initialOwner); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + OwnableStorage storage $ = _getOwnableStorage(); + return $._owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + if (owner() != _msgSender()) { + revert OwnableUnauthorizedAccount(_msgSender()); + } + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby disabling any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + OwnableStorage storage $ = _getOwnableStorage(); + address oldOwner = $._owner; + $._owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} + + +// File @openzeppelin/contracts/interfaces/IERC5267.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5267.sol) + +pragma solidity ^0.8.20; + +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 + ); +} + + +// File @openzeppelin/contracts/utils/math/Math.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + /** + * @dev Muldiv operation overflow. + */ + error MathOverflowedMulDiv(); + + enum Rounding { + Floor, // Toward negative infinity + Ceil, // Toward positive infinity + Trunc, // Toward zero + Expand // Away from zero + } + + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @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 towards infinity instead + * of rounding towards zero. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + return a / b; + } + + // (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 = x * y; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + 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. + if (denominator <= prod1) { + revert MathOverflowedMulDiv(); + } + + /////////////////////////////////////////////// + // 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. + + uint256 twos = denominator & (0 - denominator); + 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 (unsignedRoundsUp(rounding) && 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 + * towards zero. + * + * 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 + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0); + } + } + + /** + * @dev Return the log in base 2 of a positive value rounded towards zero. + * 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 + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 10 of a positive value rounded towards zero. + * 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 + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0); + } + } + + /** + * @dev Return the log in base 256 of a positive value rounded towards zero. + * 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 + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0); + } + } + + /** + * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. + */ + function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) { + return uint8(rounding) % 2 == 1; + } +} + + +// File @openzeppelin/contracts/utils/math/SignedMath.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SignedMath.sol) + +pragma solidity ^0.8.20; + +/** + * @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); + } + } +} + + +// File @openzeppelin/contracts/utils/Strings.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Strings.sol) + +pragma solidity ^0.8.20; + + +/** + * @dev String operations. + */ +library Strings { + bytes16 private constant HEX_DIGITS = "0123456789abcdef"; + uint8 private constant ADDRESS_LENGTH = 20; + + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + + /** + * @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), HEX_DIGITS)) + } + value /= 10; + if (value == 0) break; + } + return buffer; + } + } + + /** + * @dev Converts a `int256` to its ASCII `string` decimal representation. + */ + function toStringSigned(int256 value) internal pure returns (string memory) { + return string.concat(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) { + uint256 localValue = value; + 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] = HEX_DIGITS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); + } + 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 bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + } +} + + +// File @openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MessageHashUtils.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing. + * + * The library provides methods for generating a hash of a message that conforms to the + * https://eips.ethereum.org/EIPS/eip-191[EIP 191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] + * specifications. + */ +library MessageHashUtils { + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing a bytes32 `messageHash` with + * `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with + * keccak256, although any bytes32 value can be safely used because the final digest will + * be re-hashed. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash + mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix + digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20) + } + } + + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing an arbitrary `message` with + * `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) { + return + keccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * `0x00` (data with intended validator). + * + * The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended + * `validator` address. Then hashing the result. + * + * See {ECDSA-recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(hex"19_00", validator, data)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-712 typed data (EIP-191 version `0x01`). + * + * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with + * `\x19\x01` and hashing the result. It corresponds to the hash signed by the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712. + * + * See {ECDSA-recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, hex"19_01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + digest := keccak256(ptr, 0x42) + } + } +} + + +// File @openzeppelin/contracts-upgradeable5/utils/cryptography/EIP712Upgradeable.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/EIP712.sol) + +pragma solidity ^0.8.20; + + + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * + * The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose + * encoding is very generic and therefore its 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 order to + * produce the hash of their typed data 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. + */ +abstract contract EIP712Upgradeable is Initializable, IERC5267 { + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /// @custom:storage-location erc7201:openzeppelin.storage.EIP712 + struct EIP712Storage { + /// @custom:oz-renamed-from _HASHED_NAME + bytes32 _hashedName; + /// @custom:oz-renamed-from _HASHED_VERSION + bytes32 _hashedVersion; + + string _name; + string _version; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.EIP712")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant EIP712StorageLocation = 0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100; + + function _getEIP712Storage() private pure returns (EIP712Storage storage $) { + assembly { + $.slot := EIP712StorageLocation + } + } + + /** + * @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]. + */ + function __EIP712_init(string memory name, string memory version) internal onlyInitializing { + __EIP712_init_unchained(name, version); + } + + function __EIP712_init_unchained(string memory name, string memory version) internal onlyInitializing { + EIP712Storage storage $ = _getEIP712Storage(); + $._name = name; + $._version = version; + + // Reset prior values in storage if upgrading + $._hashedName = 0; + $._hashedVersion = 0; + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + return _buildDomainSeparator(); + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), 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 MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev See {IERC-5267}. + */ + function eip712Domain() + public + view + virtual + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + EIP712Storage storage $ = _getEIP712Storage(); + // If the hashed name and version in storage are non-zero, the contract hasn't been properly initialized + // and the EIP712 domain is not reliable, as it will be missing name and version. + require($._hashedName == 0 && $._hashedVersion == 0, "EIP712: Uninitialized"); + + return ( + hex"0f", // 01111 + _EIP712Name(), + _EIP712Version(), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /** + * @dev The name parameter for the EIP712 domain. + * + * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs + * are a concern. + */ + function _EIP712Name() internal view virtual returns (string memory) { + EIP712Storage storage $ = _getEIP712Storage(); + return $._name; + } + + /** + * @dev The version parameter for the EIP712 domain. + * + * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs + * are a concern. + */ + function _EIP712Version() internal view virtual returns (string memory) { + EIP712Storage storage $ = _getEIP712Storage(); + return $._version; + } + + /** + * @dev The hash of the name parameter for the EIP712 domain. + * + * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Name` instead. + */ + function _EIP712NameHash() internal view returns (bytes32) { + EIP712Storage storage $ = _getEIP712Storage(); + string memory name = _EIP712Name(); + if (bytes(name).length > 0) { + return keccak256(bytes(name)); + } else { + // If the name is empty, the contract may have been upgraded without initializing the new storage. + // We return the name hash in storage if non-zero, otherwise we assume the name is empty by design. + bytes32 hashedName = $._hashedName; + if (hashedName != 0) { + return hashedName; + } else { + return keccak256(""); + } + } + } + + /** + * @dev The hash of the version parameter for the EIP712 domain. + * + * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead. + */ + function _EIP712VersionHash() internal view returns (bytes32) { + EIP712Storage storage $ = _getEIP712Storage(); + string memory version = _EIP712Version(); + if (bytes(version).length > 0) { + return keccak256(bytes(version)); + } else { + // If the version is empty, the contract may have been upgraded without initializing the new storage. + // We return the version hash in storage if non-zero, otherwise we assume the version is empty by design. + bytes32 hashedVersion = $._hashedVersion; + if (hashedVersion != 0) { + return hashedVersion; + } else { + return keccak256(""); + } + } + } +} + + +// File @openzeppelin/contracts/token/ERC20/IERC20.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} + + +// File @openzeppelin/contracts/utils/cryptography/ECDSA.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.20; + +/** + * @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 + } + + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + * return address(0) without also returning an error description. Errors are documented using an enum (error type) + * and a bytes32 providing additional information about the error. + * + * If no error is returned, then the address can be used for verification purposes. + * + * The `ecrecover` EVM precompile 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 {MessageHashUtils-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] + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { + 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, bytes32(signature.length)); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM precompile 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 {MessageHashUtils-toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); + 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] + */ + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + 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. + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { + // 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, s); + } + + // 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, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /** + * @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, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + */ + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); + } + } +} + + +// File @openzeppelin/contracts/utils/cryptography/MerkleProof.sol@v5.0.2 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.20; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the Merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates Merkle trees that are safe + * against this attack out of the box. + */ +library MerkleProof { + /** + *@dev The multiproof provided is not valid. + */ + error MerkleProofInvalidMultiproof(); + + /** + * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree + * defined by `root`. For this, a `proof` must be provided, containing + * sibling hashes on the branch from the leaf to the root of the tree. Each + * pair of leaves and each pair of pre-images are assumed to be sorted. + */ + function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) { + return processProof(proof, leaf) == root; + } + + /** + * @dev Calldata version of {verify} + */ + function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) { + return processProofCalldata(proof, leaf) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. When processing the proof, the pairs + * of leafs & pre-images are assumed to be sorted. + */ + function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = _hashPair(computedHash, proof[i]); + } + return computedHash; + } + + /** + * @dev Calldata version of {processProof} + */ + function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = _hashPair(computedHash, proof[i]); + } + return computedHash; + } + + /** + * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by + * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}. + * + * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. + */ + function multiProofVerify( + bytes32[] memory proof, + bool[] memory proofFlags, + bytes32 root, + bytes32[] memory leaves + ) internal pure returns (bool) { + return processMultiProof(proof, proofFlags, leaves) == root; + } + + /** + * @dev Calldata version of {multiProofVerify} + * + * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. + */ + function multiProofVerifyCalldata( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32 root, + bytes32[] memory leaves + ) internal pure returns (bool) { + return processMultiProofCalldata(proof, proofFlags, leaves) == root; + } + + /** + * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction + * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another + * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false + * respectively. + * + * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree + * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the + * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer). + */ + function processMultiProof( + bytes32[] memory proof, + bool[] memory proofFlags, + bytes32[] memory leaves + ) internal pure returns (bytes32 merkleRoot) { + // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by + // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the + // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of + // the Merkle tree. + uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; + uint256 totalHashes = proofFlags.length; + + // Check proof validity. + if (leavesLen + proofLen != totalHashes + 1) { + revert MerkleProofInvalidMultiproof(); + } + + // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using + // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". + bytes32[] memory hashes = new bytes32[](totalHashes); + uint256 leafPos = 0; + uint256 hashPos = 0; + uint256 proofPos = 0; + // At each step, we compute the next hash using two values: + // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we + // get the next hash. + // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the + // `proof` array. + for (uint256 i = 0; i < totalHashes; i++) { + bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]; + bytes32 b = proofFlags[i] + ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) + : proof[proofPos++]; + hashes[i] = _hashPair(a, b); + } + + if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } + unchecked { + return hashes[totalHashes - 1]; + } + } else if (leavesLen > 0) { + return leaves[0]; + } else { + return proof[0]; + } + } + + /** + * @dev Calldata version of {processMultiProof}. + * + * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details. + */ + function processMultiProofCalldata( + bytes32[] calldata proof, + bool[] calldata proofFlags, + bytes32[] memory leaves + ) internal pure returns (bytes32 merkleRoot) { + // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by + // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the + // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of + // the Merkle tree. + uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; + uint256 totalHashes = proofFlags.length; + + // Check proof validity. + if (leavesLen + proofLen != totalHashes + 1) { + revert MerkleProofInvalidMultiproof(); + } + + // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using + // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". + bytes32[] memory hashes = new bytes32[](totalHashes); + uint256 leafPos = 0; + uint256 hashPos = 0; + uint256 proofPos = 0; + // At each step, we compute the next hash using two values: + // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we + // get the next hash. + // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the + // `proof` array. + for (uint256 i = 0; i < totalHashes; i++) { + bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]; + bytes32 b = proofFlags[i] + ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++]) + : proof[proofPos++]; + hashes[i] = _hashPair(a, b); + } + + if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } + unchecked { + return hashes[totalHashes - 1]; + } + } else if (leavesLen > 0) { + return leaves[0]; + } else { + return proof[0]; + } + } + + /** + * @dev Sorts the pair (a, b) and hashes the result. + */ + function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { + return a < b ? _efficientHash(a, b) : _efficientHash(b, a); + } + + /** + * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. + */ + function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } +} + + +// File contracts/tokens/UP/Airdrops.sol + +// Original license: SPDX_License_Identifier: MIT +pragma solidity ^0.8.21; + + + + + + + +interface ISanctionsList { + function isSanctioned(address addr) external view returns (bool); +} + +// Custom errors +error MissingCampaign(bytes32 campaignHash); +error WrongSigner(bytes32 campaignHash, address expected); +error InvalidProof( + bytes32 campaignHash, + address recipient, + uint amount, + bytes32[] proof +); +error AlreadyClaimed(bytes32 campaignHash, address recipient, uint amount); + +error UserBlocked(address recipient); + +/** + * @title Airdrops + * @notice This contract handles airdrop claims protected by a Merkle tree and TOS signature verification. + * The contract allows only those who have signed the campaign's Terms of Service (TOS) to claim tokens. + * The owner sets up campaigns with a TOS and a corresponding Merkle root representing eligible entries. + */ +contract Airdrops is Initializable, OwnableUpgradeable, EIP712Upgradeable { + using ECDSA for bytes32; + + /// @notice Structure representing a campaign's TOS hash and its corresponding Merkle tree root. + struct Campaign { + string name; + bytes32 merkleRoot; + } + + /// @notice The ERC20 token to be airdropped (UP) + IERC20 public token; + + /// @notice Mapping from campaign name to campaign details. + mapping(bytes32 => Campaign) public campaigns; + + /// @notice Mapping of Merkle tree leaves that have been claimed. + mapping(bytes32 => mapping(bytes32 => uint)) public claimedLeafs; + + /// @notice Mapping of addresses that are blocklisted. + mapping(address => bool) public blocklist; + + /// @notice Chainalysis sanction oracle + address public chainalysisOracle; + + /// @notice Emitted when a campaign is set. + event CampaignSet(bytes32 indexed campaign, bytes32 merkleRoot); + + /// @notice Emitted when tokens are claimed. + event TokensClaimed( + bytes32 indexed campaign, + address indexed recipient, + uint256 amount + ); + + event AddedToBlockList(address indexed recipient); + event RemovedFromBlockList(address indexed recipient); + event ChainalysisOracleSet(address indexed chainalysisOracle); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice initialize that sets the token to be airdropped and initializes the owner. + * @param _token The address of the ERC20 token contract (UP). + */ + function initialize( + address _token, + address _chainalysisOracle + ) public initializer { + __Ownable_init(msg.sender); + __EIP712_init("Unlock Protocol Airdrops", "1"); + token = IERC20(_token); + setChainalysisOracle(_chainalysisOracle); + } + + function EIP712Name() public view returns (string memory) { + return _EIP712Name(); + } + + function EIP712Version() public view returns (string memory) { + return _EIP712Version(); + } + + /** + * @notice Sets the Merkle root and Terms of Service (TOS) for a campaign. + * @param campaignName The name/identifier of the campaign. + * @param root The Merkle tree root corresponding to eligible entries. + */ + function setMerkleRootForCampaign( + string calldata campaignName, + bytes32 root + ) external onlyOwner { + bytes32 campaignHash = keccak256(abi.encodePacked(campaignName)); + campaigns[campaignHash] = Campaign(campaignName, root); + emit CampaignSet(campaignHash, root); + } + + function getTosSignatureHash( + address signer, + string calldata campaignName, + uint256 timestamp + ) private view returns (bytes32) { + bytes32 structHash = keccak256( + abi.encode( + keccak256( + "TosSignature(address signer,string campaignName,uint256 timestamp)" + ), + signer, + keccak256(abi.encodePacked(campaignName)), + timestamp + ) + ); + + return _hashTypedDataV4(structHash); + } + + function verifySignature( + address signer, + string calldata campaignName, + uint256 timestamp, + bytes calldata signature + ) public view returns (bool) { + bytes32 digest = getTosSignatureHash(signer, campaignName, timestamp); + return digest.recover(signature) == signer; + } + + /** + * @notice Claims tokens for the airdrop if the recipient is eligible via the Merkle proof and has a signed TOS. + * @param campaignName The campaign identifier. + * @param timestamp The timestamp of the signature. + * @param recipient The address for whom the tokens are being claimed. + * @param amount The token amount to claim. + * @param proof The proof bytes (multiple of 32 bytes) used for Merkle proof verification. + * @param tosSignature The signature bytes (generated off-chain) of the campaign's TOS. + */ + function claim( + string calldata campaignName, + uint256 timestamp, + address recipient, + uint256 amount, + bytes32[] memory proof, + bytes calldata tosSignature + ) external { + bytes32 campaignHash = keccak256(abi.encodePacked(campaignName)); + + // Blocked users are not allowed to claim tokens. + if (isBlocked(recipient)) revert UserBlocked(recipient); + + Campaign storage campaign = campaigns[campaignHash]; + if (campaign.merkleRoot == bytes32(0)) { + revert MissingCampaign(campaignHash); + } + + // Ensure the TOS has been signed. + if (!verifySignature(recipient, campaignName, timestamp, tosSignature)) { + revert WrongSigner(campaignHash, recipient); + } + + // Compute the Merkle tree leaf. + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(recipient, amount))) + ); + + // Verify the Merkle proof. + bool valid = MerkleProof.verify(proof, campaign.merkleRoot, leaf); + if (!valid) { + revert InvalidProof(campaignHash, recipient, amount, proof); + } + + // Prevent double-claiming; check that no hash is stored yet. + if (claimedLeafs[campaignHash][leaf] != 0) + revert AlreadyClaimed(campaignHash, recipient, amount); + + // Record the claim with the leaf hash itself + claimedLeafs[campaignHash][leaf] = block.timestamp; + + // Transfer tokens to the recipient + token.transfer(recipient, amount); + emit TokensClaimed(campaignHash, recipient, amount); + } + + /** + * @notice Adds an address to the blocklist. + * @param user The address to be added to the blocklist. + */ + function addToBlocklist(address user) external onlyOwner { + blocklist[user] = true; + emit AddedToBlockList(user); + } + + /** + * @notice Removes an address from the blocklist. + * @param user The address to be removed from the blocklist. + */ + function removeFromBlocklist(address user) external onlyOwner { + blocklist[user] = false; + emit RemovedFromBlockList(user); + } + + /** + * @notice Sets the chainalysis oracle address. https://go.chainalysis.com/chainalysis-oracle-docs.html + * @param _chainalysisOracle The address of the oracle + */ + function setChainalysisOracle(address _chainalysisOracle) public onlyOwner { + chainalysisOracle = _chainalysisOracle; + emit ChainalysisOracleSet(_chainalysisOracle); + } + + /** + * @notice Check if the user is blocked or on the sanctions list. + * @param user The address to check + */ + function isBlocked(address user) public view returns (bool) { + return + blocklist[user] || ISanctionsList(chainalysisOracle).isSanctioned(user); + } +} diff --git a/packages/hardhat-helpers/src/deploy.js b/packages/hardhat-helpers/src/deploy.js index 249b6096f84..c5c4f2993cb 100644 --- a/packages/hardhat-helpers/src/deploy.js +++ b/packages/hardhat-helpers/src/deploy.js @@ -56,6 +56,7 @@ export const deployUpgradeableContract = async ( deployOptions = {} ) => { const { ethers, upgrades } = require('hardhat') + const Factory = await getContractFactory( contractNameOrFullyQualifiedNameOrEthersFactory ) diff --git a/packages/hardhat-helpers/src/tasks/deploy.js b/packages/hardhat-helpers/src/tasks/deploy.js index c7e59bfdc82..1bbb8dbacf6 100644 --- a/packages/hardhat-helpers/src/tasks/deploy.js +++ b/packages/hardhat-helpers/src/tasks/deploy.js @@ -33,13 +33,13 @@ const initializeDeployTask = () => } if (!proxied) { - ;({ address, hash } = await deployContract(qualified, params)) + ; ({ address, hash } = await deployContract(qualified, params)) } else { console.log(`Deploying using a Transparent proxy:`) - ;({ address, hash } = await deployUpgradeableContract( - qualified, - params - )) + ; ({ address, hash } = await deployUpgradeableContract( + qualified, + params + )) } console.log(`Contract ${qualified} deployed at ${address} (tx: ${hash})`) }) diff --git a/smart-contracts/contracts/tokens/UP/Airdrops.sol b/smart-contracts/contracts/tokens/UP/Airdrops.sol new file mode 100644 index 00000000000..70980724f72 --- /dev/null +++ b/smart-contracts/contracts/tokens/UP/Airdrops.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts-upgradeable5/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable5/proxy/utils/Initializable.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable5/utils/cryptography/EIP712Upgradeable.sol"; + +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +interface ISanctionsList { + function isSanctioned(address addr) external view returns (bool); +} + +// Custom errors +error MissingCampaign(bytes32 campaignHash); +error WrongSigner(bytes32 campaignHash, address expected); +error InvalidProof( + bytes32 campaignHash, + address recipient, + uint amount, + bytes32[] proof +); +error AlreadyClaimed(bytes32 campaignHash, address recipient, uint amount); + +error UserBlocked(address recipient); + +/** + * @title Airdrops + * @notice This contract handles airdrop claims protected by a Merkle tree and TOS signature verification. + * The contract allows only those who have signed the campaign's Terms of Service (TOS) to claim tokens. + * The owner sets up campaigns with a TOS and a corresponding Merkle root representing eligible entries. + */ +contract Airdrops is Initializable, OwnableUpgradeable, EIP712Upgradeable { + using ECDSA for bytes32; + + /// @notice Structure representing a campaign's TOS hash and its corresponding Merkle tree root. + struct Campaign { + string name; + bytes32 merkleRoot; + } + + /// @notice The ERC20 token to be airdropped (UP) + IERC20 public token; + + /// @notice Mapping from campaign name to campaign details. + mapping(bytes32 => Campaign) public campaigns; + + /// @notice Mapping of Merkle tree leaves that have been claimed. + mapping(bytes32 => mapping(bytes32 => uint)) public claimedLeafs; + + /// @notice Mapping of addresses that are blocklisted. + mapping(address => bool) public blocklist; + + /// @notice Chainalysis sanction oracle + address public chainalysisOracle; + + /// @notice Emitted when a campaign is set. + event CampaignSet(bytes32 indexed campaign, bytes32 merkleRoot); + + /// @notice Emitted when tokens are claimed. + event TokensClaimed( + bytes32 indexed campaign, + address indexed recipient, + uint256 amount + ); + + event AddedToBlockList(address indexed recipient); + event RemovedFromBlockList(address indexed recipient); + event ChainalysisOracleSet(address indexed chainalysisOracle); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice initialize that sets the token to be airdropped and initializes the owner. + * @param _token The address of the ERC20 token contract (UP). + */ + function initialize( + address _token, + address _chainalysisOracle + ) public initializer { + __Ownable_init(msg.sender); + __EIP712_init("Unlock Protocol Airdrops", "1"); + token = IERC20(_token); + setChainalysisOracle(_chainalysisOracle); + } + + function EIP712Name() public view returns (string memory) { + return _EIP712Name(); + } + + function EIP712Version() public view returns (string memory) { + return _EIP712Version(); + } + + /** + * @notice Sets the Merkle root and Terms of Service (TOS) for a campaign. + * @param campaignName The name/identifier of the campaign. + * @param root The Merkle tree root corresponding to eligible entries. + */ + function setMerkleRootForCampaign( + string calldata campaignName, + bytes32 root + ) external onlyOwner { + bytes32 campaignHash = keccak256(abi.encodePacked(campaignName)); + campaigns[campaignHash] = Campaign(campaignName, root); + emit CampaignSet(campaignHash, root); + } + + function getTosSignatureHash( + address signer, + string calldata campaignName, + uint256 timestamp + ) private view returns (bytes32) { + bytes32 structHash = keccak256( + abi.encode( + keccak256( + "TosSignature(address signer,string campaignName,uint256 timestamp)" + ), + signer, + keccak256(abi.encodePacked(campaignName)), + timestamp + ) + ); + + return _hashTypedDataV4(structHash); + } + + function verifySignature( + address signer, + string calldata campaignName, + uint256 timestamp, + bytes calldata signature + ) public view returns (bool) { + bytes32 digest = getTosSignatureHash(signer, campaignName, timestamp); + return digest.recover(signature) == signer; + } + + /** + * @notice Claims tokens for the airdrop if the recipient is eligible via the Merkle proof and has a signed TOS. + * @param campaignName The campaign identifier. + * @param timestamp The timestamp of the signature. + * @param recipient The address for whom the tokens are being claimed. + * @param amount The token amount to claim. + * @param proof The proof bytes (multiple of 32 bytes) used for Merkle proof verification. + * @param tosSignature The signature bytes (generated off-chain) of the campaign's TOS. + */ + function claim( + string calldata campaignName, + uint256 timestamp, + address recipient, + uint256 amount, + bytes32[] memory proof, + bytes calldata tosSignature + ) external { + bytes32 campaignHash = keccak256(abi.encodePacked(campaignName)); + + // Blocked users are not allowed to claim tokens. + if (isBlocked(recipient)) revert UserBlocked(recipient); + + Campaign storage campaign = campaigns[campaignHash]; + if (campaign.merkleRoot == bytes32(0)) { + revert MissingCampaign(campaignHash); + } + + // Ensure the TOS has been signed. + if (!verifySignature(recipient, campaignName, timestamp, tosSignature)) { + revert WrongSigner(campaignHash, recipient); + } + + // Compute the Merkle tree leaf. + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(recipient, amount))) + ); + + // Verify the Merkle proof. + bool valid = MerkleProof.verify(proof, campaign.merkleRoot, leaf); + if (!valid) { + revert InvalidProof(campaignHash, recipient, amount, proof); + } + + // Prevent double-claiming; check that no hash is stored yet. + if (claimedLeafs[campaignHash][leaf] != 0) + revert AlreadyClaimed(campaignHash, recipient, amount); + + // Record the claim with the leaf hash itself + claimedLeafs[campaignHash][leaf] = block.timestamp; + + // Transfer tokens to the recipient + token.transfer(recipient, amount); + emit TokensClaimed(campaignHash, recipient, amount); + } + + /** + * @notice Adds an address to the blocklist. + * @param user The address to be added to the blocklist. + */ + function addToBlocklist(address user) external onlyOwner { + blocklist[user] = true; + emit AddedToBlockList(user); + } + + /** + * @notice Removes an address from the blocklist. + * @param user The address to be removed from the blocklist. + */ + function removeFromBlocklist(address user) external onlyOwner { + blocklist[user] = false; + emit RemovedFromBlockList(user); + } + + /** + * @notice Sets the chainalysis oracle address. https://go.chainalysis.com/chainalysis-oracle-docs.html + * @param _chainalysisOracle The address of the oracle + */ + function setChainalysisOracle(address _chainalysisOracle) public onlyOwner { + chainalysisOracle = _chainalysisOracle; + emit ChainalysisOracleSet(_chainalysisOracle); + } + + /** + * @notice Check if the user is blocked or on the sanctions list. + * @param user The address to check + */ + function isBlocked(address user) public view returns (bool) { + return + blocklist[user] || ISanctionsList(chainalysisOracle).isSanctioned(user); + } +} diff --git a/smart-contracts/contracts/utils/SanctionsList.sol b/smart-contracts/contracts/utils/SanctionsList.sol new file mode 100644 index 00000000000..2d93ae70036 --- /dev/null +++ b/smart-contracts/contracts/utils/SanctionsList.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract SanctionsList { + mapping(address => bool) public list; + + constructor() {} + + function addToList(address account) external { + list[account] = true; + } + + function isSanctioned(address account) external view returns (bool) { + return list[account]; + } +} diff --git a/smart-contracts/test/UPToken/airdrops.js b/smart-contracts/test/UPToken/airdrops.js new file mode 100644 index 00000000000..fbc7eb06c31 --- /dev/null +++ b/smart-contracts/test/UPToken/airdrops.js @@ -0,0 +1,330 @@ +const { expect } = require('chai') +const { StandardMerkleTree } = require('@openzeppelin/merkle-tree') +const { ethers } = require('hardhat') +const { upgrades } = require('hardhat') +const { reverts } = require('../helpers') + +describe('Airdrops Contract', function () { + let owner, recipient + let token, airdrops, sanctionsList + + const airdropTokens = ethers.parseEther('10000') // 10,000 tokens for airdrop + const campaignName = 'Campaign1' + const campaignHash = ethers.keccak256(ethers.toUtf8Bytes(campaignName)) + const claimAmount = ethers.parseEther('100') // Claim 100 tokens + + /** + * Helper to compute the leaf value. + * + * Replicates the Solidity computation: + * innerHash = keccak256(abi.encode(recipient, amount)) + * leaf = keccak256(bytes.concat(innerHash)) + * + * @param {string} recipientAddr - Recipient address. + * @param {BigNumber} amount - Claim amount. + * @returns {string} - The computed leaf as a hex string. + */ + function computeLeaf(recipientAddr, amount) { + // Use ABI encoding to match the contract's abi.encode + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256'], + [recipientAddr, amount] + ) + // First hash (matches keccak256(abi.encode(...))) + const innerHash = ethers.keccak256(encoded) + // Second hash (matches keccak256(bytes.concat(...))) + return ethers.keccak256(innerHash) + } + + async function getSignature(recipient, signer) { + const domain = { + name: await airdrops.EIP712Name(), + version: await airdrops.EIP712Version(), + chainId: (await ethers.provider.getNetwork()).chainId, // Mainnet (Change for testnets) + verifyingContract: await airdrops.getAddress(), + } + + const types = { + TosSignature: [ + { name: 'signer', type: 'address' }, + { name: 'campaignName', type: 'string' }, + { name: 'timestamp', type: 'uint256' }, + ], + } + + const timestamp = new Date().getTime() + const value = { + signer: recipient.address, + campaignName, + timestamp, + } + + // Signing Typed Data (EIP-712) + return { + timestamp, + tosSignature: await signer.signTypedData(domain, types, value), + } + } + + beforeEach(async function () { + ;[owner, recipient] = await ethers.getSigners() + + // Deploy UPToken + const UP = await ethers.getContractFactory('UPToken') + token = await upgrades.deployProxy(UP, [await owner.getAddress()]) + await token.waitForDeployment() + + // Mint tokens to the owner address (which is owner in this test) + await token.mint(owner.address) + + // Deploy a SanctionsList contract + const SanctionsList = await ethers.getContractFactory('SanctionsList') + sanctionsList = await SanctionsList.deploy() + + // Deploy the Airdrops contract + const Airdrops = await ethers.getContractFactory('Airdrops') + airdrops = await upgrades.deployProxy(Airdrops, [ + await token.getAddress(), + await sanctionsList.getAddress(), + ]) + await airdrops.waitForDeployment() + + // Transfer tokens to the Airdrops contract + await token.transfer(await airdrops.getAddress(), airdropTokens) + }) + + describe('Campaign setup', function () { + it('should allow owner to set campaign and store correct values', async function () { + // Compute a valid leaf for the campaign (using recipient and claimAmount). + const leaf = computeLeaf(recipient.address, claimAmount) + + // Set the campaign with a valid Merkle root. + const tx = await airdrops.setMerkleRootForCampaign(campaignName, leaf) + await tx.wait() + + // Retrieve the campaign details. + const campaign = await airdrops.campaigns(campaignHash) + expect(campaign.merkleRoot).to.equal(leaf) + expect(campaign.name).to.equal(campaignName) + }) + + it('should revert if non-owner attempts to set campaign', async function () { + const leaf = computeLeaf(recipient.address, claimAmount) + await reverts( + airdrops + .connect(recipient) + .setMerkleRootForCampaign(campaignHash, leaf), + `OwnableUnauthorizedAccount("${recipient.address}")` + ) + }) + }) + + describe('Token Claim', function () { + let tree + beforeEach(async function () { + // create a merkle tree with the recipient and claim amount + tree = StandardMerkleTree.of( + [[recipient.address, claimAmount.toString()]], + ['address', 'uint256'] + ) + + // Get the root + const root = tree.root + + // Set the campaign with the proper Merkle root + await airdrops.setMerkleRootForCampaign(campaignName, root) + }) + + it('should fail to claim if the signature for TOS is not valid', async function () { + const [user, recipient] = await ethers.getSigners() + // Check initial token balance. + const initialBal = await token.balanceOf(recipient.address) + let proof + + for (const [i, v] of tree.entries()) { + if (v[0] === recipient.address) { + proof = tree.getProof(i) + } + } + + const { tosSignature, timestamp } = await getSignature(recipient, user) + + // Execute the claim with the valid proof - use formatted proof + await reverts( + airdrops.claim( + campaignName, + timestamp, + recipient.address, + claimAmount, + proof, + tosSignature + ), + `WrongSigner("${campaignHash}", "${recipient.address}")` + ) + }) + + it('should claim tokens successfully with a valid proof', async function () { + const [, recipient] = await ethers.getSigners() + // Check initial token balance. + const initialBal = await token.balanceOf(recipient.address) + const initialBalAirdrops = await token.balanceOf( + await airdrops.getAddress() + ) + let proof + + for (const [i, v] of tree.entries()) { + if (v[0] === recipient.address) { + proof = tree.getProof(i) + } + } + + const { tosSignature, timestamp } = await getSignature( + recipient, + recipient + ) + + // Execute the claim with the valid proof - use formatted proof + const tx = await airdrops.claim( + campaignName, + timestamp, + recipient.address, + claimAmount, + proof, + tosSignature + ) + await tx.wait() + + // Verify that the recipient received the correct tokens. + const finalBal = await token.balanceOf(recipient.address) + expect(finalBal - initialBal).to.equal(claimAmount) + + const finalBalAirdrops = await token.balanceOf( + await airdrops.getAddress() + ) + expect(initialBalAirdrops - finalBalAirdrops).to.equal(claimAmount) + }) + + it('should prevent a user from claiming twice', async function () { + const [, recipient] = await ethers.getSigners() + // Check initial token balance. + const initialBal = await token.balanceOf(recipient.address) + let proof + + for (const [i, v] of tree.entries()) { + if (v[0] === recipient.address) { + proof = tree.getProof(i) + } + } + + const { tosSignature, timestamp } = await getSignature( + recipient, + recipient + ) + + // Execute the claim with the valid proof - use formatted proof + const tx = await airdrops.claim( + campaignName, + timestamp, + recipient.address, + claimAmount, + proof, + tosSignature + ) + await tx.wait() + await reverts( + airdrops.claim( + campaignName, + timestamp, + recipient.address, + claimAmount, + proof, + tosSignature + ), + `AlreadyClaimed("${campaignHash}", "${recipient.address}", ${claimAmount})` + ) + }) + + it('should revert claim with an invalid proof', async function () { + // Provide an invalid Merkle proof. + const invalidProof = [ + '0x1234567890123456789012345678901234567890123456789012345678901234', + ] + const { tosSignature, timestamp } = await getSignature( + recipient, + recipient + ) + + await reverts( + airdrops.claim( + campaignName, + timestamp, + recipient.address, + claimAmount, + invalidProof, + tosSignature + ), + `InvalidProof("${campaignHash}", "${recipient.address}", ${claimAmount}, ${JSON.stringify(invalidProof)})` + ) + }) + + it('should revert claim for a blocklisted user', async function () { + // Blocklist the recipient. + await airdrops.addToBlocklist(recipient.address) + + let proof + for (const [i, v] of tree.entries()) { + if (v[0] === recipient.address) { + proof = tree.getProof(i) + } + } + + const { tosSignature, timestamp } = await getSignature( + recipient, + recipient + ) + + // Use the valid proof + await reverts( + airdrops.claim( + campaignName, + timestamp, + recipient.address, + claimAmount, + proof, + tosSignature + ), + `UserBlocked("${recipient.address}")` + ) + }) + + it('should revert claim for a user on the sanction list contract', async function () { + // Blocklist the recipient. + await sanctionsList.addToList(recipient.address) + + let proof + for (const [i, v] of tree.entries()) { + if (v[0] === recipient.address) { + proof = tree.getProof(i) + } + } + + const { tosSignature, timestamp } = await getSignature( + recipient, + recipient + ) + + // Use the valid proof + await reverts( + airdrops.claim( + campaignName, + timestamp, + recipient.address, + claimAmount, + proof, + tosSignature + ), + `UserBlocked("${recipient.address}")` + ) + }) + }) +})