2.2.2 • Published 1 year ago

test-contracts-types v2.2.2

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

Inverse smart contracts

Install library dependancies for local test

yarn

Start local network with ganache

yarn ganache

All available commands

The package.json file contains a set of scripts to help on the development phase. Below is a short description for each

  • "ganache" run local node (development network) with ganache-cli
  • "migrate" run migration on development network
  • "test" run tests locally
  • "test:ci" run tests in CI system
  • "lint:sol" lint solidity code according to rules
  • "lint:js" lint javascript code according to rules
  • "lint" lint solidity code
  • "truffle test -- -g "name of test"" run specific test

Solhint

You can find rules and explanations here

What is Kanji ?

Kanji is a plug & play plateform from which brands can create entire collections in a few clicks and deploy their own Marketplace contracts through the front end of our plateform. We want them to be 100% owner of their contracts, being the deployer address of their contracts.

We aim to be only a SAAS platform, not a trusted third-party, we do not have any rights on their contracts.

In all the following, we will thus quote 3 different types of addresses:

  • Kanji (our own wallet address)
  • Brands (which are our clients) and will be creators/owners of contracts
  • Brands’ clients: the potential buyers of brands’ NFTs

Introduction/problems we would like to solve

• The way we implement auctions and direct sales listings in our MarketPlace.sol contract contract requires massive writing in the contract, this enables us to list only approximately 150 NFTs with a single transaction, ideally we want to be able to list an entire collection (<=10 000 NFTs) in a single transaction.

• We want our NFTs to generate royalties on a wide variety of secondary-hand Marketplace (opensea, looksrare,rarible). This is why we implemented ERC2981 and contractURI() function. ( see contract level metadata of opensea ). We want brands to be able to split royalties between different addresses (payeesRoyalties).

To do this we set as the royalty beneficiary the NFT contract itself (see *) and implemented 2 options:

• inheriting in our NFT contracts from PayementSplitter extension, so that the royalties funds can be redeamed with releaseAll() function. We do not like this solution because it is not “automatic” someone will have to trigger this function periodically to deliver the royalties to beneficiaries.

• implementing a receive() function in the contracts that split automatically the incoming royalties funds between the beneficiaries. This solution is “automatic”, no need of extra calls to deliver royalties. However, we are not sure this solution is scalable or efficient because it uses a loop of transfer calls, which costs significant gas. We do not know how much gas secondary-hand Marketplace (opensea, looksrare,rarible) attach to their transfers and the receive function might not be triggered in case of unsufficient amount of gas from them. What do you think? You can find the corresponding contracts in contracts/contractWithReceiveSplitRoyalties folder.

(*)We had to specify the address of the NFT contract in the JSON pointed by _contractURI in the constructor. However a problem arose, how to add the address of the contract in a json even before the contract is deployed? For that we decided to use the following method:

  • precalculate the address of the contract just before its deployment with nonce and public address of deployer of contract (see line 44 in test/0-testwithreceive.js)
  • Set the obtained address as fee_recepient in the contract level metadata json file
  • send the json to ipfs
  • get this ipfs address and use it in _contractURI
let nonceME = await web3.eth.getTransactionCount(addressDeployer);
var preGeneratedAddressContract = "0x" + web3.utils.sha3(
RLP.encode(
    [addressDeployer,nonceME]
)).slice(12)
.substring(14)

let contractLevelMetada = {
    "name": "OpenSea Creatures",
    "description": "OpenSea Creatures are adorable aquatic beings primarily for demonstrating what can be done using the OpenSea platform. Adopt one today to try out all the OpenSea buying, selling, and bidding feature set.",
    "image": "external-link-url/image.png",
    "external_link": "external-link-url",
    "seller_fee_basis_points": 100, # Indicates a 1% seller fee.
    "fee_recipient": preGeneratedAddressContract # Where royalties fees will be paid to
}

let URIContractLevelMetadat = await sendToPinata(contractLevelMetada);

this.buyReveal = await KANJIDROPERC721A.new(
    'Name',
    'Symbol',
    500,//Fees royalties for superrare
    account2,//Address of Arkania fees
    500,//5% fees of arkania
    cloneAccountsfiltered,//accounts royalties for fees open sea (by payment splitter)
    cloneAccountsValue,//% royalties for fees open sea (by payment splitter)
    [account1,account2,account3,account4],//accounts beneficiaries
    [2500,2500,2500,2500],//% fees beneficiaries
    URIContractLevelMetadat,
);// we deploy contract

CONTRACTS

MarketPlace.sol

The MarketPlace.sol contract is based on the MarketPlace.sol from thirdweb.

The MarketPlace.sol contract allows the brands to create LISTs that are either auctions or direct sales, in wich you can list ERC721 or ERC1155 tokens it is deployed and owned by the brand.

The tokens in direct sales can then be bought directly by an external user by a brand's client.

The tokens in auctions can be outbided by an brand's client until the end date of the sale is passed. At the end of an auction the function endAuction can be called by anyone to transfer tokens and funds or just transfer token or just fund (depending on transact argument).

function createList (onlyowner)

Function allowing the creation of a list respecting the LIST structure found in struct.sol.

function updateList (onlyowner)

Function allowing the update of a list respecting the LIST structure found in struct.sol as long as it has not started.

function addTokenToList (onlyowner)

Function to add an array of struct TOKEN to a struct LIST, requiring that the type of the token respect the same type of tokens in the LIST.

function buy

Function to immediately purchase a token contained in a LIST for a minPrice value, sending the funds directly to the beneficiaries of the sales set up in the LIST.

:warning: minPrice has two meanings depending on type of lists, it means the price of the NFT in direct sale, or starting price in an auction.

function bidding

Function allowing to bid on a token contained in a LIST (minPrice being starting price of the auction).

function endAuction

Function allowing to receive the token after the end of an auction. It is callable by anyone. It sends the funds directly to the beneficiaries of the auction and the NFT directly to the winner of the auction but the payment and the transfer of token can also be done individually (depending on transact argument).

Note: Any idea? We would prefer to avoid this function because we would prefer to avoid an extra call from the brand (in case the last bidder do not claim his NFT) However it seems to be the only secure way to end an auction. We thought of allowing our address to end the auction but :

  • it would cost us gas fees
  • we do not want to be trust-party

function transferListingTokens (internal)

Internal function of the contract allowing to transfer tokens from one address to another.

function checkBeneficiaries (internal)

Internal function of the contract to check that the length of the array of beneficiaries addresses and shares of the beneficiaries correspond and that shares do not exceed 100 %.

function payBeneficiaries (internal)

Internal function of the contract to calculate and send the shares to the beneficiaries at the end of a sale/auction.

GETTERS

function getLists

Returns the array of LISTS contained in the MarketPlace.sol contract.

function getTokensInList

Returns the array of TOKENS contained in a specific LIST.

Making marketplace Upgradeable with KANJIFACTORY.sol

We want our Marketplace contract to be upgradeable. We want brands to be able to upgrade the source code of our functions and add new functions. For this, we used the UUPS Transparent Proxy.

Kanji will deploy a unique KANJIFACTORY.sol contract that is the admin of every proxys that will be deployed.

this.factory = await KANJIFACTORY.new({from:kanji_account});

When a brand will deploy its first MarketPlace.sol contract (MarketPlaceV1), it will be able to call the registerToFactoryProxy() function in MarketPlace.sol which calls the KANJIFACTORY contract and creates a proxy for MarketPlaceV1 (with proxy admin being the Marketplace contract itself), (the owner role of MarketPlaceV1 will be correctly assigned to the brand address thanks to transferOwnership(sender) in initializer).

this.Marketplace = await MarketPlace.new(account_three,500,500,{from:brand_account});
await this.Marketplace.registerToFactoryProxy(this.factory.address,{from:brand_account});

When the brand or one brand's client will want to perform a function from MarketPlace.sol contract, they will call the proxy contract that fallbacks the function calls to the MarketPlace.sol contract. Please see in the below diagram the workflow.

let proxies = await this.factory.proxies(this.Marketplace.address,{from: brand_account});
this.MarketplaceDelegate = await MarketPlace.at(proxies);
///Exemple of brand calls
await this.MarketplaceDelegate.VERSION.call({from: brand_account})
///Exemple of brand's client calls
await this.MarketplaceDelegate.VERSION.call({from: user_account})

When the brand wants to upgrade its contract with a new contract (MarketPlaceV2), it needs to:

  • deploy MarktplaceV2 contract
  • call upgradeTo() function from the proxy contract which takes as argument the address of MarketPlaceV2
this.MarketplaceV2 = await MarketPlaceV2.new({from: brand_account});
await this.MarketplaceDelegate.upgradeTo(this.MarketplaceV2.address,{from:brand_account});

schema

Please see test in our file test/2-deployAndUpgradeByKanji.js

KANJIERC721A.sol

The KANJIERC721A.sol contract allows brand to create ERC721A, these tokens can then be auctioned or sold in the MarketPlace.sol contract. The NFTs can be lazyminted (only set the URI) or minted by owner. It is deployed and owned by the brand address. Only a mapping allowedminters addresses are allowed to mint lazyminted tokens (in particular it can be a Marketplace.sol contract or the owner of the ERC721A contract for example). At deployement of the contract, the _lazyMint parameter is initiated to 0 and its final value will be determined at first call of setUri() function:

  • it gets equal to 1 if the first tokens are minted, in this case, the brand will only be able to mint tokens after (no lazyminting possible anymore)
  • it gets equal to 2 if the tokens are lazyminted (only the uri are registered), in this case, the brand will only be able to lazymint tokens after. The tokens will then be minted by one of the address in allowedminters (using lazyMint() function)

Variables

///Largest tokenId of each batch of tokens with the same baseURI
uint256[] private baseURIIndices;
///Mapping from 'Largest tokenId of a batch of tokens with the same baseURI' to base URI for the respective batch of tokens.
mapping(uint256 => string) private baseURI;

Indeed when the brand wants to create (either lazy minting or minting) a batch of tokens it will pass a corresponding URI that will be stored in baseURIIndices/baseURI.

SETTER

function paused (onlyOwner)

Putting on pause will block token transfers.

function setUri (onlyOwner)

Function to perform lazyminting or minting depending on the value of _lazymint.

function lazyMint

Function to perform minting of lazyminted tokens by allowedminters adresses.

function batchTransferQuantity (onlyOwner)

Function to transfer a large amount of tokens from the owner to a wallet address.

function setAllowedMinters (onlyOwner)

Updating the allowed addresses to mint tokens from this contract.

GETTER

function contractURI

The function that returns the metadatas of the contract level metadata for opensea.

function tokenURI

The function that returns the metadata of the token based on its corresponding baseURI index found in baseURIIndices.

UERC721A.sol

We overrided ERC721A to allow batch minting from a specific starting token id. We simply added a function called _mintSpecificToken based on native _mint function (instead of using "uint256 startTokenId = _currentIndex;" as next tokenID to mint we added uint256 startTokenId as an argument of the function). This library is only use in KANJIERC721A.sol because we needed a way to mint token from a certain range ID when we are performing minting from the MarketPlace.sol contract.

function _mintSpecificToken(
    address to,
    uint256 quantity,
    uint256 startTokenId,//this argument has added
    bytes memory _data,
    bool safe
) internal {
    if (to == address(0)) revert MintToZeroAddress();
    if (quantity == 0) revert MintZeroQuantity();

    _beforeTokenTransfers(address(0), to, startTokenId, quantity);

    // Overflows are incredibly unrealistic.
    // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1
    // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1
    unchecked {
        _addressData[to].balance += uint64(quantity);
        _addressData[to].numberMinted += uint64(quantity);

        _ownerships[startTokenId].addr = to;
        _ownerships[startTokenId].startTimestamp = uint64(block.timestamp);

        uint256 updatedIndex = startTokenId;
        uint256 end = updatedIndex + quantity;

        if (safe && to.isContract()) {
            do {
                emit Transfer(address(0), to, updatedIndex);
                if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) {
                    revert TransferToNonERC721ReceiverImplementer();
                }
            } while (updatedIndex != end);
            // Reentrancy protection
            if (_currentIndex != startTokenId) revert("Reentrancy protection");
        } else {
            do {
                emit Transfer(address(0), to, updatedIndex++);
            } while (updatedIndex != end);
        }
        _currentIndex = updatedIndex;
    }
    _afterTokenTransfers(address(0), to, startTokenId, quantity);
}

KANJIDROPERC721A.sol

The drop contract is based on the dropERC721.sol from thirdweb see reference here, we added the extension ERC721A and modified some functions. It allows the brand make direct sales of tokens, the real tokens metadata can be encrypted and stored in encryptedBaseURI mapping, encrypted by password until the brand reveals them with same password (when reveal() function is called). The tokens are lazyminted and only minted during purchase by the brand's client. It is possible to fix different “claim phases”. A claimPhases is a time window during wich a batch of tokens is sold for a specific price (pricePerToken). It is defined by several parameter (visible in structure ClaimCondition) This contract also implements whitelisting by merkle tree.

To perform encryption we use the following function:

let password = "password"
let tokenToMint = 1000;
let hidedUri = "ipfs://ipfsHash/mybaseuri/";
let fakeURI = "ipfs://ipfsHash/";

//Retrieve next token id to mint
let nextTokenId = await this.KANJIDROPERC721A.nextTokenIdToMint({from: accounts[0]});
//hash with (nextTokenId+tokenToLazyMint , password , address , blockchainId)
let hash = await hashDelayRevealPasword(parseInt(nextTokenId+"")+tokenToMint,password,this.KANJIDROPERC721A.address);
//use encryptDecrypt function of drop contract
const encryptedBaseUri = await this.KANJIDROPERC721A.encryptDecrypt(
    ethers.utils.toUtf8Bytes(
        hidedUri,
    ),
    hash
);
//call lazy mint function of drop contract and send it encryptedBaseUri 
await this.KANJIDROPERC721A.lazyMint(
    tokenToMint,
    fakeURI,
    encryptedBaseUri,
    {from: accounts[0]}
)

To perform reveal we decrypt with password and test validity :

let password = "password"

//call the index of the base uri you want to decrypt 
let indexEncrypt = await this.KANJIDROPERC721A.baseURIIndices(1,{from: accounts[0]});
//retrieve the encrypted baseURI corresponding to this index
let encryptedBaseURI = await this.KANJIDROPERC721A.encryptedBaseURI(indexEncrypt+"",{from: accounts[0]});
//retrieve hash with (indexEncrypt , password , address , blockchainId)
let hash = await hashDelayRevealPasword(indexEncrypt+"",password,this.KANJIDROPERC721A.address);
//call contract drop function to decrypt hashed URI
let decryptedUri = await this.KANJIDROPERC721A.encryptDecrypt(
    encryptedBaseURI,
    hash,
);
//decode hex to ascii
decryptedUri =  web3.utils.hexToAscii(decryptedUri)
//test validity of address uri decrypted
if (!decryptedUri.includes("://") || !decryptedUri.endsWith("/")) {
    throw new Error("invalid password");
}
//if all is valid, reveal with this hash
await this.KANJIDROPERC721A.reveal(
    1,//Index of baseURIIndices to reveal
    hash,
    {from: accounts[0]}
)

Function hashDelayedRevealPassword

async function hashDelayRevealPasword(
    batchTokenIndex,//Key of array encryptedURI mapping
    password,//Password for reveal
    contractAddress//Address of drop contract
) {
    const chainId = await web3.eth.getChainId();
    return ethers.utils.solidityKeccak256(
        ["string", "uint256", "uint256", "address"],
        [password, chainId, batchTokenIndex+"", contractAddress],
    );
}

SETTERS

function lazyMint (onlyowner)

Lazymint one or more tokens to be sold with their metadatas, if the variable _encryptedBaseURI is set then the real metadatas will be hidden, until reveal is done.

function reveal (onlyowner)

Reveals a previously hidden set of tokens metadatas by decrypting the relevant encrypted URI found in encryptedBaseURI, moving it to baseURI and then deleting it from encryptedBaseURI.

function setClaimConditions (onlyowner)

Update the claimCondition list. You can add a merkle root in the claim conditions that will force brand's client to send a merkle proof when claiming a token.

MERKLE ROOT EXEMPLE

///Create a merkle tree
const hashedLeaves = whitelistedAddress.map((i) =>
    hashLeafNode(
    i,
    0,
    )
);
this.tree = new MerkleTree(hashedLeaves, keccak256, {
    sort: true,
});
///Function hashLeafNode used for create leaves of merkle tree
function hashLeafNode(
    address,
    maxClaimableAmount,
) {
    return ethers.utils.solidityKeccak256(
        ["address", "uint256"],
        [address, BigNumber.from(maxClaimableAmount)],
    );
}

setClaimConditions with merkle tree exemple

const claimConditions = [
    {
        startTimestamp : Math.floor(Date.now()/1000)-5,
        maxClaimableSupply : 200,//bigger than last 
        supplyClaimed : 0,
        quantityLimitPerTransaction : 100,
        waitTimeInSecondsBetweenClaims : 10,
        merkleRoot : this.tree.getHexRoot(),
        pricePerToken : 100,
        currency : "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",//MATIC
    }
]
await this.buyReveal.setClaimConditions(claimConditions, {from: accounts[0]})

function claim

Claim a token offered for sale, sending the amount of tokens according to the current ClaimCondition (according to the current date), mint the requested number of tokens to the brand's client, then send the funds to the beneficiaries. If the ClaimCondition contains a merkle root, then you will need to send a hexadecimal proof of his address.

CLAIM WITH WHITELIST EXEMPLE

const expectedProof = this.tree.getHexProof(
    ethers.utils.solidityKeccak256(["address", "uint256"], [accountClaimer, 0]),
);

await this.buyReveal.claim(
    accountClaimer,
    1,
    "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",///Native Token
    100,
    expectedProof,
    0,
    {from: accountClaimer,value:100}
).catch(() => {
    console.log("whitelist not approved")
});

function setWalletClaimCount (onlyowner)

Define a quantity of claimable tokens for a wallet address.

function setMaxWalletClaimCount (onlyowner)

Let the contract admin (the brand) set a maximum number of NFTs that can be claimed by any wallet.

function setMaxTotalSupply (onlyowner)

Let the brand sets the global maximum supply of NFTs in the collection.

function collectClaimPrice (internal)

This internal function will send the revenues of the sales to the different beneficiaries, as well the share of kanji to its address.

function transferClaimedTokens (internal)

Once a claim is done and validated, the tokens are minted directly to the claimer.

GETTERS

function verifyClaim (public)

Allows to verify that a claimer is authorized to claim tokens and verify that the claim quantity is not exceeded and that the conditions are respected.

function verifyClaimMerkleProof (public)

Allows to verify that a claimer is authorized to claim tokens and checks the merkle proof if the claim condition has a merkle root.

function getActiveClaimConditionId

Retrieves the id of the current condition.

function getClaimTimestamp

Verifies that the latency between two claim calls is respected for a given wallet address.

function getClaimConditionById

Get the parameters of a claimCondition with respect to its id.

function contractURI

The function that returns the contract level metadata for opensea.

function tokenURI

The function that returns the metadata of a token, if the encryptedBaseURI mapping linked to the token is populated and the token is not revealed, the metadata returned is the fake URI (until the token is revealed).

KANJIDROPERC1155

The drop contract is based on the dropERC1155.sol from thirdweb see reference here.

It allows the brand make direct sales of tokens, the real tokens metadata can be encrypted and stored in encryptedBaseURI mapping, encrypted by password until the brand reveals them with same password (when reveal() function is called). The tokens are lazyminted and only minted during purchase by the brand's client. It is possible to fix different “claim phases”. A claimPhases is a time window during wich a batch of tokens is sold for a specific price (pricePerToken). It is defined by several parameter (visible in structure ClaimCondition) This contract also implements whitelisting by merkle tree.

KANJIDROPERC1155 contract functions are exactly the same as the KANJIDROPERC721A contract - but the difference is to be found in the number of tokenID mintable at each phase.

For the KANJIDROPERC1155, only one tokenID can be minted for a predetermined number whereas you can mint a predetermined number of tokenID at each phase for the KANJIDROPERC721A.

KANJIPHOENIX

This contract is based on ThirdWeb's ERC1155 drop contract.

We added the token burn and mint principle: an NFT owner can burn his token and redeem another one with an other ID, as seen on the Adidas original contract.

This contract aims to allow a brand to sell tokens during a period determined by the brand and then let the users who own this token update it during another period.

For a specific phase (ClaimCondition struct) the brand can set up 2 arrays cardIdToMint and cardIdToRedeem: the ids in cardIdToMint are the token ids a NFT owner can burn to redeem an oher token with an id (of his choice) in cardIdToMint

We want the brands to be as free as possible to customize parameters:

  • Possibility to make a phase exclusively accessible to pre-defined NFT owners (ERC721 or ERC1155).
  • Possibility to freeze the token if its ID is smaller than the blockLowerIdToMint variable.
  • Possibility to add a price to the burn-and-mint process (redeemTokenForOther())
  • Possibility for the user to choose the token he wants to burn in a list of cardIdToRedeem linked to the claimCondition active.
  • Possibility for the user to choose the token he wants to mint in a list of cardIdToMint linked to the claimCondition active.
  • Possibility to ask for a burn or not (if the array cardIdToRedeem linked to the active claimCondition is empty)
  • Possibility to make the user pay for a burn/mint (redeemTokenForOther()) or a mint if the variable pricePerToken linked to the claimCondition active is > 0

Most of the functions are identical to our drop contract summarized above. Here are the only added functions :

function claim

Allows a brand’s client to mint a new token, according to the current ClaimCondition (fixed by the current timestamp), in exchange of one of his token or a given price (or both), then send the funds to the beneficiaries.

  • If the active ClaimCondition contains a Merkle root, then the brand's client will need to send a hexadecimal proof of his address..

  • If cardIdToRedeem array in the ClaimCondition length is equal to 0, claim() function call mintClaimedTokens() function for automaticaly mint token.

  • If cardIdToRedeem array in the ClaimCondition length is greater than 0, claim() function call redeemTokenForOther() function to automatically burn and mint token whose id is chosen by the client in cardIdToMint of the active ClaimCondition.

And add them:

function redeemTokenForOther

This function allows the brand's customers to burn one of his token to redeem an other token.

KANJIDROPERC721R

This contract is based on ThirdWeb's ERC1155 drop contract. We added the randomization of mint of tokens with ERC721R, this allows a brand to put on sale / distribute a number of tokens determined randomly, obliging the users of the brands to claim their tokens without knowing what they are going to get.

The only differences are that the max supply must be set in the constructor and cannot be changed afterward.

And that we call the function _mintRandom() instead of calling the function _mint().

2.2.2

1 year ago