Skip to content
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

feat: add faucet contract #110

Merged
merged 10 commits into from
Oct 15, 2024
61 changes: 61 additions & 0 deletions script/18_SimulateReceive.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import "../test/mocks/AssetsMock.sol";
import "../test/mocks/DelegationMock.sol";

import "../src/interfaces/precompiles/IAssets.sol";
import "../src/interfaces/precompiles/IDelegation.sol";

import {Script, console} from "forge-std/Script.sol";
import {StdCheats} from "forge-std/StdCheats.sol";
import "forge-std/StdJson.sol";

import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol";

import {Origin} from "../src/lzApp/OAppReceiverUpgradeable.sol";

contract SimulateReceive is Script, StdCheats {
MaxMustermann2 marked this conversation as resolved.
Show resolved Hide resolved

using stdJson for string;

function setUp() public {
// always monkey-patch a precompile, since with LZ we need them
// TODO: AssetsMock may still complain about a few things.
deployCodeTo("AssetsMock.sol", abi.encode(uint16(40_161)), ASSETS_PRECOMPILE_ADDRESS);
deployCodeTo("DelegationMock.sol", DELEGATION_PRECOMPILE_ADDRESS);
}
MaxMustermann2 marked this conversation as resolved.
Show resolved Hide resolved

function run() public {
// https://scan-testnet.layerzero-api.com/v1/messages/tx/<hash>
string memory json = vm.readFile("./scanApiResponse.json");
uint32 srcEid = uint32(json.readUint(".data[0].pathway.srcEid"));
require(srcEid != 0, "srcEid should not be empty");
address senderAddress = json.readAddress(".data[0].pathway.sender.address");
require(senderAddress != address(0), "senderAddress should not be empty");
uint64 nonce = uint64(json.readUint(".data[0].pathway.nonce"));
require(nonce != 0, "nonce should not be empty");
bytes32 sender = addressToBytes32(senderAddress);
require(sender != bytes32(0), "sender should not be empty");
address receiver = json.readAddress(".data[0].pathway.receiver.address");
require(receiver != address(0), "receiver should not be empty");
bytes32 guid = json.readBytes32(".data[0].guid");
require(guid != bytes32(0), "guid should not be empty");
bytes memory payload = json.readBytes(".data[0].source.tx.payload");
require(payload.length != 0, "payload should not be empty");

MaxMustermann2 marked this conversation as resolved.
Show resolved Hide resolved
Origin memory origin = Origin({srcEid: srcEid, sender: sender, nonce: nonce});
bytes memory extraData = "";
vm.startBroadcast();
bytes memory encoded = abi.encodeWithSelector(
IOAppCore(receiver).endpoint().lzReceive.selector, origin, receiver, guid, payload, extraData
);
console.logBytes(encoded);
IOAppCore(receiver).endpoint().lzReceive(origin, receiver, guid, payload, extraData);
}

function addressToBytes32(address _addr) internal pure returns (bytes32) {
return bytes32(uint256(uint160(_addr)));
}

}
48 changes: 48 additions & 0 deletions script/19_DeployFaucet.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
pragma solidity ^0.8.19;

import {CombinedFaucet} from "../src/core/CombinedFaucet.sol";
import {BaseScript} from "./BaseScript.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

import "forge-std/Script.sol";

contract DeployScript is BaseScript {

address tokenAddr;
bool exoEthFaucet;
address faucetOwner;
MaxMustermann2 marked this conversation as resolved.
Show resolved Hide resolved

function setUp() public virtual override {
super.setUp();

string memory prerequisites = vm.readFile("script/prerequisiteContracts.json");

tokenAddr = stdJson.readAddress(prerequisites, ".clientChain.erc20Token");
require(tokenAddr != address(0), "token address should not be empty");

clientChain = vm.createSelectFork(clientChainRPCURL);
exocore = vm.createSelectFork(exocoreRPCURL);

exoEthFaucet = vm.envBool("EXO_ETH_FAUCET");
// for native token, using a different owner is better since the private key is exposed on the backend
faucetOwner = vm.envAddress("FAUCET_OWNER");
}

function run() public {
vm.selectFork(exoEthFaucet ? clientChain : exocore);
vm.startBroadcast(exocoreValidatorSet.privateKey);
address proxyAdmin = address(new ProxyAdmin());
CombinedFaucet faucetLogic = new CombinedFaucet();
CombinedFaucet faucet = CombinedFaucet(
payable(address(new TransparentUpgradeableProxy(address(faucetLogic), address(proxyAdmin), "")))
);
faucet.initialize(exocoreValidatorSet.addr, exoEthFaucet ? tokenAddr : address(0), 1 ether);
vm.stopBroadcast();
// do not store them as JSON since the address is intentionally kept private
console.log("faucet", address(faucet));
console.log("faucetLogic", address(faucetLogic));
console.log("proxyAdmin", address(proxyAdmin));
}

}
155 changes: 155 additions & 0 deletions src/core/CombinedFaucet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";

import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

contract CombinedFaucet is
IERC165,
IERC1155Receiver,
IERC721Receiver,
Initializable,
PausableUpgradeable,
OwnableUpgradeable,
ReentrancyGuardUpgradeable
{

address public token;
uint256 public tokenAmount;
uint256 public constant ONE_DAY = 1 days;

mapping(address => uint256) public lastRequestTime;

event TokenAddressUpdated(address newTokenAddress);
event TokenAmountUpdated(uint256 newTokenAmount);
event TokensRequested(address indexed user, uint256 amount);

constructor() {
_disableInitializers();
}

/// @dev Initialize the contract, set the owner, token address, and token amount
/// @param owner_ The owner of the contract
/// @param token_ The address of the token to distribute
/// @param tokenAmount_ The amount of tokens to distribute at each request
function initialize(address owner_, address token_, uint256 tokenAmount_) public initializer {
// The token address can be 0 to support native token
// slither-disable-next-line missing-zero-check
token = token_;
tokenAmount = tokenAmount_;

_transferOwnership(owner_);
__Pausable_init_unchained();
__ReentrancyGuard_init_unchained();
}

/// @dev Request tokens from the faucet
/// @notice Users can request tokens once every 24 hours
function requestTokens() external whenNotPaused nonReentrant {
require(token != address(0), "CombinedFaucet: not for native tokens");
_withdraw(msg.sender);
}

/// @dev Give native tokens to a user (who doesn't have any to pay for gas)
/// @param user The user to give tokens to
function withdraw(address user) external whenNotPaused onlyOwner {
require(token == address(0), "CombinedFaucet: only for native tokens");
_withdraw(user);
}

function _withdraw(address dst) internal {
if (lastRequestTime[dst] != 0) {
require(
block.timestamp >= lastRequestTime[dst] + ONE_DAY,
"CombinedFaucet: Rate limit exceeded. Please wait 24 hours."
);
}
lastRequestTime[dst] = block.timestamp;
if (token != address(0)) {
bool success = IERC20(token).transfer(dst, tokenAmount);
require(success, "CombinedFaucet: token transfer failed");
} else {
(bool success,) = payable(dst).call{value: tokenAmount}("");
require(success, "CombinedFaucet: wei transfer failed");
}
emit TokensRequested(dst, tokenAmount);
}

/// @dev Update the token address (Only owner can update)
/// @param token_ The new token address
function setTokenAddress(address token_) external onlyOwner {
// The token address can be 0 to support native token
// slither-disable-next-line missing-zero-check
token = token_;
emit TokenAddressUpdated(token_);
}

/// @dev Update the token amount to distribute (Only owner can update)
/// @param tokenAmount_ The new token amount
function setTokenAmount(uint256 tokenAmount_) external onlyOwner {
tokenAmount = tokenAmount_;
emit TokenAmountUpdated(tokenAmount_);
}

/// @dev Pause the contract (Only owner can pause)
function pause() external onlyOwner {
_pause();
}

/// @dev Unpause the contract (Only owner can unpause)
function unpause() external onlyOwner {
_unpause();
}

/// @dev Recover any tokens sent to the contract by mistake (Only owner)
/// @param token_ The token address to recover
/// @param amount_ The amount to recover
function recoverTokens(address token_, uint256 amount_) external nonReentrant onlyOwner {
if (token_ != address(0)) {
bool success = IERC20(token_).transfer(owner(), amount_);
require(success, "CombinedFaucet: token transfer failed");
} else {
(bool success,) = payable(owner()).call{value: amount_}("");
require(success, "CombinedFaucet: wei transfer failed");
}
}

/// @dev Always revert when ERC721 tokens are sent to this contract.
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
revert("Faucet: ERC721 tokens not accepted");
}

/// @dev Always revert when ERC1155 tokens are sent to this contract.
function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) {
revert("Faucet: ERC1155 tokens not accepted");
}

/// @dev Always revert when ERC1155 batch tokens are sent to this contract.
function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
external
pure
returns (bytes4)
{
revert("Faucet: ERC1155 batch tokens not accepted");
}

/// @dev ERC165 interface support check.
/// Automatically derives the interface selectors for ERC165, ERC721Receiver, and ERC1155Receiver.
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return (
interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC721Receiver).interfaceId
|| interfaceId == type(IERC1155Receiver).interfaceId
);
}

// Allow the contract to receive native token
receive() external payable {}

}
Loading
Loading