Skip to content

Commit

Permalink
feat: add faucet contract (#110)
Browse files Browse the repository at this point in the history
* feat: introduce LZ script to replay msg

https://github.com/DanL0/integrations-foundry-tooling but with better
logging in our context

* feat: add faucet code + script + test

* fix test and forge fmt

* fix: pacify slither

* fix: use correct IERC165 logic

* remove duplicated code

* refactor: update test

* refactor: update error message

* test: update scenario

* refactor: use Eid from json in mock
  • Loading branch information
MaxMustermann2 authored Oct 15, 2024
1 parent 2481fa9 commit 09197a8
Show file tree
Hide file tree
Showing 4 changed files with 587 additions and 0 deletions.
58 changes: 58 additions & 0 deletions script/18_SimulateReceive.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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 {

using stdJson for string;

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");
// 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(srcEid)), ASSETS_PRECOMPILE_ADDRESS);
deployCodeTo("DelegationMock.sol", DELEGATION_PRECOMPILE_ADDRESS);
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");

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;

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

0 comments on commit 09197a8

Please sign in to comment.