feature: conditional payment as setting (#134)

* feat: add base contract for billing

* feat: add withdraw function

* feat: add billing requirement to mint

* test: add foundry tests for minting with billing

* refactor: remove transfer billing and add access point

* test: add access point billing foundry tests

* test: add test for billing value change

* test: add hardhat test setup for billing

* test: add hardhat tests for billing

* feat: add withdrawn event and add public withdraw function

* test: add tests for withdrawing founds and access control for billing

* refactor: fix misspells and change variable names

* feat: add initialize params for billing

* feat: add gap to FleekBilling

* fix: testname misspell
This commit is contained in:
Felipe Mendes 2023-02-27 17:30:19 -03:00 committed by GitHub
parent 969cd12d92
commit b8b8cb28ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 458 additions and 12 deletions

View File

@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
error RequiredPayment(uint requiredValue);
abstract contract FleekBilling is Initializable {
/**
* @dev Available billing values.
*/
enum Billing {
Mint,
AddAccessPoint
}
/**
* @dev Emitted when the billing value is changed.
*/
event BillingChanged(Billing key, uint256 price);
/**
* @dev Emitted when contract is withdrawn.
*/
event Withdrawn(uint256 value, address indexed byAddress);
/**
* @dev Mapping of billing values.
*/
mapping(Billing => uint256) public _billings;
/**
* @dev Initializes the contract by setting default billing values.
*/
function __FleekBilling_init(uint256[] memory initialBillings) internal onlyInitializing {
for (uint256 i = 0; i < initialBillings.length; i++) {
_setBilling(Billing(i), initialBillings[i]);
}
}
/**
* @dev Returns the billing value for a given key.
*/
function getBilling(Billing key) public view returns (uint256) {
return _billings[key];
}
/**
* @dev Sets the billing value for a given key.
*/
function _setBilling(Billing key, uint256 price) internal {
_billings[key] = price;
emit BillingChanged(key, price);
}
/**
* @dev Internal function to require a payment value.
*/
function _requirePayment(Billing key) internal {
uint256 requiredValue = _billings[key];
if (msg.value != _billings[key]) revert RequiredPayment(requiredValue);
}
/**
* @dev Internal function to withdraw the contract balance.
*/
function _withdraw() internal {
address by = msg.sender;
uint256 value = address(this).balance;
payable(by).transfer(value);
emit Withdrawn(value, by);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}

View File

@ -6,6 +6,7 @@ import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./FleekAccessControl.sol";
import "./FleekBilling.sol";
import "./util/FleekStrings.sol";
import "./FleekPausable.sol";
@ -18,7 +19,7 @@ error ThereIsNoTokenMinted();
error InvalidTokenIdForAccessPoint();
error AccessPointCreationStatusAlreadySet();
contract FleekERC721 is Initializable, ERC721Upgradeable, FleekAccessControl, FleekPausable {
contract FleekERC721 is Initializable, ERC721Upgradeable, FleekAccessControl, FleekPausable, FleekBilling {
using Strings for uint256;
using FleekStrings for FleekERC721.App;
using FleekStrings for FleekERC721.AccessPoint;
@ -123,10 +124,14 @@ contract FleekERC721 is Initializable, ERC721Upgradeable, FleekAccessControl, Fl
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
*/
function initialize(string memory _name, string memory _symbol) public initializer {
function initialize(
string memory _name,
string memory _symbol,
uint256[] memory initialBillings
) public initializer {
__ERC721_init(_name, _symbol);
__FleekAccessControl_init();
_appIds = 0;
__FleekBilling_init(initialBillings);
__FleekPausable_init();
}
@ -146,6 +151,7 @@ contract FleekERC721 is Initializable, ERC721Upgradeable, FleekAccessControl, Fl
* Requirements:
*
* - the caller must have ``collectionOwner``'s admin role.
* - billing for the minting may be applied.
* - the contract must be not paused.
*
*/
@ -160,7 +166,7 @@ contract FleekERC721 is Initializable, ERC721Upgradeable, FleekAccessControl, Fl
string memory logo,
uint24 color,
bool accessPointAutoApproval
) public payable requireCollectionRole(CollectionRoles.Owner) returns (uint256) {
) public payable requirePayment(Billing.Mint) requireCollectionRole(CollectionRoles.Owner) returns (uint256) {
uint256 tokenId = _appIds;
_mint(to, tokenId);
@ -448,11 +454,14 @@ contract FleekERC721 is Initializable, ERC721Upgradeable, FleekAccessControl, Fl
* Requirements:
*
* - the tokenId must be minted and valid.
* - billing for add acess point may be applied.
* - the contract must be not paused.
*
* IMPORTANT: The payment is not set yet
*/
function addAccessPoint(uint256 tokenId, string memory apName) public payable whenNotPaused {
function addAccessPoint(
uint256 tokenId,
string memory apName
) public payable whenNotPaused requirePayment(Billing.AddAccessPoint) {
// require(msg.value == 0.1 ether, "You need to pay at least 0.1 ETH"); // TODO: define a minimum price
_requireMinted(tokenId);
if (_accessPoints[apName].owner != address(0)) revert AccessPointAlreadyExists();
@ -800,4 +809,44 @@ contract FleekERC721 is Initializable, ERC721Upgradeable, FleekAccessControl, Fl
function setPausable(bool pausable) public requireCollectionRole(CollectionRoles.Owner) {
_setPausable(pausable);
}
/*//////////////////////////////////////////////////////////////
BILLING
//////////////////////////////////////////////////////////////*/
/**
* @dev Modifier to require billing with a given key.
*/
modifier requirePayment(Billing key) {
_requirePayment(key);
_;
}
/**
* @dev Sets the billing value for a given key.
*
* May emit a {BillingChanged} event.
*
* Requirements:
*
* - the sender must have the `collectionOwner` role.
*
*/
function setBilling(Billing key, uint256 value) public requireCollectionRole(CollectionRoles.Owner) {
_setBilling(key, value);
}
/**
* @dev Withdraws all the funds from contract.
*
* May emmit a {Withdrawn} event.
*
* Requirements:
*
* - the sender must have the `collectionOwner` role.
*
*/
function withdraw() public requireCollectionRole(CollectionRoles.Owner) {
_withdraw();
}
}

View File

@ -8,6 +8,7 @@ const { getProxyAddress, proxyStore } = require('./utils/proxy-store');
const ARGUMENTS = [
'FleekNFAs', // Collection name
'FLKNFA', // Collection symbol
[], // Billing values
];
// --- Script Settings ---

View File

@ -3,6 +3,7 @@
pragma solidity ^0.8.17;
import "./TestBase.sol";
import {FleekBilling} from "contracts/FleekBilling.sol";
import "contracts/FleekAccessControl.sol";
contract Test_FleekERC721_AccessControlAssertions is Test {
@ -17,10 +18,10 @@ contract Test_FleekERC721_AccessControlAssertions is Test {
contract Test_FleekERC721_AccessControl is Test_FleekERC721_Base, Test_FleekERC721_AccessControlAssertions {
uint256 internal tokenId;
address internal collectionOwner = address(1);
address internal tokenOwner = address(3);
address internal tokenController = address(4);
address internal anyAddress = address(5);
address internal collectionOwner = address(100);
address internal tokenOwner = address(200);
address internal tokenController = address(300);
address internal anyAddress = address(400);
function setUp() public {
baseSetUp();
@ -357,6 +358,56 @@ contract Test_FleekERC721_AccessControl is Test_FleekERC721_Base, Test_FleekERC7
CuT.burn(tokenId);
}
function test_setBilling() public {
// ColletionOwner
vm.prank(collectionOwner);
CuT.setBilling(FleekBilling.Billing.Mint, 1 ether);
// TokenOwner
vm.prank(tokenOwner);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Owner);
CuT.setBilling(FleekBilling.Billing.Mint, 2 ether);
// TokenController
vm.prank(tokenController);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Owner);
CuT.setBilling(FleekBilling.Billing.Mint, 2 ether);
// AnyAddress
vm.prank(anyAddress);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Owner);
CuT.setBilling(FleekBilling.Billing.Mint, 2 ether);
}
function test_withdraw() public {
// ColletionOwner
vm.deal(address(CuT), 1 ether);
vm.prank(collectionOwner);
CuT.withdraw();
// TokenOwner
vm.prank(tokenOwner);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Owner);
CuT.withdraw();
// TokenController
vm.prank(tokenController);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Owner);
CuT.withdraw();
// AnyAddress
vm.prank(anyAddress);
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Owner);
CuT.withdraw();
}
/**
* @dev `receive` and `fallback` are required for test contract receive ETH
*/
receive() external payable {}
fallback() external payable {}
function test_pauseAndUnpause() public {
// ColletionOwner
vm.startPrank(collectionOwner);

View File

@ -0,0 +1,164 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "./TestBase.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {FleekAccessControl} from "contracts/FleekAccessControl.sol";
import "contracts/FleekBilling.sol";
contract Test_FleekERC721_BillingAssertions is Test {
event BillingChanged(FleekBilling.Billing key, uint256 price);
event Withdrawn(uint256 value, address indexed byAddress);
function expectRevertWithRequiredPayment(uint256 value) public {
vm.expectRevert(abi.encodeWithSelector(RequiredPayment.selector, value));
}
function expectEmitBillingChanged(FleekBilling.Billing key, uint256 price) public {
vm.expectEmit(true, true, true, true);
emit BillingChanged(key, price);
}
function expectEmitWithdawn(uint256 value, address byAddress) public {
vm.expectEmit(true, true, true, true);
emit Withdrawn(value, byAddress);
}
}
contract Test_FleekERC721_Billing is Test_FleekERC721_Base, Test_FleekERC721_BillingAssertions {
using Strings for address;
uint256 internal tokenId;
uint256 internal constant mintPrice = 1 ether;
uint256 internal constant addAPPrice = 1 ether;
function setUp() public {
baseSetUp();
tokenId = mintDefault(deployer);
CuT.setBilling(FleekBilling.Billing.Mint, mintPrice);
CuT.setBilling(FleekBilling.Billing.AddAccessPoint, addAPPrice);
}
function test_setUp() public {
assertEq(CuT.getBilling(FleekBilling.Billing.Mint), mintPrice);
assertEq(CuT.getBilling(FleekBilling.Billing.AddAccessPoint), addAPPrice);
assertEq(address(CuT).balance, 0);
}
function test_mint() public {
CuT.mint{value: mintPrice}(
deployer,
TestConstants.APP_NAME,
TestConstants.APP_DESCRIPTION,
TestConstants.APP_EXTERNAL_URL,
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS
);
assertEq(CuT.ownerOf(tokenId), deployer);
assertEq(address(CuT).balance, mintPrice);
}
function testFuzz_cannotMintWithWrongValue(uint256 value) public {
vm.assume(value != mintPrice);
vm.deal(deployer, value);
expectRevertWithRequiredPayment(mintPrice);
CuT.mint{value: value}(
deployer,
TestConstants.APP_NAME,
TestConstants.APP_DESCRIPTION,
TestConstants.APP_EXTERNAL_URL,
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS
);
assertEq(address(CuT).balance, 0);
}
function testFuzz_shouldChangeMintBillingValue(uint256 value) public {
expectEmitBillingChanged(FleekBilling.Billing.Mint, value);
CuT.setBilling(FleekBilling.Billing.Mint, value);
assertEq(CuT.getBilling(FleekBilling.Billing.Mint), value);
vm.deal(deployer, value);
CuT.mint{value: value}(
deployer,
TestConstants.APP_NAME,
TestConstants.APP_DESCRIPTION,
TestConstants.APP_EXTERNAL_URL,
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS
);
assertEq(CuT.ownerOf(tokenId), deployer);
assertEq(address(CuT).balance, value);
}
function test_addAccessPoint() public {
CuT.addAccessPoint{value: addAPPrice}(tokenId, "accesspoint.com");
assertFalse(CuT.isAccessPointNameVerified("accesspoint.com"));
assertEq(address(CuT).balance, addAPPrice);
}
function testFuzz_cannotAddAccessPointWithWrongValue(uint256 value) public {
vm.assume(value != addAPPrice);
vm.deal(deployer, value);
expectRevertWithRequiredPayment(addAPPrice);
CuT.addAccessPoint{value: value}(tokenId, "accesspoint.com");
assertEq(address(CuT).balance, 0);
}
function testFuzz_shouldChangeAddAPBillingValue(uint256 value) public {
expectEmitBillingChanged(FleekBilling.Billing.AddAccessPoint, value);
CuT.setBilling(FleekBilling.Billing.AddAccessPoint, value);
assertEq(CuT.getBilling(FleekBilling.Billing.AddAccessPoint), value);
vm.deal(deployer, value);
CuT.addAccessPoint{value: value}(tokenId, "accesspoint.com");
assertFalse(CuT.isAccessPointNameVerified("accesspoint.com"));
assertEq(address(CuT).balance, value);
}
function testFuzz_shouldWithdrawAnyContractFunds(uint128 value) public {
uint256 balanceBefore = address(this).balance;
vm.deal(address(CuT), value);
CuT.withdraw();
assertEq(address(this).balance, value + balanceBefore);
}
function testFuzz_shouldWithdrawAllContractFundsAfterPayableCall(uint8 iterations) public {
// this test is going to add access points up to 256 times and then withdraw all funds
uint256 balanceBefore = address(this).balance;
address randomAddress = address(1);
uint256 totalExpectedValue = iterations * addAPPrice;
vm.deal(randomAddress, totalExpectedValue);
vm.startPrank(randomAddress);
for (uint256 i = 0; i < iterations; i++) {
CuT.addAccessPoint{value: addAPPrice}(tokenId, Strings.toString(i));
}
vm.stopPrank();
expectEmitWithdawn(totalExpectedValue, deployer);
CuT.withdraw();
assertEq(address(this).balance, totalExpectedValue + balanceBefore);
}
/**
* @dev `receive` and `fallback` are required for test contract receive ETH
*/
receive() external payable {}
fallback() external payable {}
}

View File

@ -17,6 +17,8 @@ library TestConstants {
uint24 public constant APP_COLOR = 0x123456;
bool public constant APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS = true;
string public constant LOGO_0 =
"data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI1MDAiIHdpZHRoPSIyMTgzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjQgMTQxLjUzMTk5OTk5OTk5OTk4Ij48cGF0aCBkPSJNMTAuMzgzIDEyNi44OTRMMCAwbDEyNCAuMjU1LTEwLjk3OSAxMjYuNjM5LTUwLjU1MyAxNC42Mzh6IiBmaWxsPSIjZTM0ZjI2Ii8+PHBhdGggZD0iTTYyLjQ2OCAxMjkuMjc3VjEyLjA4NWw1MS4wNjQuMTctOS4xMDYgMTA0Ljg1MXoiIGZpbGw9IiNlZjY1MmEiLz48cGF0aCBkPSJNOTkuNDkgNDEuMzYybDEuNDQ2LTE1LjQ5SDIyLjM4M2w0LjM0IDQ3LjQ5aDU0LjIxM0w3OC44MSA5My42MTdsLTE3LjM2MiA0LjY4LTE3LjYxNy01LjEwNi0uOTM2LTEyLjA4NUgyNy4zMTlsMi4xMjggMjQuNjgxIDMyIDguOTM2IDMyLjI1NS04LjkzNiA0LjM0LTQ4LjE3SDQxLjEwN0wzOS40OSA0MS4zNjJ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+";

View File

@ -24,7 +24,7 @@ contract Test_FleekERC721_Deploy is Test_FleekERC721_Base {
function testFuzz_nameAndSymbol(string memory _name, string memory _symbol) public {
CuT = new FleekERC721();
CuT.initialize(_name, _symbol);
CuT.initialize(_name, _symbol, new uint256[](0));
assertEq(CuT.name(), _name);
assertEq(CuT.symbol(), _symbol);

View File

@ -46,7 +46,7 @@ abstract contract Test_FleekERC721_Base is Test, Test_FleekERC721_Assertions {
function baseSetUp() internal {
CuT = new FleekERC721();
CuT.initialize("Test Contract", "FLKAPS");
CuT.initialize("Test Contract", "FLKAPS", new uint256[](0));
deployer = address(this);
}

View File

@ -0,0 +1,90 @@
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { ethers } from 'hardhat';
import { expect } from 'chai';
import { Fixtures, TestConstants, Errors } from './helpers';
const { Billing, MintParams } = TestConstants;
describe('FleekERC721.Billing', () => {
let fixture: Awaited<ReturnType<typeof Fixtures.withMint>>;
const mintPrice = ethers.utils.parseEther('1');
const addAPPrice = ethers.utils.parseEther('1');
const mint = (value?: any) => {
const { contract, owner } = fixture;
return contract.mint(
owner.address,
MintParams.name,
MintParams.description,
MintParams.externalUrl,
MintParams.ens,
MintParams.commitHash,
MintParams.gitRepository,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
{ value }
);
};
const addAP = (value?: any) => {
const { contract } = fixture;
return contract.addAccessPoint(0, 'random.com', { value });
};
beforeEach(async () => {
fixture = await loadFixture(Fixtures.withMint);
const { contract } = fixture;
await contract.setBilling(Billing.Mint, mintPrice);
await contract.setBilling(Billing.AddAccessPoint, addAPPrice);
});
it('should start with mint prices', async () => {
const { contract } = fixture;
expect(await contract.getBilling(Billing.Mint)).to.equal(mintPrice);
expect(await contract.getBilling(Billing.AddAccessPoint)).to.equal(
addAPPrice
);
});
it('should allow mint with transfer', async () => {
const { contract, owner } = fixture;
await mint(mintPrice);
console.log('hit');
expect(await contract.ownerOf(0)).to.equal(owner.address);
});
it('should not allow mint with empty value', async () => {
const { contract } = fixture;
await expect(mint())
.to.be.revertedWithCustomError(contract, Errors.RequiredPayment)
.withArgs(mintPrice);
});
it('should not allow mint with different value', async () => {
const { contract } = fixture;
await expect(mint(ethers.utils.parseEther('2')))
.to.be.revertedWithCustomError(contract, Errors.RequiredPayment)
.withArgs(mintPrice);
});
it('should allow add access point with transfer', async () => {
const { contract } = fixture;
await addAP(addAPPrice);
expect(await contract.getAccessPointJSON('random.com')).to.exist;
});
it('should not allow add access point with empty value', async () => {
const { contract } = fixture;
await expect(addAP())
.to.be.revertedWithCustomError(contract, Errors.RequiredPayment)
.withArgs(addAPPrice);
});
it('should not allow add access point with different value', async () => {
const { contract } = fixture;
await expect(addAP(ethers.utils.parseEther('2')))
.to.be.revertedWithCustomError(contract, Errors.RequiredPayment)
.withArgs(addAPPrice);
});
});

View File

@ -11,6 +11,10 @@ export const TestConstants = Object.freeze({
REJECTED: 2,
REMOVED: 3,
},
Billing: {
Mint: 0,
AddAccessPoint: 1,
},
MintParams: {
name: 'Fleek Test App',
description: 'Fleek Test App Description',

View File

@ -13,4 +13,5 @@ export const Errors = Object.freeze({
ContractIsNotPausable: 'ContractIsNotPausable',
PausableIsSetTo: 'PausableIsSetTo',
ThereIsNoTokenMinted: 'ThereIsNoTokenMinted',
RequiredPayment: 'RequiredPayment',
});

View File

@ -16,11 +16,13 @@ export abstract class Fixtures {
const Contract = await ethers.getContractFactory('FleekERC721', {
libraries,
});
const contract = await upgrades.deployProxy(
Contract,
[
TestConstants.CollectionParams.name,
TestConstants.CollectionParams.symbol,
[], // Initial Billings
],
{
unsafeAllow: ['external-library-linking'],