Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions addresses/address.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,9 @@
"EnterpriseFeeCollector": "0x8c98ea273bA22327F896Aa1a1a46E1BFf56e9b1D",
"FixedPriceEnterprise": "0xfa48673a7C36A2A768f89AC1ee8C355D5c367B02",
"AccessListFactory": "0x43eC0a34E1b70C7f8E579ab866F37642777727E7",
"EnterpriseEscrow": "0x49E35cd2bAE043Abd9074B6e5a649a5AdEB05C33"
"EnterpriseEscrow": "0x49E35cd2bAE043Abd9074B6e5a649a5AdEB05C33",
"COMPY":"0x973e69303259B0c2543a38665122b773D28405fB",
"COMPYFaucet":"0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4"
},
"oasis_sapphire": {
"chainId": 23294,
Expand Down Expand Up @@ -416,6 +418,8 @@
"BatchPayments": "0xFe7967A5176fDAFa8DE109b3507016B885a82D6e",
"OPFCommunityFeeCollectorCompute": "0x7b0576CF01E868bce46cca91b2a8E674141b0355",
"Escrow": "0xf0c7A31D7Ee26bEBfb4BAD8e37490bEadE3F846f",
"AccessListFactory": "0xE5aa2C9B551aFcA4C0A98BB3B37D7A43084d0a66"
"AccessListFactory": "0xE5aa2C9B551aFcA4C0A98BB3B37D7A43084d0a66",
"COMPY":"0x298f163244e0c8cc9316D6E97162e5792ac5d410",
"COMPYFaucet":"0x23A8b2D7176485a6349e4830605F323f31019333"
}
}
174 changes: 174 additions & 0 deletions contracts/grants/GrantsTokenFaucet.sol
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;
}
Comment on lines +128 to +156

Check warning

Code scanning / Slither

Assembly usage Warning


/**
* @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);
}
}
20 changes: 18 additions & 2 deletions scripts/deploy_grants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
107 changes: 107 additions & 0 deletions scripts/deploy_grants_faucet.js
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);
});
Loading
Loading