Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions contracts/build/BuyPoints.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

contract BuyPoints is Ownable {
using SafeERC20 for IERC20Metadata;

// wallet => amount bought
mapping(address => uint256) public walletBoughtAmount;

// totalNumberOfBuys
uint256 public totalBuys;

// totalAmountBought
uint256 public totalAmountBought;

// flag to enable or disable the contract
bool public enabled = true;

// the accepted safeAddress
address public safeAddress;

// the accepted token
address public buildAddress;

// A new buy has been created
event Buy(address indexed wallet, uint256 amount, uint256 walletTotalAmountBought);

constructor(address contractOwner, address _safeAddress, address _buildAddress) Ownable(contractOwner) {
transferOwnership(contractOwner);
safeAddress = _safeAddress;
buildAddress = _buildAddress;
}

modifier onlyWhileEnabled() {
require(enabled, "The contract is disabled.");
_;
}

function buy(uint256 _amount) public onlyWhileEnabled {
require(_amount > 0, "Invalid amount");
require(IERC20Metadata(buildAddress).balanceOf(msg.sender) >= _amount, "You don't have enough balance");
require(
IERC20Metadata(buildAddress).allowance(msg.sender, address(this)) >= _amount,
"Must approve contract first"
);

// transfer from sender => contract
IERC20Metadata(buildAddress).safeTransferFrom(msg.sender, safeAddress, _amount);

// setup the internal state
totalBuys = totalBuys + 1;
totalAmountBought = totalAmountBought + _amount;
uint256 _totalWalletBoughtAmount = walletBoughtAmount[msg.sender] + _amount;
walletBoughtAmount[msg.sender] = _totalWalletBoughtAmount;

// emit event
emit Buy(msg.sender, _amount, _totalWalletBoughtAmount);
}

// Safe method in case someone sends tokens to the smart contract
function adminWithdraw(uint256 _amount, address _token, address wallet) public onlyOwner {
require(_amount > 0, "Invalid amount");

require(IERC20Metadata(_token).balanceOf(address(this)) >= _amount, "Not enough funds on contract");

IERC20Metadata(_token).safeTransfer(wallet, _amount);
}

// Admin

/**
* @notice Disables the contract, disabling future sponsorships.
* @dev Can only be called by the owner.
*/
function disable() public onlyWhileEnabled onlyOwner {
enabled = false;
}

/**
* @notice Enables the contract, enabling new sponsors.
* @dev Can only be called by the owner.
*/
function enable() public onlyOwner {
enabled = true;
}
}
26 changes: 26 additions & 0 deletions scripts/build/deployBuyPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ethers, network } from "hardhat";

async function main() {
console.log(`Deploying buy points at ${network.name}`);

const [admin] = await ethers.getSigners();

console.log(`Admin will be ${admin.address}`);

const buyPoints = await ethers.getContractFactory("BuyPoints");

const contract = await buyPoints.deploy();
await contract.deployed();

console.log(`BuyPoints Address: ${contract.address}`);
console.log(`BuyPoints owner: ${await contract.owner()}`);

console.log("Done");
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
140 changes: 140 additions & 0 deletions test/contracts/build/BuyPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import chai from "chai";
import { ethers, waffle } from "hardhat";
import { solidity } from "ethereum-waffle";

import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";

import { ERC20Mock, BuyPoints } from "../../../typechain-types";
import { Artifacts } from "../../shared";

import { findEvent } from "../../shared/utils";

chai.use(solidity);

const { expect } = chai;
const { parseUnits } = ethers.utils;
const { deployContract } = waffle;

describe("BuyPoints", () => {
let admin: SignerWithAddress;
let buyer: SignerWithAddress;
let safe: SignerWithAddress;

let contract: BuyPoints;
let build: ERC20Mock;

beforeEach(async () => {
[admin, buyer, safe] = await ethers.getSigners();

build = (await deployContract(admin, Artifacts.ERC20Mock, ["Build", "BUILD"])) as ERC20Mock;

await build.connect(admin).transfer(buyer.address, parseUnits("10000"));
});

async function builder() {
return deployContract(admin, Artifacts.BuyPoints, [admin.address, safe.address, build.address]);
}

describe("behaviour", () => {
beforeEach(async () => {
contract = (await builder()) as BuyPoints;
});

it("is created with no state", async () => {
expect(await contract.totalBuys()).to.eq(0);
expect(await contract.totalAmountBought()).to.eq(0);
});

it("emits a buy event everytime a buy is created", async () => {
const amount = parseUnits("10");
await build.connect(buyer).approve(contract.address, amount);

const tx = await contract.connect(buyer).buy(amount);

const event = await findEvent(tx, "Buy");

expect(event).to.exist;
expect(event?.args?.wallet).to.eq(buyer.address);
expect(event?.args?.amount).to.eq(amount);
expect(event?.args?.walletTotalAmountBought).to.eq(amount);

expect(await build.balanceOf(safe.address)).to.eq(amount);

expect(await contract.totalBuys()).to.eq(1);
expect(await contract.totalAmountBought()).to.eq(amount);
});
});

describe("testing contract enable and disable", () => {
beforeEach(async () => {
contract = (await builder()) as BuyPoints;
});

it("allows the contract owner to disable and enable the contract", async () => {
expect(await contract.enabled()).to.be.equal(true);

await contract.connect(admin).disable();

expect(await contract.enabled()).to.be.equal(false);

await contract.connect(admin).enable();

expect(await contract.enabled()).to.be.equal(true);
});

it("prevents other accounts to disable the contract", async () => {
expect(await contract.enabled()).to.be.equal(true);

const action = contract.connect(buyer).disable();

await expect(action).to.be.reverted;

expect(await contract.enabled()).to.be.equal(true);
});

it("prevents other accounts to enable the contract", async () => {
const action = contract.connect(buyer).enable();

await expect(action).to.be.reverted;
});

it("prevents disable when the contract is already disabled", async () => {
expect(await contract.enabled()).to.be.equal(true);

await contract.connect(admin).disable();

const action = contract.connect(admin).disable();

await expect(action).to.be.reverted;
});

it("prevents new buys when the contract is disabled", async () => {
expect(await contract.enabled()).to.be.equal(true);

await contract.connect(admin).disable();

expect(await contract.enabled()).to.be.equal(false);

const amount = parseUnits("10");
await build.connect(buyer).approve(contract.address, amount);

const action = contract.connect(buyer).buy(amount);

await expect(action).to.be.revertedWith("The contract is disabled.");
});

it("allows the owner to withdraw", async () => {
const amount = parseUnits("100");

const initialBalance = await build.balanceOf(buyer.address);

await build.connect(buyer).approve(contract.address, amount);

await build.connect(buyer).transfer(contract.address, amount);

await contract.connect(admin).adminWithdraw(amount, build.address, buyer.address);

expect(await build.balanceOf(buyer.address)).to.eq(initialBalance);
});
});
});
4 changes: 3 additions & 1 deletion test/shared/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ import PassportRegistry from "../../artifacts/contracts/passport/PassportRegistr
import TalentProtocolToken from "../../artifacts/contracts/talent/TalentProtocolToken.sol/TalentProtocolToken.json";
import TalentRewardClaim from "../../artifacts/contracts/talent/TalentRewardClaim.sol/TalentRewardClaim.json";
import PassportBuilderScore from "../../artifacts/contracts/passport/PassportBuilderScore.sol/PassportBuilderScore.json";
import BuyPoints from "../../artifacts/contracts/build/BuyPoints.sol/BuyPoints.json";
import ERC20Mock from "../../artifacts/contracts/test/ERC20Mock.sol/ERC20Mock.json";

export { PassportRegistry, TalentProtocolToken, TalentRewardClaim, PassportBuilderScore };
export { PassportRegistry, TalentProtocolToken, TalentRewardClaim, PassportBuilderScore, ERC20Mock, BuyPoints };