From 2d0cf23d51c236de5fe2cd86052dd4aebd3e54b6 Mon Sep 17 00:00:00 2001 From: bashybaranaba Date: Tue, 31 Dec 2024 22:28:04 +0300 Subject: [PATCH] feat: abraham with bonding curve and conviction --- contracts/Abraham.sol | 241 ++++++++++------- contracts/Manna.sol | 54 ++++ experimental/Abraham.sol | 137 ++++++++++ experimental/Abraham2.sol | 212 +++++++++++++++ experimental/AbrahamMarket.sol | 293 +++++++++++++++++++++ {contracts => experimental}/AbrahamNFT.sol | 7 +- {contracts => experimental}/MannaToken.sol | 4 +- scripts/deploy.ts | 92 +------ test/Abraham.ts | 226 ++++++++-------- test/AbrahamNFT.ts | 38 --- test/Manna.ts | 44 ++-- 11 files changed, 996 insertions(+), 352 deletions(-) create mode 100644 contracts/Manna.sol create mode 100644 experimental/Abraham.sol create mode 100644 experimental/Abraham2.sol create mode 100644 experimental/AbrahamMarket.sol rename {contracts => experimental}/AbrahamNFT.sol (70%) rename {contracts => experimental}/MannaToken.sol (89%) delete mode 100644 test/AbrahamNFT.ts diff --git a/contracts/Abraham.sol b/contracts/Abraham.sol index 658a9e1..4a1ebb9 100644 --- a/contracts/Abraham.sol +++ b/contracts/Abraham.sol @@ -1,135 +1,184 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; - -interface IMannaToken { - function burnFrom(address account, uint256 amount) external; -} - -interface IAbrahamNFT { - function mintCreationNFT(address to, uint256 creationId) external; -} - -contract Abraham is ERC1155, Ownable { - address public mannaToken; - address public abrahamNFT; - uint256 public creationCounter; - uint256 public endTimestamp; - - uint256 public minimumMannaSpend = 1e18; - - struct CreationData { - uint256 praises; - uint256 burns; - uint256 blessings; - uint256 totalMannaSpent; - string image; // New field for image URL - } - struct UserStats { - uint256 praiseCount; - uint256 praiseMannaSpent; - uint256 burnCount; - uint256 burnMannaSpent; +contract Abraham is Ownable { + IERC20 public immutable manna; + + struct Creation { + uint256 id; + string metadataUri; + uint256 totalStaked; + uint256 praisePool; + uint256 conviction; + mapping(address => uint256) praiseBalance; + mapping(address => uint256) stakeTime; } - mapping(uint256 => CreationData) public creations; - mapping(uint256 => mapping(address => UserStats)) public userParticipation; + uint256 public creationCount; + mapping(uint256 => Creation) public creations; + uint256[] private _allCreationIds; - event CreationReleased(uint256 indexed creationId, string image); // include image in event - event Praised(uint256 indexed creationId, address indexed user, uint256 amount); - event Burned(uint256 indexed creationId, address indexed user, uint256 amount); - event Blessed(uint256 indexed creationId, address indexed user, uint256 amount); + uint256 public initPraisePrice = 1e17; // 0.1 Manna + uint256 public initUnpraisePrice = 1e17; // 0.1 Manna - constructor(address _mannaToken, string memory uri_, address initialOwner) ERC1155(uri_) Ownable(initialOwner) { - mannaToken = _mannaToken; - endTimestamp = block.timestamp + (13 * 365 days); + struct PraiseListing { + uint256 creationId; + address seller; + uint256 amount; + uint256 pricePerPraise; } - modifier notEnded() { - require(block.timestamp < endTimestamp, "Contract expired"); - _; - } + PraiseListing[] public praiseListings; - function setMinimumMannaSpend(uint256 newMin) external onlyOwner { - minimumMannaSpend = newMin; + 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); } - function setAbrahamNFT(address _abrahamNFT) external onlyOwner { - abrahamNFT = _abrahamNFT; + 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 releaseCreation(string memory image) external onlyOwner notEnded { - creationCounter += 1; + function praise(uint256 creationId) external { + Creation storage c = creations[creationId]; + require(c.id > 0, "Creation does not exist"); - creations[creationCounter] = CreationData({ - praises: 0, - burns: 0, - blessings: 0, - totalMannaSpent: 0, - image: image - }); + uint256 currentStaked = c.totalStaked; + uint256 priceForOne = initPraisePrice + (currentStaked * initPraisePrice); - emit CreationReleased(creationCounter, image); + bool transferred = manna.transferFrom(msg.sender, address(this), priceForOne); + require(transferred, "Manna transfer failed"); - require(abrahamNFT != address(0), "AbrahamNFT not set"); - IAbrahamNFT(abrahamNFT).mintCreationNFT(owner(), creationCounter); - } + c.praisePool += priceForOne; - function _spendManna(uint256 amount) internal { - require(amount >= minimumMannaSpend, "Spend more Manna"); - (bool success, ) = mannaToken.call( - abi.encodeWithSignature("burnFrom(address,uint256)", msg.sender, amount) - ); - require(success, "Manna burn failed"); + _updateConvictionOnPraise(c, msg.sender); + + c.totalStaked = currentStaked + 1; + c.praiseBalance[msg.sender]++; + emit Praised(creationId, msg.sender, priceForOne, 1); } - function praise(uint256 creationId, uint256 amount) external notEnded { - require(creationId > 0 && creationId <= creationCounter, "Invalid creation"); - _spendManna(amount); + 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); - creations[creationId].praises += 1; - creations[creationId].totalMannaSpent += amount; + c.praiseBalance[msg.sender]--; + c.totalStaked--; + c.praisePool -= refundForOne; - userParticipation[creationId][msg.sender].praiseCount += 1; - userParticipation[creationId][msg.sender].praiseMannaSpent += amount; + bool sent = manna.transfer(msg.sender, refundForOne); + require(sent, "Refund transfer failed"); - _mint(msg.sender, creationId, amount, ""); - emit Praised(creationId, msg.sender, amount); + emit Unpraised(creationId, msg.sender, 1, refundForOne); } - function burnCreation(uint256 creationId, uint256 amount) external notEnded { - require(creationId > 0 && creationId <= creationCounter, "Invalid creation"); - _spendManna(amount); + 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"); - creations[creationId].burns += 1; - creations[creationId].totalMannaSpent += amount; + uint256 totalCost = amount * listing.pricePerPraise; + bool transferred = manna.transferFrom(msg.sender, listing.seller, totalCost); + require(transferred, "Payment failed"); - userParticipation[creationId][msg.sender].burnCount += 1; - userParticipation[creationId][msg.sender].burnMannaSpent += amount; + Creation storage c = creations[listing.creationId]; + c.praiseBalance[msg.sender] += amount; + c.praiseBalance[listing.seller] -= amount; - _mint(msg.sender, creationId, amount, ""); - emit Burned(creationId, msg.sender, amount); + listing.amount -= amount; + + emit PraiseSold(listingId, listing.creationId, msg.sender, amount, totalCost); } - function bless(uint256 creationId, uint256 amount) external notEnded { - require(creationId > 0 && creationId <= creationCounter, "Invalid creation"); - _spendManna(amount); + 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; + } - creations[creationId].blessings += 1; - creations[creationId].totalMannaSpent += amount; + 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 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; + } - _mint(msg.sender, creationId, amount, ""); - emit Blessed(creationId, msg.sender, amount); + function getUserPraise(uint256 creationId, address user) external view returns (uint256) { + return creations[creationId].praiseBalance[user]; } - function setMannaToken(address _mannaToken) external onlyOwner notEnded { - mannaToken = _mannaToken; + function allCreationIds() external view returns (uint256[] memory) { + return _allCreationIds; } - function setURI(string memory newuri) external onlyOwner { - _setURI(newuri); + function getPraiseListings() external view returns (PraiseListing[] memory) { + return praiseListings; } -} +} \ No newline at end of file diff --git a/contracts/Manna.sol b/contracts/Manna.sol new file mode 100644 index 0000000..436dab3 --- /dev/null +++ b/contracts/Manna.sol @@ -0,0 +1,54 @@ +// 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/Abraham.sol b/experimental/Abraham.sol new file mode 100644 index 0000000..b313e43 --- /dev/null +++ b/experimental/Abraham.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +interface IMannaToken { + function burnFrom(address account, uint256 amount) external; +} + +interface IAbrahamNFT { +function mintCreationNFT(address to, uint256 creationId, string memory tokenUri) external; +} + +contract Abraham is ERC1155, Ownable { + address public mannaToken; + address public abrahamNFT; + uint256 public creationCounter; + uint256 public endTimestamp; + + uint256 public minimumMannaSpend = 1e18; + string public name; + string public symbol; + + struct CreationData { + uint256 praises; + uint256 burns; + uint256 blessings; + uint256 totalMannaSpent; + string image; // New field for image URL + } + + struct UserStats { + uint256 praiseCount; + uint256 praiseMannaSpent; + uint256 burnCount; + uint256 burnMannaSpent; + } + + mapping(uint256 => CreationData) public creations; + mapping(uint256 => mapping(address => UserStats)) public userParticipation; + + event CreationReleased(uint256 indexed creationId, string image); // include image in event + event Praised(uint256 indexed creationId, address indexed user, uint256 amount); + event Burned(uint256 indexed creationId, address indexed user, uint256 amount); + event Blessed(uint256 indexed creationId, address indexed user, uint256 amount); + + constructor(address _mannaToken, string memory uri_, address initialOwner) ERC1155(uri_) Ownable(initialOwner) { + mannaToken = _mannaToken; + endTimestamp = block.timestamp + (13 * 365 days); + name = "Abraham ERC1155 Collection"; + symbol = "ABR1155"; + } + + modifier notEnded() { + require(block.timestamp < endTimestamp, "Contract expired"); + _; + } + + function setMinimumMannaSpend(uint256 newMin) external onlyOwner { + minimumMannaSpend = newMin; + } + + function setAbrahamNFT(address _abrahamNFT) external onlyOwner { + abrahamNFT = _abrahamNFT; + } + + function releaseCreation(string memory image) external onlyOwner notEnded { + creationCounter += 1; + + creations[creationCounter] = CreationData({ + praises: 0, + burns: 0, + blessings: 0, + totalMannaSpent: 0, + image: image + }); + + emit CreationReleased(creationCounter, image); + + require(abrahamNFT != address(0), "AbrahamNFT not set"); + IAbrahamNFT(abrahamNFT).mintCreationNFT(owner(), creationCounter, image); + } + + function _spendManna(uint256 amount) internal { + require(amount >= minimumMannaSpend, "Spend more Manna"); + IMannaToken(mannaToken).burnFrom(msg.sender, amount); + } + + + function praise(uint256 creationId, uint256 amount) external notEnded { + require(creationId > 0 && creationId <= creationCounter, "Invalid creation"); + _spendManna(amount); + + creations[creationId].praises += 1; + creations[creationId].totalMannaSpent += amount; + + userParticipation[creationId][msg.sender].praiseCount += 1; + userParticipation[creationId][msg.sender].praiseMannaSpent += amount; + + _mint(msg.sender, creationId, amount, ""); + emit Praised(creationId, msg.sender, amount); + } + + function burnCreation(uint256 creationId, uint256 amount) external notEnded { + require(creationId > 0 && creationId <= creationCounter, "Invalid creation"); + _spendManna(amount); + + creations[creationId].burns += 1; + creations[creationId].totalMannaSpent += amount; + + userParticipation[creationId][msg.sender].burnCount += 1; + userParticipation[creationId][msg.sender].burnMannaSpent += amount; + + _mint(msg.sender, creationId, amount, ""); + emit Burned(creationId, msg.sender, amount); + } + + function bless(uint256 creationId, uint256 amount) external notEnded { + require(creationId > 0 && creationId <= creationCounter, "Invalid creation"); + _spendManna(amount); + + creations[creationId].blessings += 1; + creations[creationId].totalMannaSpent += amount; + + _mint(msg.sender, creationId, amount, ""); + emit Blessed(creationId, msg.sender, amount); + } + + function setMannaToken(address _mannaToken) external onlyOwner notEnded { + mannaToken = _mannaToken; + } + + function setURI(string memory newuri) external onlyOwner { + _setURI(newuri); + } +} diff --git a/experimental/Abraham2.sol b/experimental/Abraham2.sol new file mode 100644 index 0000000..467bc0e --- /dev/null +++ b/experimental/Abraham2.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +contract Abraham is ERC1155, Ownable { + + enum ActionType { Praise, Burn, Bless } + + struct CreationData { + string image; + uint256 totalStaked; + uint256 conviction; + uint256 lastConvictionUpdate; + } + + struct StakeInfo { + uint256 amount; + ActionType action; + uint256 stakeTime; + } + + address public mannaToken; + uint256 public creationCounter; + uint256 public endTimestamp; + + uint256 public nextCreationReleaseTime; + + // Human-readable name/symbol + string public name; + string public symbol; + + // ID -> Creation data + mapping(uint256 => CreationData) public creations; + + // creationID -> (user -> StakeInfo) + mapping(uint256 => mapping(address => StakeInfo)) public userStakes; + + event CreationReleased(uint256 indexed creationId, string image, uint256 timestamp); + event Staked(uint256 indexed creationId, address indexed user, ActionType action, uint256 amount); + event Unstaked(uint256 indexed creationId, address indexed user, uint256 amount); + event MintedTopCreation(uint256 indexed creationId, uint256 conviction); + + constructor( + address _mannaToken, + string memory baseURI, + address initialOwner + ) + ERC1155(baseURI) + Ownable(initialOwner) + { + mannaToken = _mannaToken; + endTimestamp = block.timestamp + (13 * 365 days); + name = "Abraham ERC1155 Collection"; + symbol = "ABR1155"; + nextCreationReleaseTime = block.timestamp; + } + + modifier notEnded() { + require(block.timestamp < endTimestamp, "Contract expired"); + _; + } + + + function releaseCreation(string memory image) external onlyOwner notEnded { + require( + block.timestamp >= nextCreationReleaseTime, + "Must wait until the next hourly slot" + ); + + creationCounter++; + creations[creationCounter] = CreationData({ + image: image, + totalStaked: 0, + conviction: 0, + lastConvictionUpdate: block.timestamp + }); + + // Next release in 1 hour + nextCreationReleaseTime = block.timestamp + 1 hours; + + emit CreationReleased(creationCounter, image, block.timestamp); + } + + + function stakeForCreation( + uint256 creationId, + uint256 amount, + ActionType action + ) + external + notEnded + { + require(creationId > 0 && creationId <= creationCounter, "Invalid creationId"); + require(amount > 0, "Staking amount must be > 0"); + + // 1. Update the creation’s conviction so we keep time-based data accurate + _updateConviction(creationId); + + // 2. Check bonding curve cost + uint256 cost = _bondingCurveCost(creations[creationId].totalStaked, amount); + require(amount >= cost, "Not enough Manna to meet bonding curve cost"); + + // 3. Transfer Manna from user to this contract (staking) + IERC20 manna = IERC20(mannaToken); + require( + manna.allowance(msg.sender, address(this)) >= amount, + "Must approve Abraham to spend Manna" + ); + bool success = manna.transferFrom(msg.sender, address(this), amount); + require(success, "Manna transfer failed"); + + // 4. Update totalStaked + creations[creationId].totalStaked += amount; + + // 5. Update user’s stake info + userStakes[creationId][msg.sender].amount += amount; + userStakes[creationId][msg.sender].action = action; // Overwrites old action + userStakes[creationId][msg.sender].stakeTime = block.timestamp; + + // 6. Mint an ERC1155 “stake receipt” token to represent the staked amount + _mint(msg.sender, creationId, amount, ""); + + emit Staked(creationId, msg.sender, action, amount); + } + + /** + * @notice Unstake some or all of your Manna from a creation. + * @param creationId The ID of the creation. + * @param amount How much Manna to unstake. + */ + function unstake(uint256 creationId, uint256 amount) external notEnded { + require(creationId > 0 && creationId <= creationCounter, "Invalid creationId"); + require(amount > 0, "Cannot unstake zero"); + + StakeInfo storage sInfo = userStakes[creationId][msg.sender]; + require(sInfo.amount >= amount, "Not enough staked Manna"); + + // 1. Update conviction first + _updateConviction(creationId); + + // 2. Decrease user’s stake & creation total + sInfo.amount -= amount; + creations[creationId].totalStaked -= amount; + + // 3. Burn the stake-receipt tokens + _burn(msg.sender, creationId, amount); + + // 4. Transfer Manna back to user + IERC20 manna = IERC20(mannaToken); + bool success = manna.transfer(msg.sender, amount); + require(success, "Manna transfer to user failed"); + + emit Unstaked(creationId, msg.sender, amount); + } + + + function mintTopCreation() external onlyOwner { + require(creationCounter > 0, "No creations available"); + + // 1. Find the creation with the highest conviction + uint256 topId; + uint256 maxConviction; + for (uint256 i = 1; i <= creationCounter; i++) { + _updateConviction(i); + if (creations[i].conviction > maxConviction) { + maxConviction = creations[i].conviction; + topId = i; + } + } + require(topId > 0, "No valid creation found"); + + uint256 rewardTokenId = topId + 1_000_000; + _mint(owner(), rewardTokenId, 1, ""); // Example: 1 token to the owner + + emit MintedTopCreation(topId, maxConviction); + } + + + function setMannaToken(address _mannaToken) external onlyOwner notEnded { + mannaToken = _mannaToken; + } + + + function setURI(string memory newuri) external onlyOwner { + _setURI(newuri); + } + + function _bondingCurveCost(uint256 currentStaked, uint256 stakeAmount) + internal + pure + returns (uint256 cost) + { + cost = (currentStaked / 100) + (stakeAmount / 2); + } + + + function _updateConviction(uint256 creationId) internal { + CreationData storage cd = creations[creationId]; + uint256 timeDelta = block.timestamp - cd.lastConvictionUpdate; + if (timeDelta == 0) { + return; // no new time has passed + } + + // Naive formula: conviction += totalStaked * timeDelta + cd.conviction += cd.totalStaked * timeDelta; + cd.lastConvictionUpdate = block.timestamp; + } +} \ No newline at end of file diff --git a/experimental/AbrahamMarket.sol b/experimental/AbrahamMarket.sol new file mode 100644 index 0000000..c9c56f6 --- /dev/null +++ b/experimental/AbrahamMarket.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +/** + * @title IMannaToken + * @dev Interface for the MannaToken ERC20 contract. + */ +interface IMannaToken { + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); +} +contract AbrahamMarket is ERC1155, Ownable, ReentrancyGuard { + using Strings for uint256; + + // Token name and symbol (optional for ERC1155) + string public name; + string public symbol; + + // Reference to the MannaToken ERC20 contract + IMannaToken public mannaToken; + + // Counter for new creations + uint256 public creationCounter; + + // Duration for each cycle (e.g., 1 week) + uint256 public cycleDuration; + + // Timestamp of the last cycle + uint256 public lastCycleTime; + + // Struct to store creation data + struct Creation { + uint256 id; + string imageURL; + uint256 totalStaked; + bool mintedAsTop; + uint256 createdAt; + } + + // Mapping from creation ID to Creation data + mapping(uint256 => Creation) public creations; + + // Struct to store user stake information + struct StakeInfo { + uint256 amount; // Amount of Manna staked + uint256 stakeTime; // Timestamp when the stake was made + } + + // Mapping from creation ID to user address to StakeInfo + mapping(uint256 => mapping(address => StakeInfo)) public userStakes; + + // Events + event CreationReleased(uint256 indexed creationId, string imageURL); + event Praised(uint256 indexed creationId, address indexed user, uint256 amount); + event Burned(uint256 indexed creationId, address indexed user, uint256 amount); + event Blessed(uint256 indexed creationId, address indexed user, uint256 amount); + event TopCreationSelected(uint256 indexed creationId); + event BidPlaced(uint256 indexed creationId, address indexed bidder, uint256 amount); + event AuctionFinalized(uint256 indexed creationId, address winner, uint256 amount); + + // Bidding related variables + struct Auction { + bool active; + address highestBidder; + uint256 highestBid; + uint256 biddingEndTime; + } + + // Mapping from creation ID to Auction + mapping(uint256 => Auction) public auctions; + + constructor(address _mannaToken, string memory baseURI, uint256 _cycleDuration) ERC1155(baseURI) { + name = "Abraham Collection"; + symbol = "ABRAHAM-1155"; + mannaToken = IMannaToken(_mannaToken); + cycleDuration = _cycleDuration; + lastCycleTime = block.timestamp; + } + + + modifier checkCycle() { + if (block.timestamp >= lastCycleTime + cycleDuration) { + _selectTopCreation(); + lastCycleTime = block.timestamp; + } + _; + } + + function setBaseURI(string memory newURI) external onlyOwner { + _setURI(newURI); + } + + function releaseCreation(string memory imageURL) external onlyOwner checkCycle { + creationCounter += 1; + creations[creationCounter] = Creation({ + id: creationCounter, + imageURL: imageURL, + totalStaked: 0, + mintedAsTop: false, + createdAt: block.timestamp + }); + + emit CreationReleased(creationCounter, imageURL); + } + + function praise(uint256 creationId, uint256 amount) external nonReentrant checkCycle { + require(creationId > 0 && creationId <= creationCounter, "Invalid creationId"); + require(amount > 0, "Amount must be > 0"); + + // Calculate cost using bonding curve + uint256 cost = getBondingCost(creationId, amount); + require(amount >= cost, "Insufficient Manna for bonding curve"); + + // Transfer Manna from user to contract + bool success = mannaToken.transferFrom(msg.sender, address(this), amount); + require(success, "Manna transfer failed"); + + // Update total staked for the creation + creations[creationId].totalStaked += amount; + + // Update user's stake info + StakeInfo storage stake = userStakes[creationId][msg.sender]; + stake.amount += amount; + stake.stakeTime = block.timestamp; + + // Mint ERC1155 tokens to the user + _mint(msg.sender, creationId, amount, ""); + + emit Praised(creationId, msg.sender, amount); + } + + + function burnCreation(uint256 creationId, uint256 amount) external nonReentrant checkCycle { + require(creationId > 0 && creationId <= creationCounter, "Invalid creationId"); + require(amount > 0, "Amount must be > 0"); + + // Calculate cost using bonding curve + uint256 cost = getBondingCost(creationId, amount); + require(amount >= cost, "Insufficient Manna for bonding curve"); + + // Transfer Manna from user to contract + bool success = mannaToken.transferFrom(msg.sender, address(this), amount); + require(success, "Manna transfer failed"); + + // Update total staked for the creation + creations[creationId].totalStaked += amount; + + // Update user's stake info + StakeInfo storage stake = userStakes[creationId][msg.sender]; + stake.amount += amount; + stake.stakeTime = block.timestamp; + + // Mint ERC1155 tokens to the user + _mint(msg.sender, creationId, amount, ""); + + emit Burned(creationId, msg.sender, amount); + } + + + function bless(uint256 creationId, uint256 amount) external nonReentrant checkCycle { + require(creationId > 0 && creationId <= creationCounter, "Invalid creationId"); + require(amount > 0, "Amount must be > 0"); + + // Calculate cost using bonding curve + uint256 cost = getBondingCost(creationId, amount); + require(amount >= cost, "Insufficient Manna for bonding curve"); + + // Transfer Manna from user to contract + bool success = mannaToken.transferFrom(msg.sender, address(this), amount); + require(success, "Manna transfer failed"); + + // Update total staked for the creation + creations[creationId].totalStaked += amount; + + // Update user's stake info + StakeInfo storage stake = userStakes[creationId][msg.sender]; + stake.amount += amount; + stake.stakeTime = block.timestamp; + + // Mint ERC1155 tokens to the user + _mint(msg.sender, creationId, amount, ""); + + emit Blessed(creationId, msg.sender, amount); + } + + + function getBondingCost(uint256 creationId, uint256 amount) public view returns (uint256 cost) { + // Simple linear bonding curve: cost increases by 1% per total staked + // More sophisticated curves can be implemented + Creation storage c = creations[creationId]; + cost = (amount * (c.totalStaked + amount)) / 100; // 1% of (totalStaked + amount) + } + + + function _selectTopCreation() internal { + uint256 topId = 0; + uint256 highestConviction = 0; + + for (uint256 i = 1; i <= creationCounter; i++) { + if (creations[i].mintedAsTop) { + continue; // Skip already minted top creations + } + uint256 conviction = getConviction(i); + if (conviction > highestConviction) { + highestConviction = conviction; + topId = i; + } + } + + require(topId != 0, "No valid top creation found"); + + creations[topId].mintedAsTop = true; + emit TopCreationSelected(topId); + + // Start auction for the top creation + _startAuction(topId); + } + + function getConviction(uint256 creationId) public view returns (uint256 conviction) { + Creation storage c = creations[creationId]; + // Conviction = totalStaked * (currentTime - createdAt) + uint256 timeStaked = block.timestamp - c.createdAt; + conviction = c.totalStaked * timeStaked; + } + + function _startAuction(uint256 creationId) internal { + require(!auctions[creationId].active, "Auction already active for this creation"); + + auctions[creationId] = Auction({ + active: true, + highestBidder: address(0), + highestBid: 0, + biddingEndTime: block.timestamp + 7 days // Auction lasts for 7 days + }); + } + + function placeBid(uint256 creationId, uint256 bidAmount) external nonReentrant { + Auction storage auction = auctions[creationId]; + require(auction.active, "No active auction for this creation"); + require(block.timestamp < auction.biddingEndTime, "Auction has ended"); + require(bidAmount > auction.highestBid, "Bid must be higher than current highest bid"); + + // Transfer Manna from bidder to contract + bool success = mannaToken.transferFrom(msg.sender, address(this), bidAmount); + require(success, "Manna transfer failed"); + + // Refund the previous highest bidder + if (auction.highestBidder != address(0)) { + bool refundSuccess = mannaToken.transfer(auction.highestBidder, auction.highestBid); + require(refundSuccess, "Refund to previous bidder failed"); + } + + // Update highest bid + auction.highestBidder = msg.sender; + auction.highestBid = bidAmount; + + emit BidPlaced(creationId, msg.sender, bidAmount); + } + + function finalizeAuction(uint256 creationId) external nonReentrant { + Auction storage auction = auctions[creationId]; + require(auction.active, "No active auction for this creation"); + require(block.timestamp >= auction.biddingEndTime, "Auction is still ongoing"); + require(auction.highestBidder != address(0), "No bids placed"); + + // Mark the auction as inactive + auction.active = false; + + // Transfer the ERC1155 tokens to the highest bidder + // Assuming the contract holds the ERC1155 tokens for this creation + // If not, adjust accordingly + _safeTransferFrom(owner(), auction.highestBidder, creationId, creations[creationId].totalStaked, ""); + + emit AuctionFinalized(creationId, auction.highestBidder, auction.highestBid); + } + + + + + function withdrawManna() external onlyOwner { + uint256 balance = mannaToken.balanceOf(address(this)); + require(balance > 0, "No Manna to withdraw"); + + bool success = mannaToken.transfer(owner(), balance); + require(success, "Manna transfer failed"); + } +} \ No newline at end of file diff --git a/contracts/AbrahamNFT.sol b/experimental/AbrahamNFT.sol similarity index 70% rename from contracts/AbrahamNFT.sol rename to experimental/AbrahamNFT.sol index 617d735..e48d5a4 100644 --- a/contracts/AbrahamNFT.sol +++ b/experimental/AbrahamNFT.sol @@ -1,22 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title AbrahamNFT * @dev One-of-one ERC-721 NFT for each creation, owned by Abraham contract after testing setup. */ -contract AbrahamNFT is ERC721, Ownable { +contract AbrahamNFT is ERC721URIStorage, Ownable { string private _baseTokenURI; constructor(address initialOwner, string memory baseURI) ERC721("Abraham NFT", "ABR") Ownable(initialOwner) { _baseTokenURI = baseURI; } - function mintCreationNFT(address to, uint256 creationId) external onlyOwner { + function mintCreationNFT(address to, uint256 creationId, string memory tokenUri) external onlyOwner { _safeMint(to, creationId); + _setTokenURI(creationId, tokenUri); } function setBaseURI(string memory baseURI) external onlyOwner { diff --git a/contracts/MannaToken.sol b/experimental/MannaToken.sol similarity index 89% rename from contracts/MannaToken.sol rename to experimental/MannaToken.sol index 10260f2..94e4531 100644 --- a/contracts/MannaToken.sol +++ b/experimental/MannaToken.sol @@ -48,9 +48,7 @@ contract MannaToken is ERC20, Ownable { } function burnFrom(address account, uint256 amount) external { - uint256 currentAllowance = allowance(account, msg.sender); - require(currentAllowance >= amount, "ERC20: burn amount exceeds allowance"); - _approve(account, msg.sender, currentAllowance - amount); + require(balanceOf(account) >= amount, "Not enough Manna to create art"); _burn(account, amount); } } diff --git a/scripts/deploy.ts b/scripts/deploy.ts index a715335..349d73a 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -8,100 +8,36 @@ async function main() { throw new Error("CONTRACT_OWNER not set in .env"); } - // Deploy MannaToken - const mannaToken = await deployMannaToken(contractOwner); + // Deploy Manna contract + const manna = await deployManna(contractOwner); - // Deploy AbrahamNFT - const abrahamNFT = await deployAbrahamNFT( - contractOwner, - "https://metadata.example.com/" - ); - - // Deploy Abraham - const abraham = await deployAbraham( - mannaToken, - "https://example.com/{id}.json", - contractOwner - ); - - // Transfer AbrahamNFT ownership to Abraham - await transferOwnershipAbrahamNFT(abrahamNFT, abraham); - - // Set AbrahamNFT in Abraham - await setAbrahamNFTInAbraham(abraham, abrahamNFT); + // Deploy Abraham contract + const abraham = await deployAbraham(manna, contractOwner); console.log("Deployment complete."); - console.log(`MannaToken: ${await mannaToken.getAddress()}`); - console.log(`AbrahamNFT: ${await abrahamNFT.getAddress()}`); + console.log(`Manna: ${await manna.getAddress()}`); console.log(`Abraham: ${await abraham.getAddress()}`); } -async function deployMannaToken(initialOwner: string) { - const MannaToken = await hre.ethers.getContractFactory("MannaToken"); - const mannaToken = await MannaToken.deploy(initialOwner); - await mannaToken.waitForDeployment(); - console.log(`MannaToken deployed at: ${await mannaToken.getAddress()}`); - return mannaToken; -} - -async function deployAbrahamNFT(initialOwner: string, baseURI: string) { - const AbrahamNFT = await hre.ethers.getContractFactory("AbrahamNFT"); - const abrahamNFT = await AbrahamNFT.deploy(initialOwner, baseURI); - await abrahamNFT.waitForDeployment(); - console.log(`AbrahamNFT deployed at: ${await abrahamNFT.getAddress()}`); - return abrahamNFT; +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; } async function deployAbraham( - mannaToken: { getAddress: () => any }, - uri: string, + manna: { getAddress: () => any }, initialOwner: string ) { - const mannaAddress = await mannaToken.getAddress(); + const mannaAddress = await manna.getAddress(); const Abraham = await hre.ethers.getContractFactory("Abraham"); - const abraham = await Abraham.deploy(mannaAddress, uri, initialOwner); - await abraham.waitForDeployment(); + const abraham = await Abraham.deploy(mannaAddress, initialOwner); console.log(`Abraham deployed at: ${await abraham.getAddress()}`); return abraham; } -async function transferOwnershipAbrahamNFT( - abrahamNFT: { - getAddress: () => any; - owner: () => any; - transferOwnership: (arg0: any) => any; - }, - abraham: { getAddress: () => any } -) { - const abrahamNFTAddress = await abrahamNFT.getAddress(); - const abrahamAddress = await abraham.getAddress(); - - // We need the signer who currently owns AbrahamNFT (the deployer) to call transferOwnership - const [deployer] = await hre.ethers.getSigners(); - const nftOwner = await abrahamNFT.owner(); - if (nftOwner.toLowerCase() !== deployer.address.toLowerCase()) { - throw new Error( - "Deployer does not own AbrahamNFT. Check initialOwner param." - ); - } - - const tx = await abrahamNFT.transferOwnership(abrahamAddress); - await tx.wait(); - console.log( - `AbrahamNFT ownership transferred to Abraham at ${abrahamAddress}` - ); -} - -async function setAbrahamNFTInAbraham( - abraham: { setAbrahamNFT: (arg0: any) => any }, - abrahamNFT: { getAddress: () => any } -) { - const abrahamNFTAddress = await abrahamNFT.getAddress(); - const tx = await abraham.setAbrahamNFT(abrahamNFTAddress); - await tx.wait(); - console.log(`AbrahamNFT set in Abraham: ${abrahamNFTAddress}`); -} - +// Execute the deployment script main().catch((error) => { console.error(error); process.exitCode = 1; diff --git a/test/Abraham.ts b/test/Abraham.ts index 09d8e5e..67a4994 100644 --- a/test/Abraham.ts +++ b/test/Abraham.ts @@ -1,141 +1,143 @@ import { expect } from "chai"; const hre = require("hardhat"); -import { MannaToken, Abraham, AbrahamNFT } from "../typechain-types"; -describe("Abraham", function () { - let mannaToken: MannaToken; - let abraham: Abraham; - let abrahamNFT: AbrahamNFT; - let owner: any, addr1: any, addr2: any; +describe("Abraham and Manna Contracts", function () { + let mannaContract: any; + let abrahamContract: any; + let owner: any; + let user1: any; + let user2: any; - beforeEach(async function () { - [owner, addr1, addr2] = await hre.ethers.getSigners(); + before(async () => { + [owner, user1, user2] = await hre.ethers.getSigners(); - // Deploy MannaToken - const MannaToken = await hre.ethers.getContractFactory("MannaToken"); - mannaToken = (await MannaToken.deploy(owner.address)) as MannaToken; + // Deploy Manna contract + const Manna = await hre.ethers.getContractFactory("Manna"); + mannaContract = await Manna.deploy(owner.address); - // Deploy AbrahamNFT - const AbrahamNFT = await hre.ethers.getContractFactory("AbrahamNFT"); - abrahamNFT = (await AbrahamNFT.deploy( - owner.address, - "https://metadata.example.com/" - )) as AbrahamNFT; - - // Deploy Abraham + // Deploy Abraham contract const Abraham = await hre.ethers.getContractFactory("Abraham"); - abraham = (await Abraham.deploy( - mannaToken.getAddress(), - "https://example.com/{id}.json", + abrahamContract = await Abraham.deploy( + await mannaContract.getAddress(), owner.address - )) as Abraham; - - // Transfer ownership of AbrahamNFT to Abraham so Abraham can mint - await abrahamNFT.connect(owner).transferOwnership(abraham.getAddress()); - await abraham.connect(owner).setAbrahamNFT(abrahamNFT.getAddress()); - - // Now release a creation - await abraham - .connect(owner) - .releaseCreation("https://example.com/creation2.png"); - }); - - it("Should have a released creation", async function () { - const counter = await abraham.creationCounter(); - expect(counter).to.equal(1); - }); + ); - it("Should have minted an NFT for the creation in AbrahamNFT", async function () { - const ownerOfToken = await abrahamNFT.ownerOf(1); - expect(ownerOfToken).to.equal(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 store the image URL for the creation", async function () { - await abraham - .connect(owner) - .releaseCreation("https://example.com/creation2.png"); - const creation = await abraham.creations(2); - expect(creation.image).to.equal("https://example.com/creation2.png"); + 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") + ); }); - it("Should allow praising after buying and approving Manna", async function () { - const buyAmount = hre.ethers.parseEther("0.0001"); // Buy 1 Manna - await mannaToken.connect(addr1).buyManna({ value: buyAmount }); - const oneManna = hre.ethers.parseUnits("1", 18); - - await mannaToken.connect(addr1).approve(abraham.getAddress(), oneManna); - await abraham.connect(addr1).praise(1, oneManna); + 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); - const balance1155 = await abraham.balanceOf(addr1.address, 1); - expect(balance1155).to.equal(oneManna); - - const userStats = await abraham.userParticipation(1, addr1.address); - expect(userStats.praiseCount).to.equal(1); - expect(userStats.praiseMannaSpent).to.equal(oneManna); - - const creation = await abraham.creations(1); - expect(creation.praises).to.equal(1); - expect(creation.totalMannaSpent).to.equal(oneManna); + const creation = await abrahamContract.getCreation(1); + expect(creation.uri).to.equal(metadataUri); + expect(creation.totalStaked).to.equal(0); + expect(creation.praisePool).to.equal(0); }); - it("Should allow burning creation similarly", async function () { - const buyAmount = hre.ethers.parseEther("0.0002"); // Buy 2 Manna - await mannaToken.connect(addr2).buyManna({ value: buyAmount }); - const twoManna = hre.ethers.parseUnits("2", 18); - - await mannaToken.connect(addr2).approve(abraham.getAddress(), twoManna); - await abraham.connect(addr2).burnCreation(1, twoManna); - - const balance1155 = await abraham.balanceOf(addr2.address, 1); - expect(balance1155).to.equal(twoManna); - - const userStats = await abraham.userParticipation(1, addr2.address); - expect(userStats.burnCount).to.equal(1); - expect(userStats.burnMannaSpent).to.equal(twoManna); - - const creation = await abraham.creations(1); - expect(creation.burns).to.equal(1); - expect(creation.totalMannaSpent).to.equal(twoManna); + 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 blessing creation", async function () { - const buyAmount = hre.ethers.parseEther("0.0001"); - await mannaToken.connect(addr1).buyManna({ value: buyAmount }); - const oneManna = hre.ethers.parseUnits("1", 18); + it("Should allow users to unpraise a creation", async () => { + const creationId = 1; + const unpraisePrice = await abrahamContract.initUnpraisePrice(); - await mannaToken.connect(addr1).approve(abraham.getAddress(), oneManna); - await abraham.connect(addr1).bless(1, oneManna); + await expect(abrahamContract.connect(user1).unpraise(creationId)) + .to.emit(abrahamContract, "Unpraised") + .withArgs(creationId, user1.address, 1, unpraisePrice); - const balance1155 = await abraham.balanceOf(addr1.address, 1); - expect(balance1155).to.equal(oneManna); + const creation = await abrahamContract.getCreation(creationId); + expect(creation.totalStaked).to.equal(0); + expect(creation.praisePool).to.equal(0); - const creation = await abraham.creations(1); - expect(creation.blessings).to.equal(1); - expect(creation.totalMannaSpent).to.equal(oneManna); + const userPraise = await abrahamContract.getUserPraise( + creationId, + user1.address + ); + expect(userPraise).to.equal(0); }); - it("Should revert if spending less than minimumMannaSpend", async function () { - const halfManna = hre.ethers.parseUnits("0.5", 18); // 0.5 Manna - await mannaToken.transfer(addr1.address, halfManna); + 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); - await mannaToken.connect(addr1).approve(abraham.getAddress(), halfManna); + // List praise for sale await expect( - abraham.connect(addr1).praise(1, halfManna) - ).to.be.revertedWith("Spend more Manna"); + 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); }); - it("Should fail after 13 years", async function () { - await hre.ethers.provider.send("evm_increaseTime", [13 * 365 * 24 * 3600]); - await hre.ethers.provider.send("evm_mine", []); - - const oneManna = hre.ethers.parseUnits("1", 18); - await mannaToken - .connect(addr1) - .buyManna({ value: hre.ethers.parseEther("0.0001") }); - await mannaToken.connect(addr1).approve(abraham.getAddress(), oneManna); - - await expect(abraham.connect(addr1).praise(1, oneManna)).to.be.revertedWith( - "Contract expired" + 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 + ); + + const user2Praise = await abrahamContract.getUserPraise( + listing[listingId].creationId, + user2.address ); + expect(user2Praise).to.equal(1); + + const updatedListings = await abrahamContract.getPraiseListings(); + expect(updatedListings[listingId].amount).to.equal(0); // Listing completed }); }); diff --git a/test/AbrahamNFT.ts b/test/AbrahamNFT.ts deleted file mode 100644 index 0519ef0..0000000 --- a/test/AbrahamNFT.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect } from "chai"; -const hre = require("hardhat"); -import { AbrahamNFT } from "../typechain-types"; - -describe("AbrahamNFT", function () { - let abrahamNFT: AbrahamNFT; - let owner: any, addr1: any; - - beforeEach(async function () { - [owner, addr1] = await hre.ethers.getSigners(); - const AbrahamNFT = await hre.ethers.getContractFactory("AbrahamNFT"); - abrahamNFT = (await AbrahamNFT.deploy( - owner.address, - "https://metadata.example.com/" - )) as AbrahamNFT; - }); - - it("Should mint a new NFT to the specified address", async function () { - await abrahamNFT.connect(owner).mintCreationNFT(addr1.address, 1); - const ownerOfToken = await abrahamNFT.ownerOf(1); - expect(ownerOfToken).to.equal(addr1.address); - }); - - it("Should allow changing the base URI", async function () { - await abrahamNFT - .connect(owner) - .setBaseURI("https://newmetadata.example.com/"); - await abrahamNFT.connect(owner).mintCreationNFT(owner.address, 2); - const tokenURI = await abrahamNFT.tokenURI(2); - expect(tokenURI).to.include("https://newmetadata.example.com/"); - }); - - it("Should revert if non-owner tries to mint", async function () { - await expect( - abrahamNFT.connect(addr1).mintCreationNFT(addr1.address, 2) - ).to.be.revertedWithCustomError(abrahamNFT, "OwnableUnauthorizedAccount"); - }); -}); diff --git a/test/Manna.ts b/test/Manna.ts index 4e5a9de..86b1882 100644 --- a/test/Manna.ts +++ b/test/Manna.ts @@ -1,20 +1,20 @@ import { expect } from "chai"; const hre = require("hardhat"); -import { MannaToken } from "../typechain-types"; +import { Manna } from "../typechain-types"; -describe("MannaToken", function () { - let mannaToken: MannaToken; +describe("Manna", function () { + let manna: Manna; let owner: any, addr1: any, addr2: any; beforeEach(async function () { - const MannaToken = await hre.ethers.getContractFactory("MannaToken"); + const Manna = await hre.ethers.getContractFactory("Manna"); [owner, addr1, addr2] = await hre.ethers.getSigners(); - mannaToken = (await MannaToken.deploy(owner.address)) as MannaToken; + manna = (await Manna.deploy(owner.address)) as Manna; }); it("Should assign half of initial supply to the owner", async function () { - const ownerBalance = await mannaToken.balanceOf(owner.address); - const total = await mannaToken.totalSupply(); + 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); @@ -22,8 +22,8 @@ describe("MannaToken", function () { it("Should allow buying Manna with Ether", async function () { const buyAmount = hre.ethers.parseEther("0.0001"); // 1 Manna - await mannaToken.connect(addr1).buyManna({ value: buyAmount }); - const mannaBalance = await mannaToken.balanceOf(addr1.address); + 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); }); @@ -31,31 +31,31 @@ describe("MannaToken", function () { 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(mannaToken.connect(addr1).buyManna({ value: buyAmount })) - .to.emit(mannaToken, "BoughtManna") + 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( - mannaToken.connect(addr1).buyManna({ value: tooLittle }) + 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 mannaToken.connect(addr1).buyManna({ value: buyAmount }); + await manna.connect(addr1).buyManna({ value: buyAmount }); // Now addr1 has some Manna - const mannaBalance = await mannaToken.balanceOf(addr1.address); + const mannaBalance = await manna.balanceOf(addr1.address); // Sell it back const initialEthBalance = await hre.ethers.provider.getBalance( addr1.address ); - await mannaToken.connect(addr1).sellManna(mannaBalance); + await manna.connect(addr1).sellManna(mannaBalance); const finalEthBalance = await hre.ethers.provider.getBalance(addr1.address); expect(finalEthBalance).to.be.gt(initialEthBalance); @@ -63,24 +63,24 @@ describe("MannaToken", function () { it("Should emit SoldManna event when selling Manna", async function () { const buyAmount = hre.ethers.parseEther("0.0001"); - await mannaToken.connect(addr1).buyManna({ value: buyAmount }); + await manna.connect(addr1).buyManna({ value: buyAmount }); - const mannaBalance = await mannaToken.balanceOf(addr1.address); - await expect(mannaToken.connect(addr1).sellManna(mannaBalance)) - .to.emit(mannaToken, "SoldManna") + 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(mannaToken.connect(addr1).sellManna(1)).to.be.revertedWith( + 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 mannaToken.connect(addr1).buyManna({ value: buyAmount }); - const [mannaBalance, ethBalance] = await mannaToken.getContractBalances(); + await manna.connect(addr1).buyManna({ value: buyAmount }); + const [mannaBalance, ethBalance] = await manna.getContractBalances(); expect(mannaBalance).to.equal(0); expect(ethBalance).to.equal(buyAmount); });