Skip to content

Commit

Permalink
feat: add abraham nft
Browse files Browse the repository at this point in the history
  • Loading branch information
bashybaranaba committed Dec 14, 2024
1 parent 2a6ca61 commit 818993b
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 43 deletions.
35 changes: 27 additions & 8 deletions contracts/Abraham.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,6 +25,7 @@ contract Abraham is ERC1155, Ownable {
uint256 burns;
uint256 blessings;
uint256 totalMannaSpent;
string image; // New field for image URL
}

struct UserStats {
Expand All @@ -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);
}
Expand All @@ -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)
);
Expand Down
29 changes: 29 additions & 0 deletions contracts/AbrahamNFT.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 0 additions & 6 deletions contracts/MannaToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
96 changes: 89 additions & 7 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
54 changes: 32 additions & 22 deletions test/Abraham.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand All @@ -14,58 +15,75 @@ 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 () {
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);
});

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);
Expand All @@ -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);
Expand All @@ -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");
Expand Down
Loading

0 comments on commit 818993b

Please sign in to comment.