Add membership contract, deployment scripts, and Base network config
This commit is contained in:
parent
082b29b205
commit
dbac2f0883
10
.env.example
Normal file
10
.env.example
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
DEPLOYER_PRIVATE_KEY=0x...
|
||||||
|
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
|
||||||
|
BASE_MAINNET_RPC_URL=https://mainnet.base.org
|
||||||
|
|
||||||
|
TREASURY_WALLET=0x0000000000000000000000000000000000000000
|
||||||
|
|
||||||
|
# Use zero address for native ETH pricing.
|
||||||
|
MINT_CURRENCY_ADDRESS=0x0000000000000000000000000000000000000000
|
||||||
|
MINT_AMOUNT_ATOMIC=5000000000000000
|
||||||
|
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
cache/
|
||||||
|
out/
|
||||||
|
artifacts/
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
10
Makefile
Normal file
10
Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.PHONY: check build test
|
||||||
|
|
||||||
|
check:
|
||||||
|
@npm run check
|
||||||
|
|
||||||
|
build:
|
||||||
|
@npm run build
|
||||||
|
|
||||||
|
test:
|
||||||
|
@npm run test
|
||||||
42
README.md
42
README.md
@ -4,11 +4,51 @@ On-chain contracts and deployment artifacts for membership and entitlements.
|
|||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
1. Membership contract (soulbound utility access).
|
1. Human membership contract (soulbound governance identity).
|
||||||
2. Offer registry contract interfaces.
|
2. Offer registry contract interfaces.
|
||||||
3. Entitlement contract interfaces.
|
3. Entitlement contract interfaces.
|
||||||
4. ABI and deployment artifact publication.
|
4. ABI and deployment artifact publication.
|
||||||
|
|
||||||
|
## Current Contract
|
||||||
|
|
||||||
|
`contracts/EdutHumanMembership.sol`
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
1. One-time soulbound human token mint.
|
||||||
|
2. Sponsor mint support (`mintMembership(recipient)` can be paid by inviter/company wallet).
|
||||||
|
3. Owner-configurable flat mint price (`updateMintPrice`), launch default can stay flat `$5` equivalent.
|
||||||
|
4. Membership status lifecycle (`ACTIVE/SUSPENDED/REVOKED`) for runtime gates.
|
||||||
|
5. Treasury address control for settlement routing.
|
||||||
|
|
||||||
|
## Local Commands
|
||||||
|
|
||||||
|
1. `npm install`
|
||||||
|
2. `npm run build`
|
||||||
|
3. `npm run test`
|
||||||
|
4. `npm run deploy:sepolia`
|
||||||
|
5. `npm run deploy:mainnet`
|
||||||
|
|
||||||
|
`make check` wraps build + tests.
|
||||||
|
|
||||||
|
## Deployment Environment
|
||||||
|
|
||||||
|
Copy `.env.example` values into your shell/session before deploy:
|
||||||
|
|
||||||
|
1. `DEPLOYER_PRIVATE_KEY`
|
||||||
|
2. `BASE_SEPOLIA_RPC_URL` / `BASE_MAINNET_RPC_URL`
|
||||||
|
3. `TREASURY_WALLET`
|
||||||
|
4. `MINT_CURRENCY_ADDRESS` (`0x000...000` for ETH)
|
||||||
|
5. `MINT_AMOUNT_ATOMIC`
|
||||||
|
|
||||||
|
Example (Sepolia):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/vsg/Documents/VSG\ Codex/contracts
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
npm run deploy:sepolia
|
||||||
|
```
|
||||||
|
|
||||||
## Boundary
|
## Boundary
|
||||||
|
|
||||||
Contracts are settlement primitives. Runtime execution remains off-chain and fail-closed by entitlement state.
|
Contracts are settlement primitives. Runtime execution remains off-chain and fail-closed by entitlement state.
|
||||||
|
|||||||
185
contracts/EdutHumanMembership.sol
Normal file
185
contracts/EdutHumanMembership.sol
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.24;
|
||||||
|
|
||||||
|
interface IERC20 {
|
||||||
|
function transferFrom(address from, address to, uint256 value) external returns (bool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @title EDUT Human Membership (Soulbound)
|
||||||
|
/// @notice One-time human governance token with configurable mint price and sponsor mint support.
|
||||||
|
contract EdutHumanMembership {
|
||||||
|
string public constant name = "EDUT Human Membership";
|
||||||
|
string public constant symbol = "EDUTHUM";
|
||||||
|
|
||||||
|
uint8 public constant MEMBERSHIP_NONE = 0;
|
||||||
|
uint8 public constant MEMBERSHIP_ACTIVE = 1;
|
||||||
|
uint8 public constant MEMBERSHIP_SUSPENDED = 2;
|
||||||
|
uint8 public constant MEMBERSHIP_REVOKED = 3;
|
||||||
|
|
||||||
|
error NotOwner();
|
||||||
|
error InvalidAddress();
|
||||||
|
error AlreadyMinted();
|
||||||
|
error UnknownToken();
|
||||||
|
error InvalidStatus();
|
||||||
|
error SoulboundNonTransferable();
|
||||||
|
error IncorrectEthValue();
|
||||||
|
error UnexpectedEthValue();
|
||||||
|
error PaymentTransferFailed();
|
||||||
|
|
||||||
|
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
|
||||||
|
event MembershipMinted(address indexed wallet, uint256 indexed tokenId, uint256 amountPaid, address currency);
|
||||||
|
event MintPriceUpdated(address indexed currency, uint256 amountAtomic);
|
||||||
|
event MembershipStatusUpdated(address indexed wallet, uint8 status);
|
||||||
|
event TreasuryUpdated(address indexed treasury);
|
||||||
|
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
|
||||||
|
|
||||||
|
address public owner;
|
||||||
|
address public treasury;
|
||||||
|
|
||||||
|
address private _mintCurrency;
|
||||||
|
uint256 private _mintAmountAtomic;
|
||||||
|
uint256 private _nextTokenId = 1;
|
||||||
|
|
||||||
|
mapping(uint256 => address) private _tokenOwner;
|
||||||
|
mapping(address => uint256) private _walletToken;
|
||||||
|
mapping(address => uint256) private _walletBalance;
|
||||||
|
mapping(address => uint8) private _membershipStatus;
|
||||||
|
|
||||||
|
modifier onlyOwner() {
|
||||||
|
if (msg.sender != owner) revert NotOwner();
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(address treasury_, address currency_, uint256 amountAtomic_) {
|
||||||
|
if (treasury_ == address(0)) revert InvalidAddress();
|
||||||
|
owner = msg.sender;
|
||||||
|
treasury = treasury_;
|
||||||
|
_mintCurrency = currency_;
|
||||||
|
_mintAmountAtomic = amountAtomic_;
|
||||||
|
emit OwnershipTransferred(address(0), msg.sender);
|
||||||
|
emit TreasuryUpdated(treasury_);
|
||||||
|
emit MintPriceUpdated(currency_, amountAtomic_);
|
||||||
|
}
|
||||||
|
|
||||||
|
function balanceOf(address wallet) external view returns (uint256) {
|
||||||
|
if (wallet == address(0)) revert InvalidAddress();
|
||||||
|
return _walletBalance[wallet];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ownerOf(uint256 tokenId) public view returns (address) {
|
||||||
|
address tokenOwner = _tokenOwner[tokenId];
|
||||||
|
if (tokenOwner == address(0)) revert UnknownToken();
|
||||||
|
return tokenOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenOf(address wallet) external view returns (uint256 tokenId, uint8 status) {
|
||||||
|
return (_walletToken[wallet], _membershipStatus[wallet]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMembership(address wallet) external view returns (bool) {
|
||||||
|
return _membershipStatus[wallet] == MEMBERSHIP_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function membershipStatus(address wallet) external view returns (uint8 status) {
|
||||||
|
return _membershipStatus[wallet];
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentMintPrice() external view returns (address currency, uint256 amountAtomic) {
|
||||||
|
return (_mintCurrency, _mintAmountAtomic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Mint one human membership token to recipient. Caller may sponsor payment.
|
||||||
|
function mintMembership(address recipient) external payable returns (uint256 tokenId) {
|
||||||
|
if (recipient == address(0)) revert InvalidAddress();
|
||||||
|
if (_walletToken[recipient] != 0) revert AlreadyMinted();
|
||||||
|
|
||||||
|
_collectMintPayment(msg.sender);
|
||||||
|
|
||||||
|
tokenId = _nextTokenId;
|
||||||
|
_nextTokenId += 1;
|
||||||
|
|
||||||
|
_walletToken[recipient] = tokenId;
|
||||||
|
_tokenOwner[tokenId] = recipient;
|
||||||
|
_walletBalance[recipient] = 1;
|
||||||
|
_membershipStatus[recipient] = MEMBERSHIP_ACTIVE;
|
||||||
|
|
||||||
|
emit Transfer(address(0), recipient, tokenId);
|
||||||
|
emit MembershipMinted(recipient, tokenId, _mintAmountAtomic, _mintCurrency);
|
||||||
|
emit MembershipStatusUpdated(recipient, MEMBERSHIP_ACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMintPrice(address currency, uint256 amountAtomic) external onlyOwner {
|
||||||
|
_mintCurrency = currency;
|
||||||
|
_mintAmountAtomic = amountAtomic;
|
||||||
|
emit MintPriceUpdated(currency, amountAtomic);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTreasury(address nextTreasury) external onlyOwner {
|
||||||
|
if (nextTreasury == address(0)) revert InvalidAddress();
|
||||||
|
treasury = nextTreasury;
|
||||||
|
emit TreasuryUpdated(nextTreasury);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMembershipStatus(address wallet, uint8 status) external onlyOwner {
|
||||||
|
uint256 tokenId = _walletToken[wallet];
|
||||||
|
if (tokenId == 0) revert UnknownToken();
|
||||||
|
if (status > MEMBERSHIP_REVOKED) revert InvalidStatus();
|
||||||
|
_membershipStatus[wallet] = status;
|
||||||
|
emit MembershipStatusUpdated(wallet, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transferOwnership(address nextOwner) external onlyOwner {
|
||||||
|
if (nextOwner == address(0)) revert InvalidAddress();
|
||||||
|
address previous = owner;
|
||||||
|
owner = nextOwner;
|
||||||
|
emit OwnershipTransferred(previous, nextOwner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soulbound: all transfer/approval paths are disabled.
|
||||||
|
function approve(address, uint256) external pure {
|
||||||
|
revert SoulboundNonTransferable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setApprovalForAll(address, bool) external pure {
|
||||||
|
revert SoulboundNonTransferable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApproved(uint256) external pure returns (address) {
|
||||||
|
return address(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isApprovedForAll(address, address) external pure returns (bool) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transferFrom(address, address, uint256) external pure {
|
||||||
|
revert SoulboundNonTransferable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeTransferFrom(address, address, uint256) external pure {
|
||||||
|
revert SoulboundNonTransferable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeTransferFrom(address, address, uint256, bytes calldata) external pure {
|
||||||
|
revert SoulboundNonTransferable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _collectMintPayment(address payer) private {
|
||||||
|
uint256 amount = _mintAmountAtomic;
|
||||||
|
if (amount == 0) {
|
||||||
|
if (msg.value != 0) revert UnexpectedEthValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_mintCurrency == address(0)) {
|
||||||
|
if (msg.value != amount) revert IncorrectEthValue();
|
||||||
|
(bool ok, ) = treasury.call{value: amount}("");
|
||||||
|
if (!ok) revert PaymentTransferFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.value != 0) revert UnexpectedEthValue();
|
||||||
|
bool transferred = IERC20(_mintCurrency).transferFrom(payer, treasury, amount);
|
||||||
|
if (!transferred) revert PaymentTransferFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,3 +7,13 @@ Place versioned deployment manifests here:
|
|||||||
3. deployment tx hashes
|
3. deployment tx hashes
|
||||||
4. verifier links
|
4. verifier links
|
||||||
5. policy hash snapshot
|
5. policy hash snapshot
|
||||||
|
|
||||||
|
Template:
|
||||||
|
|
||||||
|
- `membership-deploy.template.json`
|
||||||
|
|
||||||
|
Recommended process:
|
||||||
|
|
||||||
|
1. Run `npm run deploy:sepolia` or `npm run deploy:mainnet`.
|
||||||
|
2. Copy template to a dated file (for example `membership-base-sepolia-2026-02-18.json`).
|
||||||
|
3. Fill all deployment fields from script output and explorer links.
|
||||||
|
|||||||
13
deploy/membership-deploy.template.json
Normal file
13
deploy/membership-deploy.template.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"network": "base-sepolia",
|
||||||
|
"chain_id": 84532,
|
||||||
|
"deployer": "0x0000000000000000000000000000000000000000",
|
||||||
|
"treasury_wallet": "0x0000000000000000000000000000000000000000",
|
||||||
|
"mint_currency_address": "0x0000000000000000000000000000000000000000",
|
||||||
|
"mint_amount_atomic": "5000000000000000",
|
||||||
|
"membership_contract": "0x0000000000000000000000000000000000000000",
|
||||||
|
"deployment_tx_hash": "0x",
|
||||||
|
"deployed_at": "2026-02-18T00:00:00Z",
|
||||||
|
"notes": "Fill from deployment output and keep under change control."
|
||||||
|
}
|
||||||
|
|
||||||
29
hardhat.config.cjs
Normal file
29
hardhat.config.cjs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
require("@nomiclabs/hardhat-ethers");
|
||||||
|
|
||||||
|
const accounts = process.env.DEPLOYER_PRIVATE_KEY
|
||||||
|
? [process.env.DEPLOYER_PRIVATE_KEY]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
solidity: {
|
||||||
|
version: "0.8.24",
|
||||||
|
settings: {
|
||||||
|
optimizer: {
|
||||||
|
enabled: true,
|
||||||
|
runs: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
baseSepolia: {
|
||||||
|
url: process.env.BASE_SEPOLIA_RPC_URL || "",
|
||||||
|
chainId: 84532,
|
||||||
|
accounts
|
||||||
|
},
|
||||||
|
base: {
|
||||||
|
url: process.env.BASE_MAINNET_RPC_URL || "",
|
||||||
|
chainId: 8453,
|
||||||
|
accounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
3408
package-lock.json
generated
Normal file
3408
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@edut/contracts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "EDUT membership and entitlement contracts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "hardhat compile",
|
||||||
|
"test": "hardhat test",
|
||||||
|
"check": "npm run build && npm run test",
|
||||||
|
"deploy:sepolia": "hardhat run scripts/deploy-membership.cjs --network baseSepolia",
|
||||||
|
"deploy:mainnet": "hardhat run scripts/deploy-membership.cjs --network base"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nomiclabs/hardhat-ethers": "^2.2.3",
|
||||||
|
"chai": "^4.5.0",
|
||||||
|
"ethers": "^5.7.2",
|
||||||
|
"hardhat": "^2.22.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
scripts/deploy-membership.cjs
Normal file
34
scripts/deploy-membership.cjs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const hre = require("hardhat");
|
||||||
|
|
||||||
|
function requiredEnv(name) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value || !value.trim()) {
|
||||||
|
throw new Error(`Missing required env: ${name}`);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const treasury = requiredEnv("TREASURY_WALLET");
|
||||||
|
const mintCurrency = (process.env.MINT_CURRENCY_ADDRESS || hre.ethers.constants.AddressZero).trim();
|
||||||
|
const mintAmountAtomic = requiredEnv("MINT_AMOUNT_ATOMIC");
|
||||||
|
|
||||||
|
const [deployer] = await hre.ethers.getSigners();
|
||||||
|
console.log("deployer:", deployer.address);
|
||||||
|
console.log("network:", hre.network.name);
|
||||||
|
console.log("treasury:", treasury);
|
||||||
|
console.log("mint_currency:", mintCurrency);
|
||||||
|
console.log("mint_amount_atomic:", mintAmountAtomic);
|
||||||
|
|
||||||
|
const factory = await hre.ethers.getContractFactory("EdutHumanMembership");
|
||||||
|
const contract = await factory.deploy(treasury, mintCurrency, mintAmountAtomic);
|
||||||
|
await contract.deployed();
|
||||||
|
|
||||||
|
console.log("membership_contract:", contract.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|
||||||
81
test/EdutHumanMembership.test.js
Normal file
81
test/EdutHumanMembership.test.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
const { expect } = require("chai");
|
||||||
|
const { ethers } = require("hardhat");
|
||||||
|
|
||||||
|
describe("EdutHumanMembership", function () {
|
||||||
|
async function expectRevertWithCustomError(promise, errorName) {
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
expect.fail(`expected revert ${errorName}`);
|
||||||
|
} catch (err) {
|
||||||
|
expect(String(err)).to.contain(errorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deployFixture() {
|
||||||
|
const [deployer, treasury, sponsor, recipient, other] = await ethers.getSigners();
|
||||||
|
const factory = await ethers.getContractFactory("EdutHumanMembership", deployer);
|
||||||
|
const price = ethers.utils.parseEther("0.002");
|
||||||
|
const contract = await factory.deploy(treasury.address, ethers.constants.AddressZero, price);
|
||||||
|
await contract.deployed();
|
||||||
|
return { contract, deployer, treasury, sponsor, recipient, other, price };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("mints to recipient with sponsor payment", async function () {
|
||||||
|
const { contract, treasury, sponsor, recipient, price } = await deployFixture();
|
||||||
|
const before = await ethers.provider.getBalance(treasury.address);
|
||||||
|
|
||||||
|
const tx = await contract.connect(sponsor).mintMembership(recipient.address, { value: price });
|
||||||
|
await tx.wait();
|
||||||
|
|
||||||
|
const after = await ethers.provider.getBalance(treasury.address);
|
||||||
|
expect(after.sub(before).eq(price)).to.equal(true);
|
||||||
|
expect(await contract.ownerOf(1)).to.equal(recipient.address);
|
||||||
|
|
||||||
|
const token = await contract.tokenOf(recipient.address);
|
||||||
|
expect(token.tokenId.toNumber()).to.equal(1);
|
||||||
|
expect(token.status).to.equal(1);
|
||||||
|
expect(await contract.hasMembership(recipient.address)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects duplicate mint for same human wallet", async function () {
|
||||||
|
const { contract, sponsor, recipient, price } = await deployFixture();
|
||||||
|
await (await contract.connect(sponsor).mintMembership(recipient.address, { value: price })).wait();
|
||||||
|
|
||||||
|
await expectRevertWithCustomError(
|
||||||
|
contract.connect(sponsor).mintMembership(recipient.address, { value: price }),
|
||||||
|
"AlreadyMinted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports owner-configurable price and owner-only admin", async function () {
|
||||||
|
const { contract, deployer, other } = await deployFixture();
|
||||||
|
const nextPrice = ethers.utils.parseEther("0.003");
|
||||||
|
|
||||||
|
await expectRevertWithCustomError(
|
||||||
|
contract.connect(other).updateMintPrice(ethers.constants.AddressZero, nextPrice),
|
||||||
|
"NotOwner"
|
||||||
|
);
|
||||||
|
|
||||||
|
await (await contract.connect(deployer).updateMintPrice(ethers.constants.AddressZero, nextPrice)).wait();
|
||||||
|
const price = await contract.currentMintPrice();
|
||||||
|
expect(price.amountAtomic.eq(nextPrice)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks transfer paths (soulbound)", async function () {
|
||||||
|
const { contract, sponsor, recipient, other, price } = await deployFixture();
|
||||||
|
await (await contract.connect(sponsor).mintMembership(recipient.address, { value: price })).wait();
|
||||||
|
|
||||||
|
await expectRevertWithCustomError(
|
||||||
|
contract.connect(recipient).transferFrom(recipient.address, other.address, 1),
|
||||||
|
"SoulboundNonTransferable"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows owner to update status", async function () {
|
||||||
|
const { contract, deployer, sponsor, recipient, price } = await deployFixture();
|
||||||
|
await (await contract.connect(sponsor).mintMembership(recipient.address, { value: price })).wait();
|
||||||
|
await (await contract.connect(deployer).setMembershipStatus(recipient.address, 2)).wait();
|
||||||
|
|
||||||
|
expect(await contract.membershipStatus(recipient.address)).to.equal(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user