Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: Shop Chests #11

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ffb978e
buy and sell chests
dhvanipa Jun 1, 2024
ff5e31a
update
dhvanipa Jun 3, 2024
c535f89
new structure
dhvanipa Jun 4, 2024
d815ade
update buy chest
dhvanipa Jun 4, 2024
87f9887
withdraw
dhvanipa Jun 4, 2024
90b4d44
fix
dhvanipa Jun 4, 2024
f3f6d1a
fix
dhvanipa Jun 4, 2024
824b3e6
only buy full durability tools
dhvanipa Jun 5, 2024
537d882
only sell full durability tools
dhvanipa Jun 5, 2024
dc86257
add getters for displaying all chests
dhvanipa Jun 6, 2024
722d7c9
change to check if its setup
dhvanipa Jun 6, 2024
d00e066
fix bug
dhvanipa Jun 6, 2024
f5f4602
add fee
dhvanipa Jun 7, 2024
cd5eae0
change fee to require
dhvanipa Jun 7, 2024
ef255ff
change withdraw to fees only for buy chest
dhvanipa Jun 7, 2024
86b17a1
fix fees for buy chest
dhvanipa Jun 7, 2024
376a326
change fee
dhvanipa Jun 8, 2024
d59b80f
add getter with chest entity id
dhvanipa Jun 8, 2024
7ae6d24
add hook removed
dhvanipa Jun 9, 2024
d709ddf
add location
dhvanipa Jun 9, 2024
15c182e
deploy garnet
dhvanipa Jun 11, 2024
e8a6ad4
deploy redstone
dhvanipa Jun 12, 2024
9ab26a8
buy sell chests both
dhvanipa Jun 13, 2024
f3c9fb7
deploy garnet redstone
dhvanipa Jun 13, 2024
c15c3e6
fix buy sell chest
dhvanipa Jun 14, 2024
e750305
bonding curve chest
dhvanipa Jun 18, 2024
0ce076a
remove unused
dhvanipa Jun 18, 2024
22535f8
update
dhvanipa Jun 18, 2024
b300490
token getter
dhvanipa Jun 18, 2024
80bd91b
remove
dhvanipa Jun 18, 2024
9c9f0b5
fix
dhvanipa Jun 18, 2024
275b70a
update shops to allow owner
dhvanipa Jun 19, 2024
418527a
Fix token formula
dhvanipa Jun 19, 2024
7ea014e
start tokenized shop
dhvanipa Jun 19, 2024
e302b17
fix token calc
dhvanipa Jun 19, 2024
b88803f
add getters
dhvanipa Jun 20, 2024
9605b59
update to use chip
dhvanipa Jun 21, 2024
960198d
add simple owned chest
dhvanipa Jun 21, 2024
89b0c10
add owner getter
dhvanipa Jun 21, 2024
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
255 changes: 255 additions & 0 deletions packages/hardhat/contracts/BondingCurveChest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { IERC165 } from "@latticexyz/store/src/IERC165.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { IChip } from "@biomesaw/world/src/prototypes/IChip.sol";
import { PlayerObjectID } from "@biomesaw/world/src/ObjectTypeIds.sol";
import { MAX_CHEST_INVENTORY_SLOTS } from "@biomesaw/world/src/Constants.sol";

import { getObjectType, getDurability, getNumUsesLeft, getPlayerFromEntity, getPosition, getCount, getStackable } from "../utils/EntityUtils.sol";
import { ShopData, FullShopData } from "../utils/ShopUtils.sol";

import { IShopToken } from "./IShopToken.sol";

// Players send it items, and are given a token in return.
// Players send it token, and are given items in return.
// Price is determined by a bonding curve.
contract BondingCurveChest is IChip, Ownable {
address public immutable biomeWorldAddress;

// Note: for now, we only support shops buying/selling one type of object.
mapping(bytes32 => ShopData) private buyShopData;
mapping(bytes32 => ShopData) private sellShopData;
mapping(bytes32 => address) private chestOwner;
mapping(address => bytes32[]) private ownedChests;

mapping(uint8 => address) private objectToToken;

constructor(address _biomeWorldAddress) Ownable(msg.sender) {
biomeWorldAddress = _biomeWorldAddress;

// Set the store address, so that when reading from MUD tables in the
// Biomes world, we don't need to pass the store address every time.
StoreSwitch.setStoreAddress(_biomeWorldAddress);
}

modifier onlyBiomeWorld() {
require(msg.sender == biomeWorldAddress, "Caller is not the Biomes World contract");
_; // Continue execution
}

function updateObjectToToken(uint8 objectTypeId, address tokenAddress) external onlyOwner {
objectToToken[objectTypeId] = tokenAddress;
}

function safeAddOwnedChest(address player, bytes32 chestEntityId) internal {
for (uint i = 0; i < ownedChests[player].length; i++) {
if (ownedChests[player][i] == chestEntityId) {
return;
}
}
ownedChests[player].push(chestEntityId);
}

function removeOwnedChest(address player, bytes32 chestEntityId) internal {
bytes32[] storage chests = ownedChests[player];
for (uint i = 0; i < chests.length; i++) {
if (chests[i] == chestEntityId) {
chests[i] = chests[chests.length - 1];
chests.pop();
return;
}
}
}

function onAttached(bytes32 playerEntityId, bytes32 chestEntityId) external onlyBiomeWorld {
address owner = getPlayerFromEntity(playerEntityId);
chestOwner[chestEntityId] = owner;

buyShopData[chestEntityId] = ShopData({ objectTypeId: 0, price: 0 });
sellShopData[chestEntityId] = ShopData({ objectTypeId: 0, price: 0 });
safeAddOwnedChest(owner, chestEntityId);
}

function onDetached(bytes32 playerEntityId, bytes32 chestEntityId) external onlyBiomeWorld {
address previousOwner = chestOwner[chestEntityId];
buyShopData[chestEntityId] = ShopData({ objectTypeId: 0, price: 0 });
sellShopData[chestEntityId] = ShopData({ objectTypeId: 0, price: 0 });
removeOwnedChest(previousOwner, chestEntityId);
chestOwner[chestEntityId] = address(0);
}

function onPowered(bytes32 playerEntityId, bytes32 entityId, uint16 numBattery) external onlyBiomeWorld {}

function onChipHit(bytes32 playerEntityId, bytes32 entityId) external onlyBiomeWorld {}

function setupChest(bytes32 chestEntityId, uint8 objectTypeId) external {
address owner = chestOwner[chestEntityId];
require(owner == msg.sender, "Only the owner can set up the chest");

buyShopData[chestEntityId] = ShopData({ objectTypeId: objectTypeId, price: 0 });
sellShopData[chestEntityId] = ShopData({ objectTypeId: objectTypeId, price: 0 });

require(objectToToken[objectTypeId] != address(0), "Token not set up");
}

function destroyChest(bytes32 chestEntityId, uint8 objectTypeId) external {
address owner = chestOwner[chestEntityId];
require(owner == msg.sender, "Only the owner can destroy the chest");
require(buyShopData[chestEntityId].objectTypeId == objectTypeId, "Chest is not set up");
require(sellShopData[chestEntityId].objectTypeId == objectTypeId, "Chest is not set up");

buyShopData[chestEntityId] = ShopData({ objectTypeId: 0, price: 0 });
sellShopData[chestEntityId] = ShopData({ objectTypeId: 0, price: 0 });
}

function blocksToTokens(
uint16 supply,
uint8 transferObjectTypeId,
uint16 transferAmount,
bool isDeposit
) internal view returns (uint256) {
// Constant that adjusts the base rate of tokens per block
uint256 k = 10 * 10 ** 18;

// Constant that controls how the reward rate decreases as the chest fills up
// uint256 alpha = 0.001;

uint256 maxItemsInChest = getStackable(transferObjectTypeId) * MAX_CHEST_INVENTORY_SLOTS;

// Cumulatively sum the supply as it increases
uint256 tokens = 0;
for (uint16 i = 0; i < transferAmount; i++) {
// Map supply to the range of 0 -- 10x10^10
uint256 scaledSupply = (uint256(supply) * 10 ** 10) / maxItemsInChest;

tokens += (k * 1) / (1 + (scaledSupply / 1000));

if (isDeposit) {
supply++;
} else {
if (supply > 0) {
supply--;
}
}
}

return tokens;
}

function onTransfer(
bytes32 srcEntityId,
bytes32 dstEntityId,
uint8 transferObjectTypeId,
uint16 numToTransfer,
bytes32 toolEntityId,
bytes memory extraData
) external payable onlyBiomeWorld returns (bool) {
bool isDeposit = getObjectType(srcEntityId) == PlayerObjectID;
bytes32 chestEntityId = isDeposit ? dstEntityId : srcEntityId;
ShopData storage chestShopData = isDeposit ? buyShopData[chestEntityId] : sellShopData[chestEntityId];
if (chestShopData.objectTypeId != transferObjectTypeId) {
return false;
}
if (toolEntityId != bytes32(0)) {
require(
getNumUsesLeft(toolEntityId) == getDurability(chestShopData.objectTypeId),
"Tool must have full durability"
);
}

address owner = chestOwner[chestEntityId];
require(owner != address(0), "Chest does not exist");
uint16 currentSupplyInChest = getCount(chestEntityId, transferObjectTypeId);
// At this point, the supply has already been updated, so we need to adjust it
if (isDeposit) {
currentSupplyInChest -= numToTransfer;
} else {
currentSupplyInChest += numToTransfer;
}

address tokenAddress = objectToToken[transferObjectTypeId];
require(tokenAddress != address(0), "Token not set up");
address player = getPlayerFromEntity(isDeposit ? srcEntityId : dstEntityId);
require(player != address(0), "Player does not exist");

uint256 blockTokens = blocksToTokens(currentSupplyInChest, transferObjectTypeId, numToTransfer, isDeposit);

if (isDeposit) {
IShopToken(tokenAddress).mint(player, blockTokens);
} else {
// Note: ERC20 will check if the player has enough tokens
IShopToken(tokenAddress).burn(player, blockTokens);
}

return true;
}

function supportsInterface(bytes4 interfaceId) external view override returns (bool) {
return interfaceId == type(IChip).interfaceId || interfaceId == type(IERC165).interfaceId;
}

function getBuyShopData(bytes32 chestEntityId) public view returns (ShopData memory) {
ShopData memory buyData = buyShopData[chestEntityId];
buyData.price = blocksToTokens(getCount(chestEntityId, buyData.objectTypeId), buyData.objectTypeId, 1, true);
return buyData;
}

function getSellShopData(bytes32 chestEntityId) public view returns (ShopData memory) {
ShopData memory sellData = sellShopData[chestEntityId];
sellData.price = blocksToTokens(getCount(chestEntityId, sellData.objectTypeId), sellData.objectTypeId, 1, false);
return sellData;
}

function getOwnedChests(address player) external view returns (bytes32[] memory) {
return ownedChests[player];
}

function getFullShopData(bytes32 chestEntityId) public view returns (FullShopData memory) {
return
FullShopData({
chestEntityId: chestEntityId,
buyShopData: getBuyShopData(chestEntityId),
sellShopData: getSellShopData(chestEntityId),
balance: 0,
location: getPosition(chestEntityId)
});
}

function getFullShopData(address player) external view returns (FullShopData[] memory) {
bytes32[] memory chestEntityIds = ownedChests[player];
FullShopData[] memory fullShopData = new FullShopData[](chestEntityIds.length);

for (uint i = 0; i < chestEntityIds.length; i++) {
bytes32 chestEntityId = chestEntityIds[i];
address owner = chestOwner[chestEntityId];
fullShopData[i] = getFullShopData(chestEntityId);
}

return fullShopData;
}

function getTokens() external view returns (address[] memory) {
address[] memory tokens = new address[](256);
uint16 numTokens = 0;
for (uint16 i = 0; i < 256; i++) {
address token = objectToToken[uint8(i)];
if (token != address(0)) {
tokens[numTokens] = token;
numTokens++;
}
}
address[] memory result = new address[](numTokens);
for (uint16 i = 0; i < numTokens; i++) {
result[i] = tokens[i];
}
return result;
}

function getOwner(bytes32 chestEntityId) external view returns (address) {
return chestOwner[chestEntityId];
}
}
Loading