Skip to content

Commit

Permalink
feat: add airdrop from fork JosephJamesReynolds@da95cb7
Browse files Browse the repository at this point in the history
  • Loading branch information
decebal committed Nov 17, 2024
1 parent dad3d6b commit 50bae28
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 5 deletions.
105 changes: 102 additions & 3 deletions packages/hardhat/contracts/RentToOwn.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "./security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./security/ReentrancyGuard.sol";

contract RentToOwn is ReentrancyGuard, Ownable {
using Address for address;
using SafeERC20 for IERC20;

struct Agreement {
address borrower;
address lender;
Expand All @@ -18,7 +25,16 @@ contract RentToOwn is ReentrancyGuard, Ownable {
bool isActive;
}

struct AirdropClaim {
address tokenContract;
uint256 amount;
bytes32 merkleRoot;
uint256 expiryTime;
}

mapping(uint256 => Agreement) public agreements;
mapping(bytes32 => AirdropClaim) public airdrops;
mapping(uint256 => mapping(bytes32 => bool)) public claimedAirdrops;
uint256 public agreementCounter;

event AgreementCreated(
Expand All @@ -32,6 +48,20 @@ contract RentToOwn is ReentrancyGuard, Ownable {
event PaymentMade(uint256 agreementId, uint256 amount, uint256 remaining);
event AgreementCompleted(uint256 agreementId, address newOwner);
event AgreementDefaulted(uint256 agreementId);
event AirdropRegistered(
bytes32 indexed airdropId,
address tokenContract,
uint256 amount,
bytes32 merkleRoot
);
event AirdropClaimed(
uint256 indexed agreementId,
address indexed borrower,
uint256 nftId,
address tokenContract,
uint256 amount,
bytes32 indexed airdropId
);

constructor(address initialOwner) Ownable(initialOwner) {}

Expand Down Expand Up @@ -142,7 +172,7 @@ contract RentToOwn is ReentrancyGuard, Ownable {

// Transfer NFT back to lender
IERC721(agreement.nftContract).transferFrom(
agreement.borrower,
address(this),
agreement.lender,
agreement.nftId
);
Expand All @@ -151,9 +181,78 @@ contract RentToOwn is ReentrancyGuard, Ownable {
emit AgreementDefaulted(_agreementId);
}

// View remaining balance
// Register new airdrop
function registerAirdrop(
address tokenContract,
uint256 amount,
bytes32 merkleRoot,
uint256 duration
) external onlyOwner {
bytes32 airdropId = keccak256(abi.encodePacked(
tokenContract,
amount,
merkleRoot,
block.timestamp
));

airdrops[airdropId] = AirdropClaim({
tokenContract: tokenContract,
amount: amount,
merkleRoot: merkleRoot,
expiryTime: block.timestamp + duration
});

emit AirdropRegistered(airdropId, tokenContract, amount, merkleRoot);
}

// Claim airdrop
function claimAirdrop(
uint256 agreementId,
bytes32 airdropId,
bytes32[] calldata merkleProof
) external nonReentrant {
Agreement storage agreement = agreements[agreementId];
AirdropClaim storage airdrop = airdrops[airdropId];

require(agreement.isActive, "Agreement not active");
require(agreement.borrower == msg.sender, "Not the borrower");
require(!claimedAirdrops[agreementId][airdropId], "Airdrop already claimed");
require(block.timestamp <= airdrop.expiryTime, "Airdrop expired");

// Verify merkle proof
bytes32 leaf = keccak256(abi.encodePacked(agreementId, msg.sender));
require(
MerkleProof.verify(merkleProof, airdrop.merkleRoot, leaf),
"Invalid merkle proof"
);

// Mark as claimed
claimedAirdrops[agreementId][airdropId] = true;

// Transfer tokens
IERC20(airdrop.tokenContract).safeTransfer(msg.sender, airdrop.amount);

emit AirdropClaimed(
agreementId,
msg.sender,
agreement.nftId,
airdrop.tokenContract,
airdrop.amount,
airdropId
);
}

// Helper functions
function getRemainingBalance(uint256 _agreementId) external view returns (uint256) {
Agreement storage agreement = agreements[_agreementId];
return agreement.totalPrice - agreement.totalPaid;
}

function isAirdropClaimed(uint256 agreementId, bytes32 airdropId)
external
view
returns (bool)
{
return claimedAirdrops[agreementId][airdropId];
}
}
10 changes: 10 additions & 0 deletions packages/hardhat/contracts/test/MockER20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1000000 * 10 ** 18);
}
}
68 changes: 68 additions & 0 deletions packages/hardhat/test/RentToOwn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { expect } from "chai";
import { ethers } from "hardhat";
import { MyNFT, RentToOwn } from "../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { ContractTransactionReceipt, EventLog } from "ethers";

describe("RentToOwn", function () {
let rentToOwn: RentToOwn;
let rentToOwnAddress: any;
let myNFT: MyNFT;
let myNFTAddress: any;
let mockToken: any;
let lender: SignerWithAddress;
let borrower: SignerWithAddress;
let tokenId: any; //BigNumberish //import { BigNumberish } from "ethers";
Expand Down Expand Up @@ -95,4 +97,70 @@ describe("RentToOwn", function () {
).to.be.revertedWith("Payment is late");
});
});

describe("Airdrop functionality", function () {
let merkleRoot: string;
let merkleProof: string[];
let airdropId: string;

beforeEach(async function () {
// Deploy mock ERC20 token for airdrop testing
const MockERC20Factory = await ethers.getContractFactory("MockERC20");
mockToken = await MockERC20Factory.deploy("Mock Token", "MTK");
await mockToken.waitForDeployment();

// Setup existing agreement like in other tests
await myNFT.connect(lender).approve(rentToOwnAddress, tokenId);
await rentToOwn.connect(lender).listNFT(myNFTAddress, tokenId, monthlyPayment, 12 as any);
await rentToOwn.connect(borrower).startAgreement(0 as any, { value: monthlyPayment } as any);

// Create merkle root and proof (simplified for testing)
const agreementId = 0n;
const leaf = ethers.keccak256(ethers.solidityPacked(["uint256", "address"], [agreementId, borrower.address]));
merkleRoot = leaf;
merkleProof = [];

// Register airdrop
const amount = ethers.parseEther("100");
const duration = 86400n;
const tx = await rentToOwn
.connect(lender)
.registerAirdrop(await mockToken.getAddress(), amount as any, merkleRoot as any, duration as any);

const receipt = (await tx.wait()) as ContractTransactionReceipt;
if (!receipt) throw new Error("No receipt available");

const registeredEvent = receipt.logs[0] as EventLog;
if (!registeredEvent) throw new Error("AirdropRegistered event not found");
airdropId = registeredEvent.args[0];

// Fund contract with airdrop tokens
await mockToken.transfer(rentToOwnAddress, amount);
});

it("Should register new airdrop", async function () {
const airdrop = await rentToOwn.airdrops(airdropId as any);
expect(airdrop.tokenContract).to.equal(await mockToken.getAddress());
});

it("Should allow borrower to claim airdrop", async function () {
const initialBalance = await mockToken.balanceOf(borrower.address);
await rentToOwn.connect(borrower).claimAirdrop(0n as any, airdropId as any, merkleProof as any);
const finalBalance = await mockToken.balanceOf(borrower.address);
expect(finalBalance).to.be.gt(initialBalance);
});

it("Should prevent double claiming", async function () {
await rentToOwn.connect(borrower).claimAirdrop(0n as any, airdropId as any, merkleProof as any);
await expect(
rentToOwn.connect(borrower).claimAirdrop(0n as any, airdropId as any, merkleProof as any),
).to.be.revertedWith("Airdrop already claimed");
});

it("Should prevent non-borrowers from claiming", async function () {
await expect(
rentToOwn.connect(lender).claimAirdrop(0n as any, airdropId as any, merkleProof as any),
).to.be.revertedWith("Not the borrower");
});
});
});
2 changes: 1 addition & 1 deletion packages/nextjs/app/borrow/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function BorrowPage() {
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">NFT Rent-to-Own Agreements</h1>

<SkipTimeComponent />
{/*<SkipTimeComponent />*/}

<h2 className="text-2xl font-bold mb-6 mt-6">Available Agreements</h2>
{isLoading && <p>Loading agreements...</p>}
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ThemeProvider } from "~~/components/ThemeProvider";
import "~~/styles/globals.css";
import { getMetadata } from "~~/utils/scaffold-eth/getMetadata";

export const metadata = getMetadata({ title: "Scaffold-ETH 2 App", description: "Built with 🏗 Scaffold-ETH 2" });
export const metadata = getMetadata({ title: "Rent2Own", description: "Rent NFTs" });

const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
return (
Expand Down

0 comments on commit 50bae28

Please sign in to comment.