diff --git a/contracts/FoodMeme.sol b/contracts/FoodMeme.sol index d722c0a..c685175 100644 --- a/contracts/FoodMeme.sol +++ b/contracts/FoodMeme.sol @@ -50,7 +50,7 @@ contract FoodMeme is MemeMetadata, OFT, AccessControl { error AlreadyInitialized(); error NotInitialized(); error NotMasterChain(); - error NotMintChain(); + error NotMintChain(uint256 chainId); error MintDisabled(); event EarlyUnlock(uint256 previousUnlockTime); @@ -88,14 +88,14 @@ contract FoodMeme is MemeMetadata, OFT, AccessControl { } } if (!isMintChain) { - revert NotMintChain(); + revert NotMintChain(block.chainid); } _; } constructor(string memory _name, string memory _symbol, address _lzEndpoint, address _delegate) - OFT(_name, _symbol, _lzEndpoint, _delegate) - Ownable(_delegate) + OFT(_name, _symbol, _lzEndpoint, _delegate) + Ownable(_delegate) { _grantRole(DEFAULT_ADMIN_ROLE, _delegate); _grantRole(ROLE_FACTORY, msg.sender); @@ -177,7 +177,7 @@ contract FoodMeme is MemeMetadata, OFT, AccessControl { if (quantity > maxPerMint) { revert PerMintAmountExceeded(); } - uint256 price = Utils.computeUnitPrice(priceSettings, quantity, totalSupply()); + uint256 price = mintPrice(quantity); if (!(msg.value >= price * quantity)) { revert InsufficientPayment(); } @@ -186,6 +186,10 @@ contract FoodMeme is MemeMetadata, OFT, AccessControl { // TODO: also note the total supply should be propagated to all chains, if we still want to have supply control - and very important to do that, in the case which the bounding curve (unit price) is linear or quadratic, } + function mintPrice(uint256 quantity) public view mintChainOnly returns (uint256) { + return Utils.computeUnitPrice(priceSettings, quantity, totalSupply(), maxSupply); + } + function hasReviewed(address user) public view masterChainOnly returns (bool) { return uint256(reviews[user].hash) == 0 && reviews[user].rating == 0; } diff --git a/contracts/FoodMemeFactory.sol b/contracts/FoodMemeFactory.sol index d25fb6a..259f3ed 100644 --- a/contracts/FoodMemeFactory.sol +++ b/contracts/FoodMemeFactory.sol @@ -59,8 +59,9 @@ contract FoodMemeFactory is Ownable { } function launch(Utils.MemeParams memory params, address maker, Utils.InitParams memory initParams) - external - payable + external + payable + returns (FoodMeme) { if (msg.value < launchFee) { revert InsufficientLaunchFee(); @@ -72,19 +73,23 @@ contract FoodMemeFactory is Ownable { address instance = Create2.deploy(0, salt, abi.encodePacked(helper.code(args))); emit MemeLaunched(params.name, params.symbol, instance, msg.sender); FoodMeme f = FoodMeme(instance); + if (bytes(initParams.baseUri).length == 0) { + initParams.baseUri = baseUrl; + } f.initialize(maker, initParams); + return f; } // called after contracts are deployed in local chain and in all remote chains - function setupLz(FoodMeme f, Utils.LzParams memory params) internal { + function setupLz(FoodMeme f, Utils.LzParams memory params) external { require(msg.sender == owner() || f.hasRole(f.ROLE_MAKER(), msg.sender), "Unauthorized"); require(params.endPointIds.length == params.remoteContractAddresses.length, "Bad lz params"); - require(params.endPointIds.length == params.receiveLibraries.length, "Bad lz params"); - require(params.endPointIds.length == params.sendLibraries.length, "Bad lz params"); - require(params.endPointIds.length == params.sendConfigParams.length, "Bad lz params"); - require(params.endPointIds.length == params.receiveConfigParams.length, "Bad lz params"); - require(params.endPointIds.length == params.enforceConfigParams.length, "Bad lz params"); - require(params.endPointIds.length == params.minGasEnforceConfig.length, "Bad lz params"); +// require(params.endPointIds.length == params.receiveLibraries.length, "Bad lz params"); +// require(params.endPointIds.length == params.sendLibraries.length, "Bad lz params"); +// require(params.endPointIds.length == params.sendConfigParams.length, "Bad lz params"); +// require(params.endPointIds.length == params.receiveConfigParams.length, "Bad lz params"); +// require(params.endPointIds.length == params.enforceConfigParams.length, "Bad lz params"); +// require(params.endPointIds.length == params.minGasEnforceConfig.length, "Bad lz params"); for (uint256 i = 0; i < params.endPointIds.length; i++) { f.setPeer(params.endPointIds[i], params.remoteContractAddresses[i]); @@ -96,13 +101,13 @@ contract FoodMemeFactory is Ownable { if (params.sendLibraries.length > i) { f.endpoint().setSendLibrary(address(f), params.endPointIds[i], params.sendLibraries[i]); } - if (params.sendConfigParams[i].config.length > 0) { + if (params.sendConfigParams.length > i && params.sendConfigParams[i].config.length > 0) { f.endpoint().setConfig(address(f), params.sendLibraries[i], params.sendConfigParams[i].config); } - if (params.receiveConfigParams[i].config.length > 0) { + if (params.receiveConfigParams.length > i && params.receiveConfigParams[i].config.length > 0) { f.endpoint().setConfig(address(f), params.receiveLibraries[i], params.receiveConfigParams[i].config); } - if (params.minGasEnforceConfig[i] > 0) { + if (params.minGasEnforceConfig.length > i && params.minGasEnforceConfig[i] > 0) { EnforcedOptionParam memory p = EnforcedOptionParam({ eid: params.endPointIds[i], msgType: 1, // OFTCore.SEND, @@ -112,7 +117,7 @@ contract FoodMemeFactory is Ownable { pp[0] = p; f.setEnforcedOptions(pp); } - if (params.enforceConfigParams[i].config.length > 0) { + if (params.enforceConfigParams.length > i && params.enforceConfigParams[i].config.length > 0) { f.setEnforcedOptions(params.enforceConfigParams[i].config); } } diff --git a/contracts/Utils.sol b/contracts/Utils.sol index ebc2a3f..49345ba 100644 --- a/contracts/Utils.sol +++ b/contracts/Utils.sol @@ -66,18 +66,18 @@ library Utils { } // TODO: check for risk of arithmetic error here. Probably switch to Q64 or Q96 - function computeUnitPrice(PriceSettings memory s, uint256 quantity, uint256 supply) - internal - pure - returns (uint256) + function computeUnitPrice(PriceSettings memory s, uint256 quantity, uint256 supply, uint256 maxSupply) + internal + pure + returns (uint256) { if (s.mode == PriceMode.ConstantPrice) { return s.c; } else if (s.mode == PriceMode.LinearPrice) { - return s.b * (quantity + supply) + s.c; + return s.b * (quantity + supply) / maxSupply / DECIMALS + s.c / DECIMALS; } uint256 newSupply = quantity + supply; - return s.a * s.a * newSupply + s.b * newSupply + s.c; + return s.a * s.a * newSupply / DECIMALS / maxSupply / DECIMALS + s.b * newSupply / maxSupply / DECIMALS + s.c / DECIMALS; } struct Review { diff --git a/remappings.txt b/remappings.txt index cc7129e..3431af7 100644 --- a/remappings.txt +++ b/remappings.txt @@ -10,4 +10,6 @@ layerzero-v2/=lib/layerzero-v2/ openzeppelin-contracts/=lib/openzeppelin-contracts/ solidity-bytes-utils/=lib/solidity-bytes-utils/ -@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib/ \ No newline at end of file +@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib/ +@layerzerolabs/test-devtools-evm-foundry=lib/devtools/packages/test-devtools-evm-foundry/ +@layerzerolabs/lz-evm-v1-0.7/contracts/interfaces/=test/mock/ \ No newline at end of file diff --git a/test/FoodMemeFactory.t.sol b/test/FoodMemeFactory.t.sol index 2384d91..f6f665d 100644 --- a/test/FoodMemeFactory.t.sol +++ b/test/FoodMemeFactory.t.sol @@ -2,12 +2,207 @@ pragma solidity ^0.8.26; import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; import {FoodMemeFactory} from "contracts/FoodMemeFactory.sol"; +import {FoodMemeFactoryHelper} from "contracts/FoodMemeFactoryHelper.sol"; +import {FoodMeme} from "contracts/FoodMeme.sol"; +import {Utils} from "contracts/Utils.sol"; -contract FoodMemeFactoryTest is Test { - // FoodMemeFactory public factory; +// OApp imports +import {OptionsBuilder} from "@layerzerolabs/oapp-evm/libs/OptionsBuilder.sol"; - function setUp() public {} +// OFT imports +import {IOFT, SendParam, OFTReceipt} from "@layerzerolabs/oft-evm/interfaces/IOFT.sol"; +import {MessagingFee, MessagingReceipt} from "@layerzerolabs/oft-evm/OFTCore.sol"; +import {OFTMsgCodec} from "@layerzerolabs/oft-evm/libs/OFTMsgCodec.sol"; +import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/libs/OFTComposeMsgCodec.sol"; +// DevTools imports +import {TestHelperOz5} from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; - function test_Increment() public {} + +contract FoodMemeFactoryTest is TestHelperOz5 { + uint256 DEPLOYER_PRIVATE_KEY = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(DEPLOYER_PRIVATE_KEY); + using OptionsBuilder for bytes; + + uint32 private aEid = 1; + uint32 private bEid = 2; + + FoodMeme private aOFT; + FoodMeme private bOFT; + + address private userA = address(0x1); + address private userB = address(0x2); + uint256 private initialBalance = 1 ether; + FoodMemeFactoryHelper helper = new FoodMemeFactoryHelper(); + + FoodMemeFactory factory1; + FoodMemeFactory factory2; + + function setUp() public virtual override { + vm.deal(userA, 1000 ether); + vm.deal(userB, 1000 ether); + + super.setUp(); + setUpEndpoints(2, LibraryType.UltraLightNode); + + vm.startPrank(deployer); + vm.chainId(31337); + factory1 = new FoodMemeFactory(helper, "1"); + vm.chainId(31338); + factory2 = new FoodMemeFactory(helper, "2"); + + vm.chainId(31337); + uint256[] memory mintChains = new uint256[](1); + mintChains[0] = block.chainid; + console.log("Current chain: %s", block.chainid); + aOFT = factory1.launch( + Utils.MemeParams({name: "a", symbol: "A", endpoint: address(endpoints[aEid])}), + userA, + Utils.InitParams({ + mintable: true, + imageHash: bytes32(0), + recipeHash: bytes32(0), + mintChains: mintChains, + masterChain: block.chainid, + maxSupply: 0, + maxPerMint: 0, + minReviewThreshold: 0, + priceSettings: Utils.PriceSettings({mode: Utils.PriceMode.LinearPrice, a: 0, b: 2, c: 1}), + baseUri: "" + }) + ); + vm.chainId(31338); + console.log("Switched to chain: %s", block.chainid); + bOFT = factory2.launch( + Utils.MemeParams({name: "b", symbol: "B", endpoint: address(endpoints[aEid])}), + userB, + Utils.InitParams({ + mintable: true, + imageHash: bytes32(0), + recipeHash: bytes32(0), + mintChains: mintChains, + masterChain: block.chainid, + maxSupply: 0, + maxPerMint: 0, + minReviewThreshold: 0, + priceSettings: Utils.PriceSettings({mode: Utils.PriceMode.LinearPrice, a: 0, b: 2, c: 2}), + baseUri: "" + }) + ); + console.log("aOFT delegate: %s", aOFT.owner()); + console.log("factory1 : %s", address(factory1)); + console.log("bOFT delegate: %s", bOFT.owner()); + console.log("factory2 : %s", address(factory2)); + { + vm.chainId(31337); + uint32[] memory endPointIds = new uint32[](1); + endPointIds[0] = bEid; + bytes32[] memory remoteContractAddresses = new bytes32[](1); + remoteContractAddresses[0] = bytes32(uint256(uint160(address(bOFT)))); + address[] memory sendLibraries = new address[](0); + address[] memory receiveLibraries = new address[](0); + uint256[] memory gracePeriods = new uint256[](0); + uint128[] memory minGasEnforceConfig = new uint128[](1); + minGasEnforceConfig[0] = 100000; + Utils.LzSetConfigParam[] memory sendConfigParams = new Utils.LzSetConfigParam[](0); + Utils.LzSetConfigParam[] memory receiveConfigParams = new Utils.LzSetConfigParam[](0); + Utils.LzEnforcedOptionParam[] memory enforceConfigParams = new Utils.LzEnforcedOptionParam[](0); + + Utils.LzParams memory lzParams = Utils.LzParams({ + endPointIds: endPointIds, + remoteContractAddresses: remoteContractAddresses, + sendLibraries: sendLibraries, + receiveLibraries: receiveLibraries, + gracePeriods: gracePeriods, + minGasEnforceConfig: minGasEnforceConfig, + sendConfigParams: sendConfigParams, + receiveConfigParams: receiveConfigParams, + enforceConfigParams: enforceConfigParams + }); + factory1.setupLz(aOFT, lzParams); + } + { + vm.chainId(31338); + uint32[] memory endPointIds = new uint32[](1); + endPointIds[0] = aEid; + bytes32[] memory remoteContractAddresses = new bytes32[](1); + remoteContractAddresses[0] = bytes32(uint256(uint160(address(aOFT)))); + address[] memory sendLibraries = new address[](0); + address[] memory receiveLibraries = new address[](0); + uint256[] memory gracePeriods = new uint256[](0); + uint128[] memory minGasEnforceConfig = new uint128[](1); + minGasEnforceConfig[0] = 100000; + Utils.LzSetConfigParam[] memory sendConfigParams = new Utils.LzSetConfigParam[](0); + Utils.LzSetConfigParam[] memory receiveConfigParams = new Utils.LzSetConfigParam[](0); + Utils.LzEnforcedOptionParam[] memory enforceConfigParams = new Utils.LzEnforcedOptionParam[](0); + + Utils.LzParams memory lzParams = Utils.LzParams({ + endPointIds: endPointIds, + remoteContractAddresses: remoteContractAddresses, + sendLibraries: sendLibraries, + receiveLibraries: receiveLibraries, + gracePeriods: gracePeriods, + minGasEnforceConfig: minGasEnforceConfig, + sendConfigParams: sendConfigParams, + receiveConfigParams: receiveConfigParams, + enforceConfigParams: enforceConfigParams + }); + factory2.setupLz(bOFT, lzParams); + } + + vm.chainId(31337); + uint256 aPrice = aOFT.mintPrice(initialBalance); + console.log("aPrice %s", aPrice); + vm.chainId(31337); + aOFT.mint{value: aPrice * initialBalance}(initialBalance, userA); + console.log("a minted %s", aOFT.balanceOf(userA)); + vm.chainId(31337); + uint256 bPrice = bOFT.mintPrice(initialBalance); + console.log("bPrice %s", bPrice); + vm.chainId(31338); + vm.expectRevert(); + bOFT.mint{value: bPrice * initialBalance}(initialBalance, userB); + vm.chainId(31337); + bOFT.mint{value: bPrice * initialBalance}(initialBalance, userB); + console.log("aOFT owner after setup: %s", aOFT.owner()); + console.log("bOFT after setup: %s", bOFT.owner()); + vm.stopPrank(); + } + + function test_constructor() public { + assertEq(aOFT.owner(), deployer); + assertEq(bOFT.owner(), deployer); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + assertEq(aOFT.token(), address(aOFT)); + assertEq(bOFT.token(), address(bOFT)); + } +// +// function test_send_oft() public { +// uint256 tokensToSend = 1 ether; +// bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); +// SendParam memory sendParam = SendParam( +// bEid, +// addressToBytes32(userB), +// tokensToSend, +// tokensToSend, +// options, +// "", +// "" +// ); +// MessagingFee memory fee = aOFT.quoteSend(sendParam, false); +// +// assertEq(aOFT.balanceOf(userA), initialBalance); +// assertEq(bOFT.balanceOf(userB), initialBalance); +// +// vm.prank(userA); +// aOFT.send{value: fee.nativeFee}(sendParam, fee, payable(address(this))); +// verifyPackets(bEid, addressToBytes32(address(bOFT))); +// +// assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); +// assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend); +// } } diff --git a/test/mock/ILayerZeroUltraLightNodeV2.sol b/test/mock/ILayerZeroUltraLightNodeV2.sol new file mode 100644 index 0000000..35720e5 --- /dev/null +++ b/test/mock/ILayerZeroUltraLightNodeV2.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity >=0.7.0; +pragma abicoder v2; + +interface ILayerZeroUltraLightNodeV2 { + // Relayer functions + function validateTransactionProof( + uint16 _srcChainId, + address _dstAddress, + uint _gasLimit, + bytes32 _lookupHash, + bytes32 _blockData, + bytes calldata _transactionProof + ) external; + + // an Oracle delivers the block data using updateHash() + function updateHash(uint16 _srcChainId, bytes32 _lookupHash, uint _confirmations, bytes32 _blockData) external; + + // can only withdraw the receivable of the msg.sender + function withdrawNative(address payable _to, uint _amount) external; + + function withdrawZRO(address _to, uint _amount) external; + + // view functions + function getAppConfig( + uint16 _remoteChainId, + address _userApplicationAddress + ) external view returns (ApplicationConfiguration memory); + + function accruedNativeFee(address _address) external view returns (uint); + + struct ApplicationConfiguration { + uint16 inboundProofLibraryVersion; + uint64 inboundBlockConfirmations; + address relayer; + uint16 outboundProofType; + uint64 outboundBlockConfirmations; + address oracle; + } + + event HashReceived( + uint16 indexed srcChainId, + address indexed oracle, + bytes32 lookupHash, + bytes32 blockData, + uint confirmations + ); + event RelayerParams(bytes adapterParams, uint16 outboundProofType); + event Packet(bytes payload); + event InvalidDst( + uint16 indexed srcChainId, + bytes srcAddress, + address indexed dstAddress, + uint64 nonce, + bytes32 payloadHash + ); + event PacketReceived( + uint16 indexed srcChainId, + bytes srcAddress, + address indexed dstAddress, + uint64 nonce, + bytes32 payloadHash + ); + event AppConfigUpdated(address indexed userApplication, uint indexed configType, bytes newConfig); + event AddInboundProofLibraryForChain(uint16 indexed chainId, address lib); + event EnableSupportedOutboundProof(uint16 indexed chainId, uint16 proofType); + event SetChainAddressSize(uint16 indexed chainId, uint size); + event SetDefaultConfigForChainId( + uint16 indexed chainId, + uint16 inboundProofLib, + uint64 inboundBlockConfirm, + address relayer, + uint16 outboundProofType, + uint64 outboundBlockConfirm, + address oracle + ); + event SetDefaultAdapterParamsForChainId(uint16 indexed chainId, uint16 indexed proofType, bytes adapterParams); + event SetLayerZeroToken(address indexed tokenAddress); + event SetRemoteUln(uint16 indexed chainId, bytes32 uln); + event SetTreasury(address indexed treasuryAddress); + event WithdrawZRO(address indexed msgSender, address indexed to, uint amount); + event WithdrawNative(address indexed msgSender, address indexed to, uint amount); +}