diff --git a/addresses/address.json b/addresses/address.json index a309d2db..954e974f 100755 --- a/addresses/address.json +++ b/addresses/address.json @@ -309,7 +309,9 @@ "EnterpriseFeeCollector": "0x8c98ea273bA22327F896Aa1a1a46E1BFf56e9b1D", "FixedPriceEnterprise": "0xfa48673a7C36A2A768f89AC1ee8C355D5c367B02", "AccessListFactory": "0x43eC0a34E1b70C7f8E579ab866F37642777727E7", - "EnterpriseEscrow": "0x49E35cd2bAE043Abd9074B6e5a649a5AdEB05C33" + "EnterpriseEscrow": "0x49E35cd2bAE043Abd9074B6e5a649a5AdEB05C33", + "COMPY":"0x973e69303259B0c2543a38665122b773D28405fB", + "COMPYFaucet":"0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4" }, "oasis_sapphire": { "chainId": 23294, @@ -416,6 +418,8 @@ "BatchPayments": "0xFe7967A5176fDAFa8DE109b3507016B885a82D6e", "OPFCommunityFeeCollectorCompute": "0x7b0576CF01E868bce46cca91b2a8E674141b0355", "Escrow": "0xf0c7A31D7Ee26bEBfb4BAD8e37490bEadE3F846f", - "AccessListFactory": "0xE5aa2C9B551aFcA4C0A98BB3B37D7A43084d0a66" + "AccessListFactory": "0xE5aa2C9B551aFcA4C0A98BB3B37D7A43084d0a66", + "COMPY":"0x298f163244e0c8cc9316D6E97162e5792ac5d410", + "COMPYFaucet":"0x23A8b2D7176485a6349e4830605F323f31019333" } } \ No newline at end of file diff --git a/contracts/grants/GrantsTokenFaucet.sol b/contracts/grants/GrantsTokenFaucet.sol new file mode 100644 index 00000000..1432695e --- /dev/null +++ b/contracts/grants/GrantsTokenFaucet.sol @@ -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); + } +} diff --git a/scripts/deploy_grants.js b/scripts/deploy_grants.js index 8f0ea0f3..9b50613c 100644 --- a/scripts/deploy_grants.js +++ b/scripts/deploy_grants.js @@ -42,9 +42,25 @@ async function main() { let RouterAddress = null; gasLimit = 6500000; gasPrice = ethers.utils.parseUnits("0.08", "gwei"); - const networkName = "base"; + let networkName = null; const grantsOwner = "0x09b575B5eC7Fff24cbccC092DE9E36eADdDbEe71"; - + switch (networkDetails.chainId) { + case 11155111: + networkName = "sepolia"; + gasPrice = ethers.utils.parseUnits("1.1", "gwei"); + gasLimit = 6000000; + break; + case 8453: + networkName = "base"; + gasPrice = ethers.utils.parseUnits("0.08", "gwei"); + gasLimit = 20000000; + // grantsTokenAddress = "0x09b575B5eC7Fff24cbccC092DE9E36eADdDbEe71"; + break; + } + if (!networkName) { + console.error("Invalid network. Aborting.."); + return null; + } let options; if (gasPrice) { options = { gasLimit: gasLimit, gasPrice: gasPrice }; diff --git a/scripts/deploy_grants_faucet.js b/scripts/deploy_grants_faucet.js new file mode 100644 index 00000000..dfbb7e33 --- /dev/null +++ b/scripts/deploy_grants_faucet.js @@ -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