diff --git a/contracts/ProxyLayer.sol b/contracts/ProxyLayer.sol new file mode 100644 index 0000000..d612229 --- /dev/null +++ b/contracts/ProxyLayer.sol @@ -0,0 +1,88 @@ +pragma solidity ^0.5.12; + +import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC721/IERC721.sol"; + +// interface for the v2 contract +interface IAsyncArtwork_v2 { + function getControlToken(uint256 controlTokenId) + external + view + returns (int256[] memory); + function useControlToken( + uint256 controlTokenId, + uint256[] calldata leverIds, + int256[] calldata newValues + ) external payable; + function ownerOf(uint256 tokenId) external view returns (address); + function getApproved(uint256 tokenId) external view returns (address); + function isApprovedForAll(address owner, address operator) external view returns (bool); + function transferFrom(address from, address to, uint256 tokenId) external; + +} + +// Copyright (C) 2020 Asynchronous Art, Inc. +// GNU General Public License v3.0 +// Full notice https://github.com/asyncart/async-contracts/blob/master/LICENSE +contract ProxyLayer { + IAsyncArtwork_v2 public asyncArtwork_V2; + + // struct for the controlling token + // of the proxy layer + struct ControllingToken { + address tokenAddress; + uint256 tokenId; + } + + // mapping of the async artwork token id to the struct storing the + // information about the token that controls this layer + mapping(uint256 => ControllingToken) public controlledTokens; + + // an event emitted when a control layer is converted to a proxy layer + event ConvertedToProxyLayer( + uint256 asncArtworkV2TokenId, + address targetTokenAddress, + uint256 targetTokenId, + address converter + ); + + // constructor: needs the address of the async artwork v2 contract + constructor(address _asyncArtworkV2) public { + asyncArtwork_V2 = IAsyncArtwork_v2(_asyncArtworkV2); + } + + // converts a control token/layer to a proxy layer, controlled by the referenced target nft + // note: **this is a permanent process that can NOT be reversed**! + function permanentlyConvertToProxyLayer(uint256 _asycArtV2TokenId, address _targetTokenAddress, uint256 _targetTokenId) external { + // msg.sender has to be the current owner of the control token + require(asyncArtwork_V2.ownerOf(_asycArtV2TokenId) == msg.sender, "Only owner of the control token."); + + require(IERC721(_targetTokenAddress).ownerOf(_targetTokenId) != address(0), "Target token doesn't exist."); + + // has to be a valid control token (correctly set up) + int256[] memory controlLevers = asyncArtwork_V2.getControlToken(_asycArtV2TokenId); + require(controlLevers.length > 0, "Only a correctly set up control token."); + + // control token can't be a proxy layer already + require(controlledTokens[_asycArtV2TokenId].tokenAddress == address(0), "Can only be converted once."); + + // the target token is registered as the controlling token for the layer + controlledTokens[_asycArtV2TokenId] = ControllingToken(_targetTokenAddress, _targetTokenId); + + // the control token is transferred here + asyncArtwork_V2.transferFrom(msg.sender, address(this), _asycArtV2TokenId); + + emit ConvertedToProxyLayer(_asycArtV2TokenId, _targetTokenAddress, _targetTokenId, msg.sender); + } + + // this function allows the current NFT holder to control the underlying control layer within this proxy mechanism + function useProxyLayer(uint256 _asycArtV2TokenId, uint256[] calldata leverIds, int256[] calldata newValues) external payable { + // msg.sender has to be the owner or approved by the owner + IERC721 token = IERC721(controlledTokens[_asycArtV2TokenId].tokenAddress); + uint256 tokenId = controlledTokens[_asycArtV2TokenId].tokenId; + address tokenOwner = token.ownerOf(tokenId); + require(tokenOwner == msg.sender || token.isApprovedForAll(tokenOwner, msg.sender) || token.getApproved(controlledTokens[_asycArtV2TokenId].tokenId) == msg.sender, "Only the NFT owner or an approved address can use the proxy layer."); + + // Relay the control token function + asyncArtwork_V2.useControlToken.value(msg.value)(_asycArtV2TokenId, leverIds, newValues); + } +} diff --git a/contracts/TestERC721.sol b/contracts/TestERC721.sol new file mode 100644 index 0000000..b3ec31c --- /dev/null +++ b/contracts/TestERC721.sol @@ -0,0 +1,10 @@ +pragma solidity ^0.5.0; + +import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC721/ERC721Mintable.sol"; + +contract TestERC721 is ERC721Mintable { + function initialize() public initializer { + ERC721.initialize(); + ERC721Mintable.initialize(msg.sender); + } +} diff --git a/test/async-proxy-layer-test.js b/test/async-proxy-layer-test.js new file mode 100644 index 0000000..b319b63 --- /dev/null +++ b/test/async-proxy-layer-test.js @@ -0,0 +1,489 @@ +const { + BN, + expectRevert, + ether, + expectEvent, + balance, + time, +} = require("@openzeppelin/test-helpers"); +const { + artifacts +} = require("hardhat"); +const { + expect +} = require("chai"); + +const AsyncArtwork_v2 = artifacts.require("AsyncArtwork_v2"); +const ProxyLayer = artifacts.require("ProxyLayer"); +const NFT = artifacts.require("TestERC721"); + +contract("Async art", (accounts) => { + let asyncContract; + let proxyLayer; + let nft1; + let nft2; + + const admin = accounts[0]; + const user1 = accounts[1]; + const user2 = accounts[2]; + + let title = "Async Art"; + let symbol = "ASYNC"; + + beforeEach(async () => { + asyncContract = await AsyncArtwork_v2.new({ + from: admin + }); + + await asyncContract.setup(title, symbol, 1, admin, { + from: admin, + }); + + proxyLayer = await ProxyLayer.new(asyncContract.address, { + from: admin + }); + + nft1 = await NFT.new({ + from: admin + }); + await nft1.initialize({ + from: admin, + }); + + nft2 = await NFT.new({ + from: admin + }); + + await nft2.initialize({ + from: admin, + }); + }); + + it("successfully initialises", async () => { + console.log("hello"); + ////////////////////////////////// + /////////////// NAMES //////////// + ////////////////////////////////// + + let user1 = accounts[1]; + let user2 = accounts[2]; + let user3 = accounts[3]; + let user4 = accounts[4]; + let user5 = accounts[5]; + let user6 = accounts[6]; + let user7 = accounts[7]; + let user8 = accounts[8]; + let user9 = accounts[9]; + + let masterToken1 = 1; // 1st Master token + let token2 = 2; + let token3 = 3; + let token4 = 4; + let token5 = 5; + let token6 = 6; + let masterToken7 = 7; // 2nd Master token + let token8 = 8; + let token9 = 9; + let token10 = 10; + + ////////////////////////////////// + ////////// Whitelists //////////// + ////////////////////////////////// + console.log("Whitelist new tokens"); + + await asyncContract.updateMinterAddress(admin, { + from: admin + }); + // params: creator, mastertokenId, layerCount, platformFirstSalePercentage, platformSecondSalePercentage + await asyncContract.whitelistTokenForCreator( + user1, + masterToken1, + 5, + 15, + 10, { + from: admin, + } + ); + + await asyncContract.whitelistTokenForCreator( + user2, + masterToken7, + 3, + 12, + 8, { + from: admin, + } + ); + + // 30 aritsts + await asyncContract.whitelistTokenForCreator(user2, 11, 30, 12, 8, { + from: admin, + }); + + // 30 artists + await asyncContract.whitelistTokenForCreator(user2, 42, 30, 12, 8, { + from: admin, + }); + + ////////////////////////////////// + ////////// Minting tokens //////// + ////////////////////////////////// + + console.log("Minting tokens"); + + let userArray = [ + user2, + user3, + user3, + user4, + user5, + user5, + user5, + user6, + user8, + user9, + user2, + user3, + user3, + user4, + user5, + user5, + user5, + user6, + user8, + user9, + user2, + user3, + user3, + user4, + user5, + user5, + user5, + user6, + user8, + user9, + ]; + + // User 1 mints his artwork + await asyncContract.mintArtwork( + masterToken1, + "DATA", + [user3, user3, user3, user3, user4], + [user3, user4], { + from: user1 + } + ); + + // User 2 mints his artwork + await asyncContract.mintArtwork( + masterToken7, + "RANDOMDATA", + [user2, user3, user3], + [user2, user3], { + from: user2, + } + ); + + await asyncContract.mintArtwork( + 11, + "RANDOMDATA", + userArray, + [user2, user3, user4, user5, user6, user8, user9], { + from: user2, + } + ); + + await asyncContract.mintArtwork( + 42, + "RANDOMDATA", + userArray, + [user2, user3, user4, user5, user6, user8, user9], { + from: user2, + } + ); + + // await asyncContract.mintArtworkOptimised2( + // 42, + // "RANDOMDATA", + // userArray, + // [user2, user3, user4, user5, user6, user8, user9], + // { + // from: user2, + // } + // ); + + /////////////////////////////////////////////// + ////////// Admin functions called ///////////// + /////////////////////////////////////////////// + console.log("Calling admin functions"); + + await asyncContract.setExpectedTokenSupply(11, { + from: admin, + }); + + await asyncContract.updatePlatformAddress(user9, { + from: admin, + }); + + await asyncContract.updatePlatformAddress(admin, { + from: user9, + }); + + await asyncContract.waiveFirstSaleRequirement( + [masterToken1, token2, token3], { + from: admin, + } + ); + + await asyncContract.waiveFirstSaleRequirement([token2, token3, token4], { + from: admin, + }); + + await asyncContract.updatePlatformSalePercentage(masterToken1, 9, 7, { + from: admin, + }); + + await asyncContract.updateMinimumBidIncreasePercent(2, { + from: admin, + }); + + // await asyncContract.updateTokenURI(token2, "NEWRANDOM", { + // from: admin, + // }); + + // await asyncContract.lockTokenURI(token2, { + // from: admin, + // }); + + await asyncContract.updateArtistSecondSalePercentage(6, { + from: admin, + }); + + ///////////////////////////////////////// + ///// Setting up control levers ///////// + ///////////////////////////////////////// + console.log("Control lever set up"); + + await asyncContract.setupControlToken( + token2, + "randomURI", + [0, 1, 2], + [10, 11, 12], + [5, 6, 7], + 30, + [user8], { + from: user3, + } + ); + + await asyncContract.setupControlToken( + token6, + "randomURI2", + [0, 0], + [100, 100], + [50, 51], + 10, + [], { + from: user4, + } + ); + + ////////////////////////////////// + //// more grantControlPermission ////// + ////////////////////////////////// + console.log("Granting more control permissions"); + + await asyncContract.grantControlPermission(masterToken1, user8, { + from: user6, + }); + + ///////////////////////////// + ////////// Bids ///////////// + ///////////////////////////// + console.log("Making bids"); + + await asyncContract.bid(masterToken1, { + from: user5, + value: "1000000000", + }); + + await asyncContract.bid(masterToken1, { + from: user6, + value: "2000000000", + }); + + await asyncContract.withdrawBid(masterToken1, { + from: user6, + }); + + await asyncContract.bid(masterToken1, { + from: user6, + value: "2500000000", + }); + + await asyncContract.acceptBid(masterToken1, "2000000000", { + from: user1, + }); + + ///////////////////////////// + ////////// Buys ///////////// + ///////////////////////////// + console.log("Making buys"); + + await asyncContract.makeBuyPrice(token2, "3000000", { + from: user3, + }); + + await asyncContract.makeBuyPrice(token2, "2000000", { + from: user3, + }); + + await asyncContract.makeBuyPrice(token2, "1000000", { + from: user3, + }); + + await asyncContract.takeBuyPrice(token2, 30, { + from: user6, + value: "1000000", + }); + + ////////////////////////////////// + //// grantControlPermission ////// + ////////////////////////////////// + console.log("Granting control permissions"); + + await asyncContract.grantControlPermission(token9, user8, { + from: user3, + }); + + await asyncContract.grantControlPermission(token10, user7, { + from: user3, + }); + + // Should handel duplicates + await asyncContract.grantControlPermission(token10, user7, { + from: user3, + }); + + /////////////////////////////////////////////// + ////////// Control tokens used //////////////// + /////////////////////////////////////////////// + console.log("Using control tokens"); + + await asyncContract.useControlToken(token2, [0, 1, 2], [8, 8, 8], { + from: user6, + }); + + await asyncContract.useControlToken(token2, [0, 2], [9, 9], { + from: user6, + }); + + await asyncContract.useControlToken(token6, [0, 1], [21, 21], { + from: user4, + }); + + /////////////////////////////////////////////// + ////////////// Proxy Layers /////////////////// + /////////////////////////////////////////////// + + console.log("Minting demo NFTs"); + + await nft1.mint(user7, 9999, { + from: admin + }); + await nft2.mint(user4, 1337, { + from: admin + }); + + console.log("Converting to proxy layers"); + + await asyncContract.useControlToken(token2, [0, 1, 2], [5, 5, 5], { + from: user6, + }); + + await asyncContract.grantControlPermission(token2, user7, { + from: user6, + }); + + await expectRevert(proxyLayer.permanentlyConvertToProxyLayer(masterToken1, nft2.address, 1337, { + from: user6, + }), "Token does not exist."); + + await expectRevert(proxyLayer.permanentlyConvertToProxyLayer(token2, nft1.address, 9999, { + from: user4, + }), "Only owner of the control token."); + + await expectRevert(proxyLayer.permanentlyConvertToProxyLayer(token2, nft1.address, 9999, { + from: user6, + }), "ERC721: transfer caller is not owner nor approved"); + + await asyncContract.approve(proxyLayer.address, token2, { + from: user6 + }) + + expect(await asyncContract.ownerOf(token2)).to.equal(user6) + + await expectRevert(proxyLayer.permanentlyConvertToProxyLayer(token2, "0x0000000000000000000000000000000000000001", 9999, { + from: user6, + }), "function call to a non-contract account"); + + await expectRevert(proxyLayer.permanentlyConvertToProxyLayer(token2, nft1.address, 666, { + from: user6, + }), "ERC721: owner query for nonexistent token"); + + await proxyLayer.permanentlyConvertToProxyLayer(token2, nft1.address, 9999, { + from: user6, + }); + + expect(await asyncContract.ownerOf(token2)).to.equal(proxyLayer.address) + + await expectRevert(asyncContract.useControlToken(token2, [0, 1, 2], [6, 6, 6], { + from: user6, + }), "Owner or permissioned only"); + + await expectRevert(proxyLayer.useProxyLayer(token2, [0, 1, 2], [6, 6, 6], { + from: user6, + }), "Only the NFT owner or an approved address can use the proxy layer."); + + await proxyLayer.useProxyLayer(token2, [0, 1, 2], [6, 6, 6], { + from: user7, + }); + + await nft1.transferFrom(user7, user6, 9999, { + from: user7 + }); + + await expectRevert(proxyLayer.useProxyLayer(token2, [0, 1, 2], [6, 6, 6], { + from: user7, + }), "Only the NFT owner or an approved address can use the proxy layer."); + + await proxyLayer.useProxyLayer(token2, [0, 1, 2], [5, 5, 5], { + from: user6, + }); + + await expectRevert(proxyLayer.useProxyLayer(token2, [0, 1, 2], [5, 5, 5], { + from: user7, + }), "Only the NFT owner or an approved address can use the proxy layer."); + + await nft1.approve(user7, 9999, { + from: user6 + }); + + await proxyLayer.useProxyLayer(token2, [0, 1, 2], [4, 4, 4], { + from: user7, + }); + + await expectRevert(proxyLayer.useProxyLayer(token2, [0, 1, 2], [5, 5, 5], { + from: user8, + }), "Only the NFT owner or an approved address can use the proxy layer."); + + await nft1.setApprovalForAll(user8, true, { + from: user6 + }); + + await proxyLayer.useProxyLayer(token2, [0, 1, 2], [5, 5, 5], { + from: user8, + }); + + }); +}); \ No newline at end of file