Skip to content

Commit

Permalink
Add minting fee and treasury management
Browse files Browse the repository at this point in the history
To be added in follow-up commits:
- oracle fee compensation
- proper tests
- updated docs
- updated deploy addresses
  • Loading branch information
TomiOhl committed Mar 20, 2023
1 parent f519501 commit 476fa31
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 12 deletions.
46 changes: 37 additions & 9 deletions contracts/GuildCredential.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import { GuildOracle } from "./GuildOracle.sol";
import { IGuildCredential } from "./interfaces/IGuildCredential.sol";
import { SoulboundERC721 } from "./token/SoulboundERC721.sol";
import { GuildOracle } from "./utils/GuildOracle.sol";
import { TreasuryManager } from "./utils/TreasuryManager.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { StringsUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { StringsUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol";

/// @title An NFT representing actions taken by Guild.xyz users.
contract GuildCredential is
Expand All @@ -16,7 +18,8 @@ contract GuildCredential is
OwnableUpgradeable,
UUPSUpgradeable,
GuildOracle,
SoulboundERC721
SoulboundERC721,
TreasuryManager
{
using StringsUpgradeable for uint256;

Expand All @@ -25,7 +28,7 @@ contract GuildCredential is
/// @notice The ipfs hash, under which the off-chain metadata is uploaded.
string internal cid;

mapping(address => mapping(GuildAction => mapping(uint256 => bool))) public hasClaimed;
mapping(address => mapping(GuildAction => mapping(uint256 => Claim))) internal claims;

/// @notice Empty space reserved for future updates.
uint256[47] private __gap;
Expand All @@ -42,25 +45,29 @@ contract GuildCredential is
/// @param cid_ The ipfs hash, under which the off-chain metadata is uploaded.
/// @param linkToken The address of the Chainlink token.
/// @param oracleAddress The address of the oracle processing the requests.
/// @param treasury The address where the collected fees will be sent.
function initialize(
string memory name,
string memory symbol,
string memory cid_,
address linkToken,
address oracleAddress
address oracleAddress,
address payable treasury
) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();
__GuildOracle_init(linkToken, oracleAddress);
__SoulboundERC721_init(name, symbol);
__TreasuryManager_init(treasury);
cid = cid_;
}

// solhint-disable-next-line no-empty-blocks
function _authorizeUpgrade(address) internal override onlyOwner {}

function claim(GuildAction guildAction, uint256 guildId) external {
if (hasClaimed[msg.sender][guildAction][guildId]) revert AlreadyClaimed();
function claim(address payToken, GuildAction guildAction, uint256 guildId) external payable {
Claim storage currentClaim = claims[msg.sender][guildAction][guildId];
if (currentClaim.claimed) revert AlreadyClaimed();

uint256 tokenId = totalSupply;

Expand All @@ -86,17 +93,34 @@ contract GuildCredential is
abi.encode(tokenId, msg.sender, GuildAction.IS_ADMIN, guildId)
);

// Fee collection
// When there is no msg.value, try transfering ERC20
if (msg.value == 0 && !IERC20Upgradeable(payToken).transferFrom(msg.sender, address(this), fee[payToken]))
revert TransferFailed(msg.sender, address(this));
// When there is msg.value, ensure it's the correct amount
else if (msg.value != fee[address(0)]) revert IncorrectFee(msg.value, fee[address(0)]);

currentClaim.payToken = payToken;
currentClaim.claimed = true;

emit ClaimRequested(msg.sender, guildAction, guildId);
}

/// @dev The actual claim function called by the oracle if the requirements are fulfilled.
function fulfillClaim(bytes32 requestId, uint256 access) public checkResponse(requestId, access) {
function fulfillClaim(bytes32 requestId, uint256 access) public recordChainlinkFulfillment(requestId) {
(uint256 tokenId, address receiver, GuildAction guildAction, uint256 id) = abi.decode(
requests[requestId].args,
(uint256, address, GuildAction, uint256)
);

hasClaimed[receiver][guildAction][id] = true;
if (access != uint256(Access.ACCESS)) {
claims[msg.sender][guildAction][id].claimed = false;

// TODO: refund fees, minus oracle fee

if (access == uint256(Access.NO_ACCESS)) revert NoAccess(receiver);
if (access >= uint256(Access.CHECK_FAILED)) revert AccessCheckFailed(receiver);
}
_safeMint(receiver, tokenId);

emit Claimed(receiver, guildAction, id);
Expand All @@ -107,6 +131,10 @@ contract GuildCredential is
return string.concat("ipfs://", cid, "/", tokenId.toString(), ".json");
}

function hasClaimed(address account, GuildAction guildAction, uint256 id) external view returns (bool claimed) {
claimed = claims[account][guildAction][id].claimed;
}

/// A version of {_safeMint} aware of total supply.
function _safeMint(address to, uint256 tokenId) internal override {
unchecked {
Expand Down
21 changes: 19 additions & 2 deletions contracts/interfaces/IGuildCredential.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title todo
/// @title An NFT representing actions taken by Guild.xyz users.
interface IGuildCredential {
/// @notice Actions that can be checked via the oracle.
enum GuildAction {
Expand All @@ -10,6 +10,12 @@ interface IGuildCredential {
IS_ADMIN
}

/// @notice The token used for paying for a credential and it's claim status.
struct Claim {
address payToken;
bool claimed;
}

/// @notice Returns true if the address has already claimed their token.
/// @param account The user's address.
/// @param guildAction The action which has been checked via the oracle.
Expand All @@ -22,9 +28,10 @@ interface IGuildCredential {
function totalSupply() external view returns (uint256 count);

/// @notice Claims tokens to the given address.
/// @param payToken The address of the token that's used for paying the minting fees. 0 for ether.
/// @param guildAction The action to check via the oracle.
/// @param guildId The id to claim the token for.
function claim(GuildAction guildAction, uint256 guildId) external;
function claim(address payToken, GuildAction guildAction, uint256 guildId) external payable;

/// @notice Event emitted whenever a claim succeeds (is fulfilled).
/// @param receiver The address that received the tokens.
Expand All @@ -41,7 +48,17 @@ interface IGuildCredential {
/// @notice Error thrown when the token is already claimed.
error AlreadyClaimed();

/// @notice Error thrown when an incorrect amount of fee is attempted to be paid.
/// @param paid The amount of funds received.
/// @param requiredAmount The amount of fees required for minting.
error IncorrectFee(uint256 paid, uint256 requiredAmount);

/// @notice Error thrown when trying to query info about a token that's not (yet) minted.
/// @param tokenId The queried id.
error NonExistentToken(uint256 tokenId);

/// @notice Error thrown when an ERC20 transfer failed.
/// @param from The sender of the token.
/// @param to The recipient of the token.
error TransferFailed(address from, address to);
}
33 changes: 33 additions & 0 deletions contracts/interfaces/ITreasuryManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title A contract that manages fee-related functionality.
interface ITreasuryManager {
/// @notice Sets the minting fee for a given token used for paying.
/// @dev Callable only by the owner.
/// @param token The token whose fee is set.
/// @param newFee The new fee in base units.
function setFee(address token, uint256 newFee) external;

/// @notice Sets the address that receives the fees.
/// @dev Callable only by the owner.
/// @param newTreasury The new address of the treasury.
function setTreasury(address payable newTreasury) external;

/// @notice The minting fee of a token.
/// @param token The token used for paying.
/// @return fee The amount of the fee in base units.
function fee(address token) external view returns (uint256 fee);

/// @notice Returns the address that receives the fees.
function treasury() external view returns (address payable);

/// @notice Event emitted when a token's fee is changed.
/// @param token The address of the token whose fee was changed. 0 for ether.
/// @param newFee The new amount of fee in base units.
event FeeChanged(address token, uint256 newFee);

/// @notice Event emitted when the treasury address is changed.
/// @param newTreasury The new address of the treasury.
event TreasuryChanged(address newTreasury);
}
18 changes: 18 additions & 0 deletions contracts/lib/LibAddress.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title Library for functions related to addresses.
library LibAddress {
/// @notice Error thrown when sending ether fails.
/// @param recipient The address that could not receive the ether.
error FailedToSendEther(address recipient);

/// @notice Send ether to an address, forwarding all available gas and reverting on errors.
/// @param recipient The recipient of the ether.
/// @param amount The amount of ether to send in base units.
function sendEther(address payable recipient, uint256 amount) internal {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = recipient.call{ value: amount }("");
if (!success) revert FailedToSendEther(recipient);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import { ChainlinkClient } from "./external/chainlink/ChainlinkClient.sol";
import { ChainlinkClient } from "../external/chainlink/ChainlinkClient.sol";
import { Chainlink } from "@chainlink/contracts/src/v0.8/Chainlink.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { StringsUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol";
Expand Down
29 changes: 29 additions & 0 deletions contracts/utils/TreasuryManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import { ITreasuryManager } from "../interfaces/ITreasuryManager.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/// @title A contract that manages fee-related functionality.
contract TreasuryManager is ITreasuryManager, Initializable, OwnableUpgradeable {
address payable public treasury;

mapping(address => uint256) public fee;

/// @param treasury_ The address that will receive the fees.
// solhint-disable-next-line func-name-mixedcase
function __TreasuryManager_init(address payable treasury_) internal onlyInitializing {
treasury = treasury_;
}

function setFee(address token, uint256 newFee) external onlyOwner {
fee[token] = newFee;
emit FeeChanged(token, newFee);
}

function setTreasury(address payable newTreasury) external onlyOwner {
treasury = newTreasury;
emit TreasuryChanged(newTreasury);
}
}

0 comments on commit 476fa31

Please sign in to comment.