-
Notifications
You must be signed in to change notification settings - Fork 66
add faucet for grants token #996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| pragma solidity 0.8.12; | ||
| // Copyright Ocean Protocol contributors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import '../interfaces/IERC20.sol'; | ||
| import '../utils/Ownable.sol'; | ||
| import '../utils/SafeERC20.sol'; | ||
|
|
||
| /** | ||
| * @title GrantsTokenFaucet | ||
| * @dev Faucet contract for GrantsToken that uses signature-based authentication. | ||
| * Backend validates user info and signs a message containing: userAddress, nonce, amount, userAddress. | ||
| * Users can claim tokens by providing a valid signature from the authorized signer. | ||
| */ | ||
| contract GrantsTokenFaucet is Ownable { | ||
| using SafeERC20 for IERC20; | ||
|
|
||
| // The token that will be distributed | ||
| IERC20 public immutable token; | ||
|
|
||
| // The address authorized to sign claim messages | ||
| address public signer; | ||
|
|
||
| // Mapping from user address to their last used nonce | ||
| mapping(address => uint256) public userNonces; | ||
|
|
||
| // Events | ||
| event TokensClaimed( | ||
| address indexed user, | ||
| uint256 nonce, | ||
| uint256 amount | ||
| ); | ||
|
|
||
| event SignerChanged( | ||
| address indexed oldSigner, | ||
| address indexed newSigner | ||
| ); | ||
|
|
||
| /** | ||
| * @dev Constructor | ||
| * @param _token Address of the GrantsToken contract | ||
| * @param _signer Address authorized to sign claim messages | ||
| */ | ||
| constructor(address _token, address _signer) { | ||
| require(_token != address(0), "GrantsTokenFaucet: invalid token address"); | ||
| require(_signer != address(0), "GrantsTokenFaucet: invalid signer address"); | ||
|
|
||
| token = IERC20(_token); | ||
| signer = _signer; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Claim tokens using a signature from the authorized signer | ||
| * @param userAddress Address of the user claiming tokens | ||
| * @param nonce Nonce for this claim (must be greater than user's last used nonce) | ||
| * @param amount Amount of tokens to claim | ||
| * @param signature Signature from the authorized signer | ||
| */ | ||
| function claim( | ||
| address userAddress, | ||
| uint256 nonce, | ||
| uint256 amount, | ||
| bytes calldata signature | ||
| ) external { | ||
| require(userAddress != address(0), "GrantsTokenFaucet: invalid user address"); | ||
| require(amount > 0, "GrantsTokenFaucet: amount must be greater than zero"); | ||
| require(nonce > userNonces[userAddress], "GrantsTokenFaucet: nonce must be greater than last used nonce"); | ||
|
|
||
| // Verify signature | ||
| // Include contract address to prevent cross-contract replay attacks | ||
| bytes32 messageHash = keccak256( | ||
| abi.encode(address(this), userAddress, nonce, amount) | ||
| ); | ||
| bytes32 ethSignedMessageHash = keccak256( | ||
| abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash) | ||
| ); | ||
|
|
||
| address recoveredSigner = _recoverSigner(ethSignedMessageHash, signature); | ||
| require(recoveredSigner != address(0), "GrantsTokenFaucet: invalid signature recovery"); | ||
| require(recoveredSigner == signer, "GrantsTokenFaucet: invalid signature"); | ||
|
|
||
| // Update user nonce | ||
| userNonces[userAddress] = nonce; | ||
| emit TokensClaimed(userAddress, nonce, amount); | ||
| // Transfer tokens to user | ||
| token.safeTransfer(userAddress, amount); | ||
|
|
||
|
|
||
| } | ||
|
|
||
| /** | ||
| * @dev Change the authorized signer (only owner) | ||
| * @param newSigner Address of the new authorized signer | ||
| */ | ||
| function setSigner(address newSigner) external onlyOwner { | ||
| require(newSigner != address(0), "GrantsTokenFaucet: invalid signer address"); | ||
| require(newSigner != signer, "GrantsTokenFaucet: signer is already set to this address"); | ||
|
|
||
| address oldSigner = signer; | ||
| signer = newSigner; | ||
|
|
||
| emit SignerChanged(oldSigner, newSigner); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Get the authorized signer address | ||
| * @return The address of the authorized signer | ||
| */ | ||
| function getSigner() external view returns (address) { | ||
| return signer; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Get the last used nonce for a user | ||
| * @param user Address of the user | ||
| * @return The last used nonce | ||
| */ | ||
| function getUserNonce(address user) external view returns (uint256) { | ||
| return userNonces[user]; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Recover signer address from signature | ||
| * @param hash The message hash | ||
| * @param signature The signature | ||
| * @return The recovered signer address | ||
| */ | ||
| function _recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address) { | ||
| require(signature.length == 65, "GrantsTokenFaucet: invalid signature length"); | ||
|
|
||
| bytes32 r; | ||
| bytes32 s; | ||
| uint8 v; | ||
|
|
||
| assembly { | ||
| r := mload(add(signature, 32)) | ||
| s := mload(add(signature, 64)) | ||
| v := and(mload(add(signature, 65)), 255) | ||
| } | ||
|
|
||
| // Prevent signature malleability: s must be in the lower half of the secp256k1 curve order | ||
| require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, | ||
| "GrantsTokenFaucet: invalid signature s value"); | ||
|
|
||
| // Handle v values 0-1 (EIP-155) and 27-28 (legacy) | ||
| if (v < 27) { | ||
| v += 27; | ||
| } | ||
|
|
||
| require(v == 27 || v == 28, "GrantsTokenFaucet: invalid signature v value"); | ||
|
|
||
| address recovered = ecrecover(hash, v, r, s); | ||
| require(recovered != address(0), "GrantsTokenFaucet: invalid signature recovery"); | ||
|
|
||
| return recovered; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Emergency function to withdraw tokens (only owner) | ||
| * Useful if tokens need to be recovered or contract needs to be upgraded | ||
| * @param amount Amount of tokens to withdraw | ||
| */ | ||
| function withdrawTokens(uint256 amount) external onlyOwner { | ||
| token.safeTransfer(owner(), amount); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Emergency function to withdraw all tokens (only owner) | ||
| */ | ||
| function withdrawAllTokens() external onlyOwner { | ||
| uint256 balance = token.balanceOf(address(this)); | ||
| token.safeTransfer(owner(), balance); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // We require the Hardhat Runtime Environment explicitly here. This is optional | ||
| // but useful for running the script in a standalone fashion through `node <script>`. | ||
| // | ||
| // When running the script with `hardhat run <script>` you'll find the Hardhat | ||
| // Runtime Environment's members available in the global scope. | ||
| const hre = require("hardhat"); | ||
| const fs = require("fs"); | ||
| const { address } = require("../test/helpers/constants"); | ||
| const { Wallet } = require("ethers"); | ||
| const { UV_FS_O_FILEMAP } = require("constants"); | ||
| const ethers = hre.ethers; | ||
| require("dotenv").config(); | ||
| const logging = true; | ||
| const show_verify = true; | ||
| async function main() { | ||
| const url = process.env.NETWORK_RPC_URL; | ||
| console.log("Using RPC: " + url); | ||
| if (!url) { | ||
| console.error("Missing NETWORK_RPC_URL. Aborting.."); | ||
| return null; | ||
| } | ||
|
|
||
| const provider = new ethers.providers.JsonRpcProvider(url); | ||
| const network = provider.getNetwork(); | ||
| // utils | ||
| const networkDetails = await network; | ||
|
|
||
| let wallet; | ||
| if (process.env.MNEMONIC) | ||
| wallet = new Wallet.fromMnemonic(process.env.MNEMONIC); | ||
| if (process.env.PRIVATE_KEY) wallet = new Wallet(process.env.PRIVATE_KEY); | ||
| if (!wallet) { | ||
| console.error("Missing MNEMONIC or PRIVATE_KEY. Aborting.."); | ||
| return null; | ||
| } | ||
| owner = wallet.connect(provider); | ||
| let gasLimit = 3000000; | ||
| let gasPrice = null; | ||
| let sleepAmount = 10; | ||
| let OPFOwner = null; | ||
| let RouterAddress = null; | ||
| let grantsTokenAddress = null; | ||
| let signerAddress = null; | ||
| let compyAddress = null; | ||
| switch (networkDetails.chainId) { | ||
| case 11155111: | ||
| networkName = "sepolia"; | ||
| gasPrice = ethers.utils.parseUnits("12", "gwei"); | ||
| gasLimit = 6000000; | ||
| compyAddress = "0x973e69303259B0c2543a38665122b773D28405fB"; | ||
| signerAddress = "0xDAcDC497AE9a678a78b703cD83B010C8f5c78E37"; | ||
| break; | ||
| case 8453: | ||
| networkName = "base"; | ||
| gasPrice = ethers.utils.parseUnits("0.02", "gwei"); | ||
| gasLimit = 5000000; | ||
| compyAddress = "0x298f163244e0c8cc9316D6E97162e5792ac5d410"; | ||
| signerAddress = "0x508F31c8d2a1B8cEE5360FA41bc0469923986C9B"; | ||
| break; | ||
| } | ||
| if (!compyAddress || !signerAddress) { | ||
| console.error("Invalid network. Aborting.."); | ||
| return null; | ||
| } | ||
| let options; | ||
| if (gasPrice) { | ||
| options = { gasLimit: gasLimit, gasPrice: gasPrice }; | ||
| } else { | ||
| options = { gasLimit }; | ||
| } | ||
| console.log("Deploying contracts with the account:", owner.address); | ||
| console.log("Deployer nonce:", await owner.getTransactionCount()); | ||
|
|
||
| if (logging) console.info("Deploying GrantsTokenFaucet"); | ||
| const GrantsTokenFaucet = await ethers.getContractFactory("GrantsTokenFaucet", owner); | ||
| const deployGrantsTokenFaucet = await GrantsTokenFaucet.connect(owner).deploy( | ||
| compyAddress, | ||
| signerAddress, | ||
| options | ||
| ); | ||
| await deployGrantsTokenFaucet.deployTransaction.wait(2); | ||
|
|
||
| if (logging) console.info("GrantsTokenFaucet deployed at:", deployGrantsTokenFaucet.address); | ||
|
|
||
| if (show_verify) { | ||
| console.log("\tRun the following to verify on etherscan"); | ||
| console.log( | ||
| "\tnpx hardhat verify --network " + | ||
| networkName + | ||
| " " + | ||
| deployGrantsTokenFaucet.address + | ||
| " " + | ||
| compyAddress + | ||
| " " + | ||
| signerAddress | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // We recommend this pattern to be able to use async/await everywhere | ||
| // and properly handle errors. | ||
| main() | ||
| .then(() => process.exit(0)) | ||
| .catch((error) => { | ||
| console.error(error); | ||
| process.exit(1); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / Slither
Assembly usage Warning