From c485d0e532b03d3f5bea2a053bb1c4a15f8025d7 Mon Sep 17 00:00:00 2001 From: bashybaranaba Date: Thu, 2 Jan 2025 00:47:59 +0300 Subject: [PATCH] feat: update abraham contract --- contracts/Abraham.sol | 209 ++++++++++++--- contracts/Manna.sol | 54 ---- experimental/AbrahamWithManna.sol | 168 +++++++++++++ experimental/AbrahamWithManna2.sol | 323 ++++++++++++++++++++++++ hardhat.config.ts | 18 +- package-lock.json | 44 ++-- package.json | 1 + scripts/deploy.ts | 32 +-- test/Abraham.ts | 392 ++++++++++++++++++++--------- test/Manna.ts | 87 ------- 10 files changed, 988 insertions(+), 340 deletions(-) delete mode 100644 contracts/Manna.sol create mode 100644 experimental/AbrahamWithManna.sol create mode 100644 experimental/AbrahamWithManna2.sol delete mode 100644 test/Manna.ts diff --git a/contracts/Abraham.sol b/contracts/Abraham.sol index 4a1ebb9..7db6ccf 100644 --- a/contracts/Abraham.sol +++ b/contracts/Abraham.sol @@ -1,75 +1,171 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -contract Abraham is Ownable { - IERC20 public immutable manna; +contract Abraham is ERC20, Ownable { + + // ========================================================================= + // MANNA TOKEN LOGIC + // ========================================================================= + + uint256 public constant INITIAL_SUPPLY = 1_000_000 * (10 ** 18); + uint256 public constant MANNA_PRICE = 0.0001 ether; + + event BoughtManna(address indexed buyer, uint256 amount); + event SoldManna(address indexed seller, uint256 mannaAmount, uint256 ethAmount); + + constructor(address initialOwner) + ERC20("Manna", "MANNA") + Ownable(initialOwner) + { + require(initialOwner != address(0), "Invalid owner address"); + uint256 initialOwnerSupply = INITIAL_SUPPLY / 2; + _mint(initialOwner, initialOwnerSupply); + } + + function buyManna() public payable { + require(msg.value > 0, "No Ether sent"); + require(msg.value >= MANNA_PRICE, "Insufficient Ether for min purchase"); + + uint256 mannaAmount = (msg.value * (10 ** 18)) / MANNA_PRICE; + + // If you want to cap at INITIAL_SUPPLY, you can check totalSupply + require(totalSupply() + mannaAmount <= INITIAL_SUPPLY, "Supply cap reached"); + + _mint(msg.sender, mannaAmount); + emit BoughtManna(msg.sender, mannaAmount); + } + + function sellManna(uint256 mannaAmount) external { + require(balanceOf(msg.sender) >= mannaAmount, "Not enough Manna to sell"); + + // Calculate how much Ether the user should receive + uint256 ethAmount = (mannaAmount * MANNA_PRICE) / (10 ** 18); + require(address(this).balance >= ethAmount, "Contract lacks Ether for buyback"); + _burn(msg.sender, mannaAmount); + + // Send Ether to the seller + (bool sent, ) = msg.sender.call{value: ethAmount}(""); + require(sent, "Failed to send Ether"); + + emit SoldManna(msg.sender, mannaAmount, ethAmount); + } + + function getContractBalances() + external + view + returns (uint256 mannaBalance, uint256 ethBalance) + { + mannaBalance = balanceOf(address(this)); + ethBalance = address(this).balance; + } + + receive() external payable { + if (msg.value > 0) { + buyManna(); + } + } + + + // ========================================================================= + // ABRAHAM CREATION/PRAISE LOGIC + // ========================================================================= struct Creation { uint256 id; string metadataUri; - uint256 totalStaked; - uint256 praisePool; - uint256 conviction; + uint256 totalStaked; // total # of "praise units" staked + uint256 praisePool; // total Manna stored in the pool for this creation + uint256 conviction; // sum of (balance * timeHeld) mapping(address => uint256) praiseBalance; mapping(address => uint256) stakeTime; } + // Creation storage uint256 public creationCount; mapping(uint256 => Creation) public creations; uint256[] private _allCreationIds; - uint256 public initPraisePrice = 1e17; // 0.1 Manna - uint256 public initUnpraisePrice = 1e17; // 0.1 Manna + // Praise pricing + uint256 public initPraisePrice = 1e18; // 1 Manna + uint256 public initUnpraisePrice = 1e18; // 1 Manna + + event CreationAdded(uint256 indexed creationId, string metadataUri); + event Praised( + uint256 indexed creationId, + address indexed user, + uint256 pricePaid, + uint256 unitsPraised + ); + event Unpraised( + uint256 indexed creationId, + address indexed user, + uint256 unitsUnpraised, + uint256 mannaRefunded + ); + event ConvictionUpdated(uint256 indexed creationId, uint256 newConviction); + // Secondary market (sell/buy praise units) events and storage struct PraiseListing { uint256 creationId; address seller; uint256 amount; uint256 pricePerPraise; } - PraiseListing[] public praiseListings; - event CreationAdded(uint256 indexed creationId, string metadataUri); - event Praised(uint256 indexed creationId, address indexed user, uint256 pricePaid, uint256 unitsPraised); - event Unpraised(uint256 indexed creationId, address indexed user, uint256 unitsUnpraised, uint256 mannaRefunded); - event ConvictionUpdated(uint256 indexed creationId, uint256 newConviction); - event PraiseListed(uint256 listingId, uint256 creationId, address indexed seller, uint256 amount, uint256 pricePerPraise); - event PraiseSold(uint256 listingId, uint256 creationId, address indexed buyer, uint256 amount, uint256 totalCost); - - constructor(address _manna, address initialOwner) Ownable(initialOwner) { - require(_manna != address(0), "Invalid token address"); - manna = IERC20(_manna); - } + event PraiseListed( + uint256 listingId, + uint256 creationId, + address indexed seller, + uint256 amount, + uint256 pricePerPraise + ); + event PraiseSold( + uint256 listingId, + uint256 creationId, + address indexed buyer, + uint256 amount, + uint256 totalCost + ); function newCreation(string calldata metadataUri) external onlyOwner { creationCount++; Creation storage c = creations[creationCount]; c.id = creationCount; c.metadataUri = metadataUri; + _allCreationIds.push(creationCount); + emit CreationAdded(creationCount, metadataUri); } + function praise(uint256 creationId) external { Creation storage c = creations[creationId]; require(c.id > 0, "Creation does not exist"); + // Price for praising 1 unit uint256 currentStaked = c.totalStaked; uint256 priceForOne = initPraisePrice + (currentStaked * initPraisePrice); - bool transferred = manna.transferFrom(msg.sender, address(this), priceForOne); - require(transferred, "Manna transfer failed"); + // Check user has enough Manna + require(balanceOf(msg.sender) >= priceForOne, "Insufficient Manna to praise"); + + // Transfer Manna from user to this contract + _transfer(msg.sender, address(this), priceForOne); + // Increase creation's pool c.praisePool += priceForOne; + // Update conviction _updateConvictionOnPraise(c, msg.sender); c.totalStaked = currentStaked + 1; c.praiseBalance[msg.sender]++; + emit Praised(creationId, msg.sender, priceForOne, 1); } @@ -79,21 +175,25 @@ contract Abraham is Ownable { require(c.praiseBalance[msg.sender] > 0, "No praise to unpraise"); uint256 refundForOne = initUnpraisePrice; + require(c.praisePool >= refundForOne, "Not enough Manna in praise pool"); - require(c.praisePool >= refundForOne, "Not enough in praise pool"); - + // Update conviction _updateConvictionOnUnpraise(c, msg.sender); + // Adjust balances c.praiseBalance[msg.sender]--; c.totalStaked--; c.praisePool -= refundForOne; - bool sent = manna.transfer(msg.sender, refundForOne); - require(sent, "Refund transfer failed"); + // Transfer Manna refund to the user + _transfer(address(this), msg.sender, refundForOne); emit Unpraised(creationId, msg.sender, 1, refundForOne); } + /** + * @dev List some of your praises for sale (secondary market). + */ function listPraiseForSale( uint256 creationId, uint256 amount, @@ -111,46 +211,72 @@ contract Abraham is Ownable { })); uint256 listingId = praiseListings.length - 1; + emit PraiseListed(listingId, creationId, msg.sender, amount, pricePerPraise); } + /** + * @dev Buy some praises from a listing (secondary market). + * Payment is done in Manna from the buyer’s balance directly to the seller. + */ function buyPraise(uint256 listingId, uint256 amount) external { PraiseListing storage listing = praiseListings[listingId]; - require(listing.amount >= amount, "Not enough praises available"); + require(listing.amount >= amount, "Not enough praises available in this listing"); uint256 totalCost = amount * listing.pricePerPraise; - bool transferred = manna.transferFrom(msg.sender, listing.seller, totalCost); - require(transferred, "Payment failed"); + require(balanceOf(msg.sender) >= totalCost, "Insufficient Manna to purchase praise"); + + // Transfer Manna from buyer to seller + _transfer(msg.sender, listing.seller, totalCost); + // Update praise balances Creation storage c = creations[listing.creationId]; - c.praiseBalance[msg.sender] += amount; + c.praiseBalance[msg.sender] += amount; c.praiseBalance[listing.seller] -= amount; + // Decrease the listing's available amount listing.amount -= amount; emit PraiseSold(listingId, listing.creationId, msg.sender, amount, totalCost); } + /** + * @dev Internal helper to update conviction at praising time. + */ function _updateConvictionOnPraise(Creation storage c, address user) internal { uint256 currentBalance = c.praiseBalance[user]; if (currentBalance > 0) { + // Add conviction for the time already held uint256 timeHeld = block.timestamp - c.stakeTime[user]; uint256 addedConviction = currentBalance * timeHeld; c.conviction += addedConviction; + emit ConvictionUpdated(c.id, c.conviction); } + // Reset the stakeTime to "now" for fresh holdings c.stakeTime[user] = block.timestamp; } + /** + * @dev Internal helper to update conviction at unpraising time. + */ function _updateConvictionOnUnpraise(Creation storage c, address user) internal { uint256 currentBalance = c.praiseBalance[user]; + // Add conviction for the time these units were held uint256 timeHeld = block.timestamp - c.stakeTime[user]; uint256 addedConviction = currentBalance * timeHeld; c.conviction += addedConviction; + emit ConvictionUpdated(c.id, c.conviction); + + // Reset the stakeTime to now (for any remaining units) c.stakeTime[user] = block.timestamp; } + // ========================================================================= + // = VIEW METHODS = + // ========================================================================= + function getCreation(uint256 creationId) external view @@ -163,21 +289,34 @@ contract Abraham is Ownable { ) { Creation storage c = creations[creationId]; - id = c.id; - uri = c.metadataUri; + id = c.id; + uri = c.metadataUri; totalStaked = c.totalStaked; - praisePool = c.praisePool; - conviction = c.conviction; + praisePool = c.praisePool; + conviction = c.conviction; } - function getUserPraise(uint256 creationId, address user) external view returns (uint256) { + /** + * @notice Returns how many praise units a user has staked for a given creation. + */ + function getUserPraise(uint256 creationId, address user) + external + view + returns (uint256) + { return creations[creationId].praiseBalance[user]; } + /** + * @notice Returns an array of all creation IDs. + */ function allCreationIds() external view returns (uint256[] memory) { return _allCreationIds; } + /** + * @notice Returns the current array of praise listings. + */ function getPraiseListings() external view returns (PraiseListing[] memory) { return praiseListings; } diff --git a/contracts/Manna.sol b/contracts/Manna.sol deleted file mode 100644 index 436dab3..0000000 --- a/contracts/Manna.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; - -contract Manna is ERC20, Ownable { - uint256 public constant INITIAL_SUPPLY = 1000000 * (10 ** 18); - uint256 public constant MANNA_PRICE = 0.0001 ether; - - event BoughtManna(address indexed buyer, uint256 amount); - event SoldManna(address indexed seller, uint256 mannaAmount, uint256 ethAmount); - - constructor(address initialOwner) ERC20("Manna", "MANNA") Ownable(initialOwner) { - uint256 initialOwnerSupply = INITIAL_SUPPLY / 2; - _mint(initialOwner, initialOwnerSupply); - } - - function buyManna() external payable { - require(msg.value >= MANNA_PRICE, "Insufficient Ether"); - uint256 mannaAmount = (msg.value * (10 ** 18)) / MANNA_PRICE; - require(totalSupply() + mannaAmount <= INITIAL_SUPPLY, "Manna supply cap reached"); - - _mint(msg.sender, mannaAmount); - emit BoughtManna(msg.sender, mannaAmount); - } - - function sellManna(uint256 mannaAmount) external { - require(balanceOf(msg.sender) >= mannaAmount, "Not enough Manna"); - uint256 ethAmount = (mannaAmount * MANNA_PRICE) / (10 ** 18); - require(address(this).balance >= ethAmount, "Contract lacks Ether"); - - _burn(msg.sender, mannaAmount); - - (bool sent, ) = msg.sender.call{value: ethAmount}(""); - require(sent, "Failed to send Ether"); - - emit SoldManna(msg.sender, mannaAmount, ethAmount); - } - - function getContractBalances() external view returns (uint256 mannaBalance, uint256 ethBalance) { - mannaBalance = balanceOf(address(this)); - ethBalance = address(this).balance; - } - - receive() external payable { - this.buyManna{value: msg.value}(); - } - - function burnFrom(address account, uint256 amount) external { - require(balanceOf(account) >= amount, "Not enough Manna to create art"); - _burn(account, amount); - } -} \ No newline at end of file diff --git a/experimental/AbrahamWithManna.sol b/experimental/AbrahamWithManna.sol new file mode 100644 index 0000000..d78f3ff --- /dev/null +++ b/experimental/AbrahamWithManna.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract Abraham is ERC20, Ownable { + + uint256 public constant INITIAL_SUPPLY = 1000000 * (10 ** 18); + uint256 public constant MANNA_PRICE = 0.0001 ether; + + struct Creation { + uint256 id; + string metadataUri; + uint256 totalStaked; + uint256 praisePool; + uint256 conviction; + mapping(address => uint256) praiseBalance; + mapping(address => uint256) stakeTime; + } + + uint256 public creationCount; + mapping(uint256 => Creation) public creations; + uint256[] private _allCreationIds; + + + uint256 public initPraisePrice = 1e18; // 1 Manna + uint256 public initUnpraisePrice = 1e18;// 1 Manna + + event CreationAdded(uint256 indexed creationId, string metadataUri); + event CreationUpdated( + uint256 indexed creationId, + string metadataUri, + uint256 totalStaked, + uint256 praisePool, + uint256 conviction + ); + event Praised(uint256 indexed creationId, address indexed user, uint256 pricePaid, uint256 unitsPraised); + event Unpraised(uint256 indexed creationId, address indexed user, uint256 unitsUnpraised, uint256 mannaRefunded); + event ConvictionUpdated(uint256 indexed creationId, uint256 newConviction); + + + constructor(address initialOwner) ERC20("Manna", "MANNA") Ownable(initialOwner) { + uint256 initialOwnerSupply = INITIAL_SUPPLY / 2; + _mint(initialOwner, initialOwnerSupply); + } + + + function newCreation(string calldata metadataUri) external onlyOwner { + creationCount++; + Creation storage c = creations[creationCount]; + c.id = creationCount; + c.metadataUri = metadataUri; + _allCreationIds.push(creationCount); + + emit CreationAdded(creationCount, metadataUri); + _emitCreationUpdated(creationCount); + } + + + function praise(uint256 creationId) external { + Creation storage c = creations[creationId]; + require(c.id > 0, "Creation does not exist"); + + uint256 currentStaked = c.totalStaked; + uint256 priceForOne = initPraisePrice + (currentStaked * initPraisePrice); + + require(balanceOf(msg.sender) >= priceForOne, "Not enough Manna"); + _burn(msg.sender, priceForOne); + + c.praisePool += priceForOne; + _updateConvictionOnPraise(c, msg.sender); + + c.totalStaked = currentStaked + 1; + c.praiseBalance[msg.sender]++; + + emit Praised(creationId, msg.sender, priceForOne, 1); + _emitCreationUpdated(creationId); + } + + /** + * @dev User unpraises a creation, receiving `initUnpraisePrice` Manna (minted). + */ + function unpraise(uint256 creationId) external { + Creation storage c = creations[creationId]; + require(c.id > 0, "Creation does not exist"); + require(c.praiseBalance[msg.sender] > 0, "No praise to unpraise"); + + require(c.praisePool >= initUnpraisePrice, "Not enough in praise pool"); + _updateConvictionOnUnpraise(c, msg.sender); + + c.praiseBalance[msg.sender]--; + c.totalStaked--; + + c.praisePool -= initUnpraisePrice; + _mint(msg.sender, initUnpraisePrice); + + emit Unpraised(creationId, msg.sender, 1, initUnpraisePrice); + _emitCreationUpdated(creationId); + } + + function _updateConvictionOnPraise(Creation storage c, address user) internal { + uint256 currentBalance = c.praiseBalance[user]; + if (currentBalance > 0) { + uint256 timeHeld = block.timestamp - c.stakeTime[user]; + uint256 addedConviction = currentBalance * timeHeld; + c.conviction += addedConviction; + emit ConvictionUpdated(c.id, c.conviction); + } + c.stakeTime[user] = block.timestamp; + } + + function _updateConvictionOnUnpraise(Creation storage c, address user) internal { + uint256 currentBalance = c.praiseBalance[user]; + uint256 timeHeld = block.timestamp - c.stakeTime[user]; + uint256 addedConviction = currentBalance * timeHeld; + c.conviction += addedConviction; + emit ConvictionUpdated(c.id, c.conviction); + + c.stakeTime[user] = block.timestamp; + } + + function _emitCreationUpdated(uint256 creationId) internal { + Creation storage c = creations[creationId]; + emit CreationUpdated( + c.id, + c.metadataUri, + c.totalStaked, + c.praisePool, + c.conviction + ); + } + + function getCreation(uint256 creationId) + external + view + returns ( + uint256 id, + string memory uri, + uint256 totalStaked, + uint256 praisePool, + uint256 conviction + ) + { + Creation storage c = creations[creationId]; + id = c.id; + uri = c.metadataUri; + totalStaked = c.totalStaked; + praisePool = c.praisePool; + conviction = c.conviction; + } + + function getUserPraise(uint256 creationId, address user) external view returns (uint256) { + return creations[creationId].praiseBalance[user]; + } + + function allCreationIds() external view returns (uint256[] memory) { + return _allCreationIds; + } + + function mintManna(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + function burnManna(address from, uint256 amount) external onlyOwner { + _burn(from, amount); + } +} \ No newline at end of file diff --git a/experimental/AbrahamWithManna2.sol b/experimental/AbrahamWithManna2.sol new file mode 100644 index 0000000..10eb91f --- /dev/null +++ b/experimental/AbrahamWithManna2.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +// OZ imports for ERC20 and Ownable +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract Abraham is ERC20, Ownable { + + uint256 public constant INITIAL_SUPPLY = 1_000_000 * (10 ** 18); + uint256 public constant MANNA_PRICE = 0.0001 ether; // for buy/sell + + + struct Creation { + uint256 id; + string metadataUri; + uint256 totalStaked; // total 'praise units' staked + uint256 praisePool; // Manna tokens currently in the creation's pool + uint256 conviction; // cumulative 'conviction' for this creation + mapping(address => uint256) praiseBalance; // how many praise units each user has + mapping(address => uint256) stakeTime; // last time user updated stake (for conviction) + } + + // For listing praise for sale (p2p) + struct PraiseListing { + uint256 creationId; + address seller; + uint256 amount; + uint256 pricePerPraise; + } + + uint256 public creationCount; + mapping(uint256 => Creation) public creations; + uint256[] private _allCreationIds; + + uint256 public initPraisePrice = 1e18; // 1 Manna + uint256 public initUnpraisePrice = 1e18; // 1 Manna + + PraiseListing[] public praiseListings; + + event CreationAdded( + uint256 indexed creationId, + string metadataUri + ); + + event CreationStateUpdated( + uint256 indexed creationId, + string metadataUri, + uint256 totalStaked, + uint256 praisePool, + uint256 conviction + ); + + event Praised( + uint256 indexed creationId, + address indexed user, + uint256 mannaPaid, + uint256 unitsPraised + ); + + event Unpraised( + uint256 indexed creationId, + address indexed user, + uint256 unitsUnpraised, + uint256 mannaRefunded + ); + + event ConvictionUpdated( + uint256 indexed creationId, + uint256 newConviction + ); + + event PraiseListed( + uint256 listingId, + uint256 creationId, + address indexed seller, + uint256 amount, + uint256 pricePerPraise + ); + + event PraiseSold( + uint256 listingId, + uint256 creationId, + address indexed buyer, + uint256 amount, + uint256 totalCost + ); + + event BoughtManna( + address indexed buyer, + uint256 amount + ); + + event SoldManna( + address indexed seller, + uint256 mannaAmount, + uint256 ethAmount + ); + + + constructor(address initialOwner) ERC20("Manna", "MANNA") Ownable(initialOwner) { + uint256 initialOwnerSupply = INITIAL_SUPPLY / 2; + _mint(initialOwner, initialOwnerSupply); + } + + function newCreation(string calldata metadataUri) external onlyOwner { + creationCount++; + Creation storage c = creations[creationCount]; + c.id = creationCount; + c.metadataUri = metadataUri; + + _allCreationIds.push(creationCount); + emit CreationAdded(creationCount, metadataUri); + emit CreationStateUpdated( + creationCount, + metadataUri, + c.totalStaked, + c.praisePool, + c.conviction + ); + } + + function praise(uint256 creationId) external { + Creation storage c = creations[creationId]; + require(c.id > 0, "Creation does not exist"); + + uint256 currentStaked = c.totalStaked; + // e.g. priceForOne = basePrice + (N * basePrice) + // => basePrice * (N + 1) + uint256 priceForOne = initPraisePrice + (currentStaked * initPraisePrice); + + // Check that user has enough Manna to pay + require(balanceOf(msg.sender) >= priceForOne, "Not enough Manna to praise"); + + // Transfer Manna from user to this contract (no approval needed, same contract) + _transfer(msg.sender, address(this), priceForOne); + + // Update creation pool and stats + c.praisePool += priceForOne; + _updateConvictionOnPraise(c, msg.sender); + + c.totalStaked = currentStaked + 1; + c.praiseBalance[msg.sender] += 1; + + emit Praised(creationId, msg.sender, priceForOne, 1); + emit CreationStateUpdated( + creationId, + c.metadataUri, + c.totalStaked, + c.praisePool, + c.conviction + ); + } + function unpraise(uint256 creationId) external { + Creation storage c = creations[creationId]; + require(c.id > 0, "Creation does not exist"); + require(c.praiseBalance[msg.sender] > 0, "No praise to unpraise"); + + uint256 refundForOne = initUnpraisePrice; + + require(c.praisePool >= refundForOne, "Not enough in praise pool"); + _updateConvictionOnUnpraise(c, msg.sender); + + c.praiseBalance[msg.sender] -= 1; + c.totalStaked -= 1; + c.praisePool -= refundForOne; + + // Transfer Manna back to user + _transfer(address(this), msg.sender, refundForOne); + + emit Unpraised(creationId, msg.sender, 1, refundForOne); + // Emit creation state update + emit CreationStateUpdated( + creationId, + c.metadataUri, + c.totalStaked, + c.praisePool, + c.conviction + ); + } + + function listPraiseForSale( + uint256 creationId, + uint256 amount, + uint256 pricePerPraise + ) external { + Creation storage c = creations[creationId]; + require(c.id > 0, "Creation does not exist"); + require(c.praiseBalance[msg.sender] >= amount, "Insufficient praises to sell"); + + praiseListings.push(PraiseListing({ + creationId: creationId, + seller: msg.sender, + amount: amount, + pricePerPraise: pricePerPraise + })); + + uint256 listingId = praiseListings.length - 1; + emit PraiseListed(listingId, creationId, msg.sender, amount, pricePerPraise); + } + + function buyPraise(uint256 listingId, uint256 amount) external { + PraiseListing storage listing = praiseListings[listingId]; + require(listing.amount >= amount, "Not enough praises available"); + + uint256 totalCost = amount * listing.pricePerPraise; + // Buyer must have enough Manna + require(balanceOf(msg.sender) >= totalCost, "Not enough Manna for purchase"); + + // Transfer Manna from buyer to seller + _transfer(msg.sender, listing.seller, totalCost); + + // Update praise balances + Creation storage c = creations[listing.creationId]; + c.praiseBalance[msg.sender] += amount; + c.praiseBalance[listing.seller] -= amount; + + listing.amount -= amount; + + emit PraiseSold(listingId, listing.creationId, msg.sender, amount, totalCost); + } + + function _updateConvictionOnPraise(Creation storage c, address user) internal { + uint256 currentBalance = c.praiseBalance[user]; + + // If user already had some praise staked, add conviction for the time it was staked + if (currentBalance > 0) { + uint256 timeHeld = block.timestamp - c.stakeTime[user]; + uint256 addedConviction = currentBalance * timeHeld; + c.conviction += addedConviction; + emit ConvictionUpdated(c.id, c.conviction); + } + + // Reset stakeTime + c.stakeTime[user] = block.timestamp; + } + + function _updateConvictionOnUnpraise(Creation storage c, address user) internal { + // Add conviction for the time the user held the stake + uint256 currentBalance = c.praiseBalance[user]; + uint256 timeHeld = block.timestamp - c.stakeTime[user]; + uint256 addedConviction = currentBalance * timeHeld; + c.conviction += addedConviction; + + emit ConvictionUpdated(c.id, c.conviction); + + // Reset stakeTime + c.stakeTime[user] = block.timestamp; + } + + function getCreation(uint256 creationId) + external + view + returns ( + uint256 id, + string memory uri, + uint256 totalStaked, + uint256 praisePool, + uint256 conviction + ) + { + Creation storage c = creations[creationId]; + id = c.id; + uri = c.metadataUri; + totalStaked = c.totalStaked; + praisePool = c.praisePool; + conviction = c.conviction; + } + + function getUserPraise(uint256 creationId, address user) + external + view + returns (uint256) + { + return creations[creationId].praiseBalance[user]; + } + + function allCreationIds() external view returns (uint256[] memory) { + return _allCreationIds; + } + + function getPraiseListings() external view returns (PraiseListing[] memory) { + return praiseListings; + } + + function buyManna() public payable { + require(msg.value >= MANNA_PRICE, "Insufficient Ether sent"); + uint256 mannaAmount = (msg.value * (10 ** 18)) / MANNA_PRICE; + + // If you want to cap at INITIAL_SUPPLY, you can check totalSupply + require(totalSupply() + mannaAmount <= INITIAL_SUPPLY, "Supply cap reached"); + + _mint(msg.sender, mannaAmount); + emit BoughtManna(msg.sender, mannaAmount); + } + + function sellManna(uint256 mannaAmount) external { + require(balanceOf(msg.sender) >= mannaAmount, "Not enough Manna to sell"); + uint256 ethAmount = (mannaAmount * MANNA_PRICE) / (10 ** 18); + require(address(this).balance >= ethAmount, "Contract lacks sufficient Ether"); + + _burn(msg.sender, mannaAmount); + + (bool sent, ) = msg.sender.call{value: ethAmount}(""); + require(sent, "Failed to send Ether"); + + emit SoldManna(msg.sender, mannaAmount, ethAmount); + } + + function getContractBalances() + external + view + returns (uint256 mannaBalance, uint256 ethBalance) + { + mannaBalance = balanceOf(address(this)); + ethBalance = address(this).balance; + } + receive() external payable { + if (msg.value > 0) { + buyManna(); + } + } +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 55672f7..c0487ba 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,5 +1,6 @@ require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config({ path: __dirname + "/.env" }); +require("@nomicfoundation/hardhat-verify"); const privateKey = process.env.PRIVATE_KEY; @@ -17,7 +18,7 @@ module.exports = { accounts: [privateKey], gasPrice: 1000000000, }, - basesepolia: { + "base-sepolia": { url: "https://sepolia.base.org", accounts: [privateKey], gasPrice: 1000000000, @@ -34,4 +35,19 @@ module.exports = { accounts: [privateKey], }, }, + etherscan: { + apiKey: { + "base-sepolia": "empty", + }, + customChains: [ + { + network: "base-sepolia", + chainId: 84532, + urls: { + apiURL: "https://base-sepolia.blockscout.com/api", + browserURL: "https://base-sepolia.blockscout.com", + }, + }, + ], + }, }; diff --git a/package-lock.json b/package-lock.json index 71935c2..62889e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "devDependencies": { "@nomicfoundation/hardhat-ignition": "^0.15.7", "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.12", "hardhat": "^2.22.15" } }, @@ -1457,18 +1458,18 @@ } }, "node_modules/@nomicfoundation/hardhat-verify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.11.tgz", - "integrity": "sha512-lGIo4dNjVQFdsiEgZp3KP6ntLiF7xJEJsbNHfSyIiFCyI0Yv0518ElsFtMC5uCuHEChiBBMrib9jWQvHHT+X3Q==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.12.tgz", + "integrity": "sha512-Lg3Nu7DCXASQRVI/YysjuAX2z8jwOCbS0w5tz2HalWGSTZThqA0v9N0v0psHbKNqzPJa8bNOeapIVSziyJTnAg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "@ethersproject/abi": "^5.1.2", "@ethersproject/address": "^5.0.2", "cbor": "^8.1.0", - "chalk": "^2.4.2", "debug": "^4.1.1", "lodash.clonedeep": "^4.5.0", + "picocolors": "^1.1.0", "semver": "^6.3.0", "table": "^6.8.0", "undici": "^5.14.0" @@ -2111,7 +2112,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2273,7 +2273,6 @@ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -2597,7 +2596,6 @@ "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", "dev": true, - "peer": true, "dependencies": { "nofilter": "^3.1.0" }, @@ -3635,8 +3633,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fast-glob": { "version": "3.3.2", @@ -3666,8 +3663,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fastq": { "version": "1.17.1", @@ -4466,8 +4462,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json-stream-stringify": { "version": "3.1.6", @@ -4593,8 +4588,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -4607,8 +4601,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -5372,6 +5365,13 @@ "node": ">=0.12" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5621,7 +5621,6 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6024,7 +6023,6 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -6042,7 +6040,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -6058,7 +6055,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -6070,8 +6066,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/solc": { "version": "0.8.26", @@ -6346,7 +6341,6 @@ "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", "dev": true, - "peer": true, "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", diff --git a/package.json b/package.json index a44508d..8f893e4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "devDependencies": { "@nomicfoundation/hardhat-ignition": "^0.15.7", "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.12", "hardhat": "^2.22.15" }, "dependencies": { diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 349d73a..ce3fb34 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -1,5 +1,7 @@ const hre = require("hardhat"); -require("dotenv").config(); +import * as dotenv from "dotenv"; + +dotenv.config(); const contractOwner = process.env.CONTRACT_OWNER; @@ -8,37 +10,25 @@ async function main() { throw new Error("CONTRACT_OWNER not set in .env"); } - // Deploy Manna contract - const manna = await deployManna(contractOwner); - // Deploy Abraham contract - const abraham = await deployAbraham(manna, contractOwner); + const abraham = await deployAbraham(contractOwner); console.log("Deployment complete."); - console.log(`Manna: ${await manna.getAddress()}`); - console.log(`Abraham: ${await abraham.getAddress()}`); -} - -async function deployManna(initialOwner: string) { - const Manna = await hre.ethers.getContractFactory("Manna"); - const manna = await Manna.deploy(initialOwner); - console.log(`Manna deployed at: ${await manna.getAddress()}`); - return manna; + console.log(`Abraham deployed at: ${await abraham.getAddress()}`); } -async function deployAbraham( - manna: { getAddress: () => any }, - initialOwner: string -) { - const mannaAddress = await manna.getAddress(); +async function deployAbraham(initialOwner: string) { const Abraham = await hre.ethers.getContractFactory("Abraham"); - const abraham = await Abraham.deploy(mannaAddress, initialOwner); + const abraham = await Abraham.deploy(initialOwner); + + await abraham.waitForDeployment(); + console.log(`Abraham deployed at: ${await abraham.getAddress()}`); return abraham; } // Execute the deployment script main().catch((error) => { - console.error(error); + console.error("Error deploying contract:", error); process.exitCode = 1; }); diff --git a/test/Abraham.ts b/test/Abraham.ts index 67a4994..49d4110 100644 --- a/test/Abraham.ts +++ b/test/Abraham.ts @@ -1,143 +1,301 @@ import { expect } from "chai"; const hre = require("hardhat"); -describe("Abraham and Manna Contracts", function () { - let mannaContract: any; - let abrahamContract: any; +describe("Abraham Contract", function () { + let abraham: any; let owner: any; let user1: any; let user2: any; - before(async () => { - [owner, user1, user2] = await hre.ethers.getSigners(); + // Ethers v6 typically returns `bigint` for parseEther, but let's store them as bigints + const INITIAL_SUPPLY = hre.ethers.parseEther("1000000"); // 1,000,000 * 10^18 + const MANNA_PRICE = hre.ethers.parseEther("0.0001"); // 0.0001 ETH - // Deploy Manna contract - const Manna = await hre.ethers.getContractFactory("Manna"); - mannaContract = await Manna.deploy(owner.address); + beforeEach(async function () { + [owner, user1, user2] = await hre.ethers.getSigners(); - // Deploy Abraham contract const Abraham = await hre.ethers.getContractFactory("Abraham"); - abrahamContract = await Abraham.deploy( - await mannaContract.getAddress(), - owner.address - ); - - // Transfer Manna to users for testing - const initialManna = hre.ethers.parseEther("1000"); - await mannaContract.connect(owner).transfer(user1.address, initialManna); - await mannaContract.connect(owner).transfer(user2.address, initialManna); - }); - it("Should deploy contracts with correct initial settings", async () => { - expect(await mannaContract.name()).to.equal("Manna"); - expect(await mannaContract.symbol()).to.equal("MANNA"); - expect(await abrahamContract.initPraisePrice()).to.equal( - hre.ethers.parseEther("0.1") - ); - expect(await abrahamContract.initUnpraisePrice()).to.equal( - hre.ethers.parseEther("0.1") - ); + abraham = await Abraham.deploy(owner.address); + await abraham.waitForDeployment(); }); - it("Should allow the owner to create a new creation", async () => { - const metadataUri = "ipfs://creation-metadata"; - await expect(abrahamContract.connect(owner).newCreation(metadataUri)) - .to.emit(abrahamContract, "CreationAdded") - .withArgs(1, metadataUri); + describe("Manna (ERC20) Logic", function () { + it("Should assign half of the INITIAL_SUPPLY to the owner at deployment", async function () { + const ownerBalance = await abraham.balanceOf(owner.address); + const totalSupply = await abraham.totalSupply(); + const expectedHalf = INITIAL_SUPPLY / 2n; - const creation = await abrahamContract.getCreation(1); - expect(creation.uri).to.equal(metadataUri); - expect(creation.totalStaked).to.equal(0); - expect(creation.praisePool).to.equal(0); - }); + expect(ownerBalance).to.equal(totalSupply); + expect(totalSupply).to.equal(expectedHalf); + }); - it("Should allow users to praise a creation", async () => { - const creationId = 1; - const praisePrice = await abrahamContract.initPraisePrice(); - const abrahamAddress = await abrahamContract.getAddress(); - await mannaContract.connect(user1).approve(abrahamAddress, praisePrice); - await expect(abrahamContract.connect(user1).praise(creationId)) - .to.emit(abrahamContract, "Praised") - .withArgs(creationId, user1.address, praisePrice, 1); - - const creation = await abrahamContract.getCreation(creationId); - expect(creation.totalStaked).to.equal(1); - expect(creation.praisePool).to.equal(praisePrice); - - const userPraise = await abrahamContract.getUserPraise( - creationId, - user1.address - ); - expect(userPraise).to.equal(1); - }); + it("Should allow buying Manna with Ether", async function () { + await abraham.connect(user1).buyManna({ value: MANNA_PRICE }); + const user1MannaBal = await abraham.balanceOf(user1.address); + const oneManna = 1n * 10n ** 18n; + expect(user1MannaBal).to.equal(oneManna); + }); - it("Should allow users to unpraise a creation", async () => { - const creationId = 1; - const unpraisePrice = await abrahamContract.initUnpraisePrice(); + it("Should emit BoughtManna event when buying Manna", async function () { + const oneManna = 1n * 10n ** 18n; + await expect(abraham.connect(user1).buyManna({ value: MANNA_PRICE })) + .to.emit(abraham, "BoughtManna") + .withArgs(user1.address, oneManna); + }); - await expect(abrahamContract.connect(user1).unpraise(creationId)) - .to.emit(abrahamContract, "Unpraised") - .withArgs(creationId, user1.address, 1, unpraisePrice); + it("Should revert if not enough Ether sent for min purchase", async function () { + const tooLittle = MANNA_PRICE - 1n; + await expect( + abraham.connect(user1).buyManna({ value: tooLittle }) + ).to.be.rejectedWith("Insufficient Ether for min purchase"); + }); - const creation = await abrahamContract.getCreation(creationId); - expect(creation.totalStaked).to.equal(0); - expect(creation.praisePool).to.equal(0); + it("Should allow selling Manna for Ether if contract has Ether", async function () { + // user1 buys 0.001 ETH worth of Manna + const buyAmount = hre.ethers.parseEther("0.001"); + await abraham.connect(user1).buyManna({ value: buyAmount }); - const userPraise = await abrahamContract.getUserPraise( - creationId, - user1.address - ); - expect(userPraise).to.equal(0); - }); + // user1 now has some Manna to sell + const user1MannaBal = await abraham.balanceOf(user1.address); - it("Should allow users to list praises for sale", async () => { - const creationId = 1; - const praisePrice = await abrahamContract.initPraisePrice(); - const listingPrice = hre.ethers.parseEther("0.2"); - const abrahamAddress = await abrahamContract.getAddress(); - // Praise a creation - await mannaContract.connect(user1).approve(abrahamAddress, praisePrice); - await abrahamContract.connect(user1).praise(creationId); - - // List praise for sale - await expect( - abrahamContract - .connect(user1) - .listPraiseForSale(creationId, 1, listingPrice) - ) - .to.emit(abrahamContract, "PraiseListed") - .withArgs(0, creationId, user1.address, 1, listingPrice); - - const listings = await abrahamContract.getPraiseListings(); - expect(listings[0].creationId).to.equal(creationId); - expect(listings[0].seller).to.equal(user1.address); - expect(listings[0].amount).to.equal(1); - expect(listings[0].pricePerPraise).to.equal(listingPrice); + // user1's ETH before + const user1InitialEthBN = BigInt( + (await hre.ethers.provider.getBalance(user1.address)).toString() + ); + + // Sell all Manna + await abraham.connect(user1).sellManna(user1MannaBal); + + // user1's ETH after + const user1FinalEthBN = BigInt( + (await hre.ethers.provider.getBalance(user1.address)).toString() + ); + + // We expect user1FinalEthBN > user1InitialEthBN (they gained some ETH from selling) + expect(user1FinalEthBN).to.be.greaterThan(user1InitialEthBN); + }); + + it("Should emit SoldManna event when selling Manna", async function () { + // user1 buys 1 Manna + await abraham.connect(user1).buyManna({ value: MANNA_PRICE }); + const user1Balance = await abraham.balanceOf(user1.address); + + await expect(abraham.connect(user1).sellManna(user1Balance)) + .to.emit(abraham, "SoldManna") + .withArgs(user1.address, user1Balance, MANNA_PRICE); + }); + + it("Should revert if user tries to sell Manna they don't have", async function () { + await expect(abraham.connect(user1).sellManna(1n)).to.be.rejectedWith( + "Not enough Manna to sell" + ); + }); + + it("Should return correct contract balances", async function () { + // user1 buys 1 Manna => 0.0001 ETH + await abraham.connect(user1).buyManna({ value: MANNA_PRICE }); + const [mannaBalance, ethBalance] = await abraham.getContractBalances(); + + // Typically 0 Manna in contract after a direct buy + expect(mannaBalance).to.equal(0n); + // Contract's ETH balance => MANNA_PRICE + expect(ethBalance).to.equal(MANNA_PRICE); + }); + + it("Should allow buying Manna via fallback (receive) function", async () => { + // For Ethers v6, the contract address is in `.target`; for older versions it's `.address`. + const contractAddress = abraham.target || abraham.address; + + // We want to buy 2 Manna => cost is MANNA_PRICE * 2 + const fallbackBuy = MANNA_PRICE * 2n; + + await user1.sendTransaction({ + to: contractAddress, + value: fallbackBuy, + }); + + // user1 should have 2 Manna now => 2 * 10^18 + const user1Bal = await abraham.balanceOf(user1.address); + const twoManna = 2n * 10n ** 18n; + expect(user1Bal).to.equal(twoManna); + }); }); - it("Should allow users to buy praises", async () => { - const listingId = 0; - const listing = await abrahamContract.getPraiseListings(); - const totalCost = listing[listingId].pricePerPraise; - const abrahamAddress = await abrahamContract.getAddress(); - - await mannaContract.connect(user2).approve(abrahamAddress, totalCost); - await expect(abrahamContract.connect(user2).buyPraise(listingId, 1)) - .to.emit(abrahamContract, "PraiseSold") - .withArgs( - listingId, - listing[listingId].creationId, - user2.address, - 1, - totalCost + // ------------------------------------ + // CREATION LOGIC + // ------------------------------------ + describe("Creation Logic", function () { + it("Should allow only owner to create a new creation", async function () { + const metadataUri = "ipfs://creation-metadata"; + + // Attempt as user1 => revert w/ custom error from new Ownable + await expect(abraham.connect(user1).newCreation(metadataUri)) + .to.be.revertedWithCustomError(abraham, "OwnableUnauthorizedAccount") + .withArgs(user1.address); + + // Now as owner => success + await expect(abraham.connect(owner).newCreation(metadataUri)) + .to.emit(abraham, "CreationAdded") + .withArgs(1n, metadataUri); + + const creationData = await abraham.getCreation(1); + expect(creationData.uri).to.equal(metadataUri); + expect(creationData.totalStaked).to.equal(0n); + expect(creationData.praisePool).to.equal(0n); + + const creationIds = await abraham.allCreationIds(); + expect(creationIds.length).to.equal(1); + expect(creationIds[0]).to.equal(1n); + }); + + it("Should allow praising a creation (assuming user has enough Manna)", async function () { + await abraham.connect(owner).newCreation("ipfs://creation1"); + + // user1 buys 2 Manna => 2 * MANNA_PRICE in ETH + const buyETH = MANNA_PRICE * 2n; + await abraham.connect(user1).buyManna({ value: buyETH }); + + const initPraisePrice = await abraham.initPraisePrice(); // typically 1e18 => 1 Manna + + await expect(abraham.connect(user1).praise(1)) + .to.emit(abraham, "Praised") + .withArgs(1n, user1.address, initPraisePrice, 1n); + + const creation = await abraham.getCreation(1); + expect(creation.totalStaked).to.equal(1n); + expect(creation.praisePool).to.equal(initPraisePrice); + + const user1Praise = await abraham.getUserPraise(1, user1.address); + expect(user1Praise).to.equal(1n); + }); + + it("Should revert praising if creation does not exist", async function () { + await expect(abraham.connect(user1).praise(999)).to.be.rejectedWith( + "Creation does not exist" ); + }); + + it("Should revert praising if user doesn’t have enough Manna", async function () { + await abraham.connect(owner).newCreation("ipfs://creation1"); + // user1 has 0 Manna + await expect(abraham.connect(user1).praise(1)).to.be.rejectedWith( + "Insufficient Manna to praise" + ); + }); + + it("Should allow unpraising and refund Manna", async function () { + await abraham.connect(owner).newCreation("ipfs://creation1"); + + // user1 buys 2 Manna and praises once + const buy2Manna = MANNA_PRICE * 2n; + await abraham.connect(user1).buyManna({ value: buy2Manna }); + await abraham.connect(user1).praise(1); + + const initUnpraisePrice = await abraham.initUnpraisePrice(); + await expect(abraham.connect(user1).unpraise(1)) + .to.emit(abraham, "Unpraised") + .withArgs(1n, user1.address, 1n, initUnpraisePrice); + + const creation = await abraham.getCreation(1); + expect(creation.totalStaked).to.equal(0n); + expect(creation.praisePool).to.equal(0n); + + const user1Praise = await abraham.getUserPraise(1, user1.address); + expect(user1Praise).to.equal(0n); + }); + + it("Should revert if user has no praise to unpraise", async function () { + await abraham.connect(owner).newCreation("ipfs://creation1"); + // user1 never praised + await expect(abraham.connect(user1).unpraise(1)).to.be.rejectedWith( + "No praise to unpraise" + ); + }); + }); + + // ------------------------------------ + // SECONDARY MARKET LOGIC + // ------------------------------------ + describe("Secondary Market", () => { + beforeEach(async () => { + await abraham.connect(owner).newCreation("ipfs://creation1"); + const buyMannaETH = MANNA_PRICE * 5n; + await abraham.connect(user1).buyManna({ value: buyMannaETH }); + // user1 praises #1 twice + await abraham.connect(user1).praise(1); + await abraham.connect(user1).praise(1); + }); - const user2Praise = await abrahamContract.getUserPraise( - listing[listingId].creationId, - user2.address - ); - expect(user2Praise).to.equal(1); + it("Should list praises for sale if user has them", async () => { + const listingPrice = hre.ethers.parseEther("0.2"); + await expect(abraham.connect(user1).listPraiseForSale(1, 2, listingPrice)) + .to.emit(abraham, "PraiseListed") + .withArgs(0n, 1n, user1.address, 2n, listingPrice); - const updatedListings = await abrahamContract.getPraiseListings(); - expect(updatedListings[listingId].amount).to.equal(0); // Listing completed + const listings = await abraham.getPraiseListings(); + expect(listings.length).to.equal(1); + expect(listings[0].creationId).to.equal(1n); + expect(listings[0].seller).to.equal(user1.address); + expect(listings[0].amount).to.equal(2n); + expect(listings[0].pricePerPraise).to.equal(listingPrice); + }); + + it("Should revert if user tries to list more praise than they own", async () => { + await expect( + abraham + .connect(user1) + .listPraiseForSale(1, 5, hre.ethers.parseEther("0.1")) + ).to.be.rejectedWith("Insufficient praises to sell"); + }); + + it("Should allow another user to buy praises from listing", async () => { + // user1 lists 2 praises at 0.2 Manna each + const listPrice = hre.ethers.parseEther("0.2"); + await abraham.connect(user1).listPraiseForSale(1, 2, listPrice); + + // user2 needs enough Manna => buy ~50 Manna + await abraham.connect(user2).buyManna({ value: MANNA_PRICE * 50n }); + + // user2 buys 1 praise => cost = 1 * 0.2 Manna + const listingId = 0; + const amountToBuy = 1; + const totalCost = listPrice * BigInt(amountToBuy); + + await expect(abraham.connect(user2).buyPraise(listingId, amountToBuy)) + .to.emit(abraham, "PraiseSold") + .withArgs(listingId, 1n, user2.address, 1n, totalCost); + + const user2Praises = await abraham.getUserPraise(1, user2.address); + expect(user2Praises).to.equal(1n); + + // user1 had 2 => sold 1 => now 1 left + const user1Praises = await abraham.getUserPraise(1, user1.address); + expect(user1Praises).to.equal(1n); + + // listing's amount is 1 now + const listings = await abraham.getPraiseListings(); + expect(listings[listingId].amount).to.equal(1n); + }); + + it("Should revert if listing doesn’t have enough praises left", async () => { + const listPrice = hre.ethers.parseEther("0.2"); + await abraham.connect(user1).listPraiseForSale(1, 2, listPrice); + + await abraham.connect(user2).buyManna({ value: MANNA_PRICE * 10n }); + await expect(abraham.connect(user2).buyPraise(0, 3)).to.be.rejectedWith( + "Not enough praises available in this listing" + ); + }); + + it("Should revert if buyer doesn't have enough Manna", async () => { + // user1 lists 2 praises at 1 Manna each => total 2 Manna + await abraham + .connect(user1) + .listPraiseForSale(1, 2, hre.ethers.parseEther("1")); + // user2 has 0 Manna => revert + await expect(abraham.connect(user2).buyPraise(0, 1)).to.be.rejectedWith( + "Insufficient Manna to purchase praise" + ); + }); }); }); diff --git a/test/Manna.ts b/test/Manna.ts deleted file mode 100644 index 86b1882..0000000 --- a/test/Manna.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { expect } from "chai"; -const hre = require("hardhat"); -import { Manna } from "../typechain-types"; - -describe("Manna", function () { - let manna: Manna; - let owner: any, addr1: any, addr2: any; - - beforeEach(async function () { - const Manna = await hre.ethers.getContractFactory("Manna"); - [owner, addr1, addr2] = await hre.ethers.getSigners(); - manna = (await Manna.deploy(owner.address)) as Manna; - }); - - it("Should assign half of initial supply to the owner", async function () { - const ownerBalance = await manna.balanceOf(owner.address); - const total = await manna.totalSupply(); - // Initially half of INITIAL_SUPPLY to owner - // The rest will be minted when buying Manna - expect(ownerBalance).to.equal(total); - }); - - it("Should allow buying Manna with Ether", async function () { - const buyAmount = hre.ethers.parseEther("0.0001"); // 1 Manna - await manna.connect(addr1).buyManna({ value: buyAmount }); - const mannaBalance = await manna.balanceOf(addr1.address); - const oneManna = hre.ethers.parseUnits("1", 18); - expect(mannaBalance).to.equal(oneManna); - }); - - it("Should emit a BoughtManna event when buying Manna", async function () { - const buyAmount = hre.ethers.parseEther("0.0001"); - const oneManna = hre.ethers.parseUnits("1", 18); - await expect(manna.connect(addr1).buyManna({ value: buyAmount })) - .to.emit(manna, "BoughtManna") - .withArgs(addr1.address, oneManna); - }); - - it("Should revert if not enough Ether to buy Manna", async function () { - const tooLittle = hre.ethers.parseEther("0.00005"); - await expect( - manna.connect(addr1).buyManna({ value: tooLittle }) - ).to.be.revertedWith("Insufficient Ether"); - }); - - it("Should allow selling Manna for Ether if contract has Ether", async function () { - // Give contract Ether by buying from owner - const buyAmount = hre.ethers.parseEther("0.001"); - await manna.connect(addr1).buyManna({ value: buyAmount }); - - // Now addr1 has some Manna - const mannaBalance = await manna.balanceOf(addr1.address); - - // Sell it back - const initialEthBalance = await hre.ethers.provider.getBalance( - addr1.address - ); - await manna.connect(addr1).sellManna(mannaBalance); - const finalEthBalance = await hre.ethers.provider.getBalance(addr1.address); - - expect(finalEthBalance).to.be.gt(initialEthBalance); - }); - - it("Should emit SoldManna event when selling Manna", async function () { - const buyAmount = hre.ethers.parseEther("0.0001"); - await manna.connect(addr1).buyManna({ value: buyAmount }); - - const mannaBalance = await manna.balanceOf(addr1.address); - await expect(manna.connect(addr1).sellManna(mannaBalance)) - .to.emit(manna, "SoldManna") - .withArgs(addr1.address, mannaBalance, buyAmount); - }); - - it("Should revert selling Manna if user doesn't have enough", async function () { - await expect(manna.connect(addr1).sellManna(1)).to.be.revertedWith( - "Not enough Manna" - ); - }); - - it("Should get correct contract balances", async function () { - const buyAmount = hre.ethers.parseEther("0.0001"); - await manna.connect(addr1).buyManna({ value: buyAmount }); - const [mannaBalance, ethBalance] = await manna.getContractBalances(); - expect(mannaBalance).to.equal(0); - expect(ethBalance).to.equal(buyAmount); - }); -});