diff --git a/contracts/Abraham.sol b/contracts/Abraham.sol index ea72589..658a9e1 100644 --- a/contracts/Abraham.sol +++ b/contracts/Abraham.sol @@ -5,12 +5,16 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; interface IMannaToken { - function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); - // no direct call to transfer to zero, we use burnFrom now + 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; @@ -21,6 +25,7 @@ contract Abraham is ERC1155, Ownable { uint256 burns; uint256 blessings; uint256 totalMannaSpent; + string image; // New field for image URL } struct UserStats { @@ -33,12 +38,12 @@ contract Abraham is ERC1155, Ownable { mapping(uint256 => CreationData) public creations; mapping(uint256 => mapping(address => UserStats)) public userParticipation; - event CreationReleased(uint256 indexed creationId); + 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_) ERC1155(uri_) Ownable(msg.sender) { + constructor(address _mannaToken, string memory uri_, address initialOwner) ERC1155(uri_) Ownable(initialOwner) { mannaToken = _mannaToken; endTimestamp = block.timestamp + (13 * 365 days); } @@ -52,15 +57,29 @@ contract Abraham is ERC1155, Ownable { minimumMannaSpend = newMin; } - function releaseCreation() external onlyOwner notEnded { + function setAbrahamNFT(address _abrahamNFT) external onlyOwner { + abrahamNFT = _abrahamNFT; + } + + function releaseCreation(string memory image) external onlyOwner notEnded { creationCounter += 1; - emit CreationReleased(creationCounter); + + 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); } function _spendManna(uint256 amount) internal { require(amount >= minimumMannaSpend, "Spend more Manna"); - // Instead of transferFrom to zero, we call burnFrom on Manna - // User must have approved Abraham contract in MannaToken for at least `amount`. (bool success, ) = mannaToken.call( abi.encodeWithSignature("burnFrom(address,uint256)", msg.sender, amount) ); diff --git a/contracts/AbrahamNFT.sol b/contracts/AbrahamNFT.sol new file mode 100644 index 0000000..617d735 --- /dev/null +++ b/contracts/AbrahamNFT.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC721/ERC721.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 { + 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 { + _safeMint(to, creationId); + } + + function setBaseURI(string memory baseURI) external onlyOwner { + _baseTokenURI = baseURI; + } + + function _baseURI() internal view override returns (string memory) { + return _baseTokenURI; + } +} diff --git a/contracts/MannaToken.sol b/contracts/MannaToken.sol index c9d6775..10260f2 100644 --- a/contracts/MannaToken.sol +++ b/contracts/MannaToken.sol @@ -14,7 +14,6 @@ contract MannaToken is ERC20, Ownable { constructor(address initialOwner) ERC20("Manna", "MANNA") Ownable(initialOwner) { uint256 initialOwnerSupply = INITIAL_SUPPLY / 2; _mint(initialOwner, initialOwnerSupply); - // The rest can be minted via buyManna until cap is reached } function buyManna() external payable { @@ -48,14 +47,9 @@ contract MannaToken is ERC20, Ownable { this.buyManna{value: msg.value}(); } - /** - * @dev Allow another contract (like Abraham) to burn tokens on behalf of a user - * The user must have approved at least `amount` to `msg.sender` (the Abraham contract). - */ function burnFrom(address account, uint256 amount) external { uint256 currentAllowance = allowance(account, msg.sender); require(currentAllowance >= amount, "ERC20: burn amount exceeds allowance"); - // Decrease allowance before burning, to prevent double spending _approve(account, msg.sender, currentAllowance - amount); _burn(account, amount); } diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 9e2be91..a715335 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -1,25 +1,107 @@ const hre = require("hardhat"); require("dotenv").config(); + const contractOwner = process.env.CONTRACT_OWNER; async function main() { - // Deploy the MannaToken contract - await deployMannaTokenContract(); -} + if (!contractOwner) { + throw new Error("CONTRACT_OWNER not set in .env"); + } + + // Deploy MannaToken + const mannaToken = await deployMannaToken(contractOwner); + + // Deploy AbrahamNFT + const abrahamNFT = await deployAbrahamNFT( + contractOwner, + "https://metadata.example.com/" + ); -async function deployMannaTokenContract() { - const initialOwner = contractOwner; + // 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); + + console.log("Deployment complete."); + console.log(`MannaToken: ${await mannaToken.getAddress()}`); + console.log(`AbrahamNFT: ${await abrahamNFT.getAddress()}`); + console.log(`Abraham: ${await abraham.getAddress()}`); +} - // Deploy the MannaToken contract +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 deployAbraham( + mannaToken: { getAddress: () => any }, + uri: string, + initialOwner: string +) { + const mannaAddress = await mannaToken.getAddress(); + const Abraham = await hre.ethers.getContractFactory("Abraham"); + const abraham = await Abraham.deploy(mannaAddress, uri, initialOwner); + await abraham.waitForDeployment(); + 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( - `MannaToken contract deployed to: ${await mannaToken.getAddress()}` + `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}`); +} + main().catch((error) => { console.error(error); process.exitCode = 1; diff --git a/test/Abraham.ts b/test/Abraham.ts index 4c7a48d..09d8e5e 100644 --- a/test/Abraham.ts +++ b/test/Abraham.ts @@ -1,10 +1,11 @@ import { expect } from "chai"; const hre = require("hardhat"); -import { MannaToken, Abraham } from "../typechain-types"; +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; beforeEach(async function () { @@ -14,15 +15,29 @@ describe("Abraham", function () { const MannaToken = await hre.ethers.getContractFactory("MannaToken"); mannaToken = (await MannaToken.deploy(owner.address)) as MannaToken; + // Deploy AbrahamNFT + const AbrahamNFT = await hre.ethers.getContractFactory("AbrahamNFT"); + abrahamNFT = (await AbrahamNFT.deploy( + owner.address, + "https://metadata.example.com/" + )) as AbrahamNFT; + // Deploy Abraham const Abraham = await hre.ethers.getContractFactory("Abraham"); abraham = (await Abraham.deploy( mannaToken.getAddress(), - "https://example.com/{id}.json" + "https://example.com/{id}.json", + owner.address )) as Abraham; - // Owner releases a creation - await abraham.connect(owner).releaseCreation(); + // 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 () { @@ -30,42 +45,45 @@ describe("Abraham", function () { 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); + }); + + 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 allow praising after buying and approving Manna", async function () { - // Buy Manna for addr1 const buyAmount = hre.ethers.parseEther("0.0001"); // Buy 1 Manna await mannaToken.connect(addr1).buyManna({ value: buyAmount }); const oneManna = hre.ethers.parseUnits("1", 18); - // Approve Abraham await mannaToken.connect(addr1).approve(abraham.getAddress(), oneManna); - - // Praise creationId=1 with 1 Manna await abraham.connect(addr1).praise(1, oneManna); - // Check ERC-1155 balance const balance1155 = await abraham.balanceOf(addr1.address, 1); expect(balance1155).to.equal(oneManna); - // Check user stats const userStats = await abraham.userParticipation(1, addr1.address); expect(userStats.praiseCount).to.equal(1); expect(userStats.praiseMannaSpent).to.equal(oneManna); - // Check creation stats const creation = await abraham.creations(1); expect(creation.praises).to.equal(1); expect(creation.totalMannaSpent).to.equal(oneManna); }); it("Should allow burning creation similarly", async function () { - // Buy Manna for addr2 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); - - // Burn creationId=1 with 2 Manna await abraham.connect(addr2).burnCreation(1, twoManna); const balance1155 = await abraham.balanceOf(addr2.address, 1); @@ -81,7 +99,6 @@ describe("Abraham", function () { }); it("Should allow blessing creation", async function () { - // Buy Manna for addr1 const buyAmount = hre.ethers.parseEther("0.0001"); await mannaToken.connect(addr1).buyManna({ value: buyAmount }); const oneManna = hre.ethers.parseUnits("1", 18); @@ -98,17 +115,10 @@ describe("Abraham", function () { }); it("Should revert if spending less than minimumMannaSpend", async function () { - // minimumMannaSpend = 1 Manna (1e18) - // We'll give the user only 0.5 Manna from the owner to ensure they have less than required. const halfManna = hre.ethers.parseUnits("0.5", 18); // 0.5 Manna - - // Owner has half the initial supply, well above 0.5 Manna. await mannaToken.transfer(addr1.address, halfManna); - // Now addr1 has only 0.5 Manna await mannaToken.connect(addr1).approve(abraham.getAddress(), halfManna); - - // Attempt to praise with 0.5 Manna, should fail with "Spend more Manna" await expect( abraham.connect(addr1).praise(1, halfManna) ).to.be.revertedWith("Spend more Manna"); diff --git a/test/AbrahamNFT.ts b/test/AbrahamNFT.ts new file mode 100644 index 0000000..0519ef0 --- /dev/null +++ b/test/AbrahamNFT.ts @@ -0,0 +1,38 @@ +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"); + }); +});