contracts/contracts/EdutOfferEntitlement.sol

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();
}
}