238 lines
7.9 KiB
Solidity
238 lines
7.9 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.24;
|
|
|
|
interface IERC20Settlement {
|
|
function transferFrom(address from, address to, uint256 value) external returns (bool);
|
|
}
|
|
|
|
interface IEdutMembershipStatus {
|
|
function hasMembership(address wallet) external view returns (bool);
|
|
}
|
|
|
|
/// @title EDUT Offer Entitlements
|
|
/// @notice Membership-gated purchase registry for solo/workspace offer SKUs.
|
|
contract EdutOfferEntitlement {
|
|
uint8 public constant ENTITLEMENT_NONE = 0;
|
|
uint8 public constant ENTITLEMENT_ACTIVE = 1;
|
|
uint8 public constant ENTITLEMENT_SUSPENDED = 2;
|
|
uint8 public constant ENTITLEMENT_REVOKED = 3;
|
|
|
|
error NotOwner();
|
|
error InvalidAddress();
|
|
error InvalidState();
|
|
error UnknownOffer();
|
|
error OfferInactive();
|
|
error OfferIDRequired();
|
|
error MembershipRequired();
|
|
error UnknownEntitlement();
|
|
error IncorrectEthValue();
|
|
error UnexpectedEthValue();
|
|
error PaymentTransferFailed();
|
|
|
|
event OfferUpserted(
|
|
string offerId,
|
|
bytes32 indexed offerKey,
|
|
uint256 priceAtomic,
|
|
bool active,
|
|
bool membershipRequired
|
|
);
|
|
event EntitlementMinted(
|
|
uint256 indexed entitlementId,
|
|
string offerId,
|
|
bytes32 indexed offerKey,
|
|
address indexed ownerWallet,
|
|
address payerWallet,
|
|
bytes32 orgRootKey,
|
|
bytes32 workspaceKey,
|
|
uint256 amountPaidAtomic,
|
|
address currency
|
|
);
|
|
event EntitlementStateChanged(uint256 indexed entitlementId, uint8 state);
|
|
event TreasuryUpdated(address indexed treasury);
|
|
event PaymentTokenUpdated(address indexed paymentToken);
|
|
event MembershipContractUpdated(address indexed membershipContract);
|
|
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
|
|
|
|
struct OfferConfig {
|
|
uint256 priceAtomic;
|
|
bool active;
|
|
bool membershipRequired;
|
|
bool exists;
|
|
}
|
|
|
|
struct Entitlement {
|
|
address ownerWallet;
|
|
address payerWallet;
|
|
bytes32 offerKey;
|
|
bytes32 orgRootKey;
|
|
bytes32 workspaceKey;
|
|
uint8 state;
|
|
uint64 issuedAt;
|
|
}
|
|
|
|
address public owner;
|
|
address public treasury;
|
|
address public paymentToken;
|
|
address public membershipContract;
|
|
uint256 public nextEntitlementId = 1;
|
|
|
|
mapping(bytes32 => OfferConfig) private _offers;
|
|
mapping(bytes32 => string) private _offerIdByKey;
|
|
mapping(uint256 => Entitlement) private _entitlements;
|
|
|
|
modifier onlyOwner() {
|
|
if (msg.sender != owner) revert NotOwner();
|
|
_;
|
|
}
|
|
|
|
constructor(address treasury_, address paymentToken_, address membershipContract_) {
|
|
if (treasury_ == address(0)) revert InvalidAddress();
|
|
owner = msg.sender;
|
|
treasury = treasury_;
|
|
paymentToken = paymentToken_;
|
|
membershipContract = membershipContract_;
|
|
emit OwnershipTransferred(address(0), msg.sender);
|
|
emit TreasuryUpdated(treasury_);
|
|
emit PaymentTokenUpdated(paymentToken_);
|
|
emit MembershipContractUpdated(membershipContract_);
|
|
}
|
|
|
|
function offerKeyFor(string memory offerId) public pure returns (bytes32) {
|
|
return keccak256(bytes(offerId));
|
|
}
|
|
|
|
function offerConfig(string calldata offerId)
|
|
external
|
|
view
|
|
returns (uint256 priceAtomic, bool active, bool membershipRequired, bool exists)
|
|
{
|
|
OfferConfig memory cfg = _offers[offerKeyFor(offerId)];
|
|
return (cfg.priceAtomic, cfg.active, cfg.membershipRequired, cfg.exists);
|
|
}
|
|
|
|
function offerIDByKey(bytes32 offerKey) external view returns (string memory offerId) {
|
|
return _offerIdByKey[offerKey];
|
|
}
|
|
|
|
function entitlementById(uint256 entitlementId) external view returns (Entitlement memory) {
|
|
Entitlement memory ent = _entitlements[entitlementId];
|
|
if (ent.ownerWallet == address(0)) revert UnknownEntitlement();
|
|
return ent;
|
|
}
|
|
|
|
function upsertOffer(
|
|
string calldata offerId,
|
|
uint256 priceAtomic,
|
|
bool active,
|
|
bool membershipRequired
|
|
) external onlyOwner {
|
|
if (bytes(offerId).length == 0) revert OfferIDRequired();
|
|
bytes32 key = offerKeyFor(offerId);
|
|
_offers[key] = OfferConfig({
|
|
priceAtomic: priceAtomic,
|
|
active: active,
|
|
membershipRequired: membershipRequired,
|
|
exists: true
|
|
});
|
|
_offerIdByKey[key] = offerId;
|
|
emit OfferUpserted(offerId, key, priceAtomic, active, membershipRequired);
|
|
}
|
|
|
|
function purchaseEntitlement(
|
|
string calldata offerId,
|
|
address ownerWallet,
|
|
bytes32 orgRootKey,
|
|
bytes32 workspaceKey
|
|
) external payable returns (uint256 entitlementId) {
|
|
if (bytes(offerId).length == 0) revert OfferIDRequired();
|
|
if (ownerWallet == address(0)) revert InvalidAddress();
|
|
|
|
bytes32 key = offerKeyFor(offerId);
|
|
OfferConfig memory cfg = _offers[key];
|
|
if (!cfg.exists) revert UnknownOffer();
|
|
if (!cfg.active) revert OfferInactive();
|
|
if (cfg.membershipRequired) {
|
|
address membership = membershipContract;
|
|
if (membership == address(0) || !IEdutMembershipStatus(membership).hasMembership(ownerWallet)) {
|
|
revert MembershipRequired();
|
|
}
|
|
}
|
|
|
|
_collectPayment(msg.sender, cfg.priceAtomic);
|
|
|
|
entitlementId = nextEntitlementId;
|
|
nextEntitlementId += 1;
|
|
_entitlements[entitlementId] = Entitlement({
|
|
ownerWallet: ownerWallet,
|
|
payerWallet: msg.sender,
|
|
offerKey: key,
|
|
orgRootKey: orgRootKey,
|
|
workspaceKey: workspaceKey,
|
|
state: ENTITLEMENT_ACTIVE,
|
|
issuedAt: uint64(block.timestamp)
|
|
});
|
|
|
|
emit EntitlementMinted(
|
|
entitlementId,
|
|
offerId,
|
|
key,
|
|
ownerWallet,
|
|
msg.sender,
|
|
orgRootKey,
|
|
workspaceKey,
|
|
cfg.priceAtomic,
|
|
paymentToken
|
|
);
|
|
emit EntitlementStateChanged(entitlementId, ENTITLEMENT_ACTIVE);
|
|
}
|
|
|
|
function setEntitlementState(uint256 entitlementId, uint8 state) external onlyOwner {
|
|
if (state > ENTITLEMENT_REVOKED) revert InvalidState();
|
|
Entitlement storage ent = _entitlements[entitlementId];
|
|
if (ent.ownerWallet == address(0)) revert UnknownEntitlement();
|
|
ent.state = state;
|
|
emit EntitlementStateChanged(entitlementId, state);
|
|
}
|
|
|
|
function setTreasury(address nextTreasury) external onlyOwner {
|
|
if (nextTreasury == address(0)) revert InvalidAddress();
|
|
treasury = nextTreasury;
|
|
emit TreasuryUpdated(nextTreasury);
|
|
}
|
|
|
|
function setPaymentToken(address nextPaymentToken) external onlyOwner {
|
|
paymentToken = nextPaymentToken;
|
|
emit PaymentTokenUpdated(nextPaymentToken);
|
|
}
|
|
|
|
function setMembershipContract(address nextMembershipContract) external onlyOwner {
|
|
membershipContract = nextMembershipContract;
|
|
emit MembershipContractUpdated(nextMembershipContract);
|
|
}
|
|
|
|
function transferOwnership(address nextOwner) external onlyOwner {
|
|
if (nextOwner == address(0)) revert InvalidAddress();
|
|
address previous = owner;
|
|
owner = nextOwner;
|
|
emit OwnershipTransferred(previous, nextOwner);
|
|
}
|
|
|
|
function _collectPayment(address payer, uint256 amountAtomic) private {
|
|
if (amountAtomic == 0) {
|
|
if (msg.value != 0) revert UnexpectedEthValue();
|
|
return;
|
|
}
|
|
|
|
if (paymentToken == address(0)) {
|
|
if (msg.value != amountAtomic) revert IncorrectEthValue();
|
|
(bool sent, ) = treasury.call{value: amountAtomic}("");
|
|
if (!sent) revert PaymentTransferFailed();
|
|
return;
|
|
}
|
|
|
|
if (msg.value != 0) revert UnexpectedEthValue();
|
|
bool transferred = IERC20Settlement(paymentToken).transferFrom(payer, treasury, amountAtomic);
|
|
if (!transferred) revert PaymentTransferFailed();
|
|
}
|
|
}
|