pragma solidity >=0.8.0 <0.9.0;
interface Etheria {
function getOwner(uint8 col, uint8 row) external view returns(address);
function setOwner(uint8 col, uint8 row, address newOwner) external;
}
interface MapElevationRetriever {
function getElevation(uint8 col, uint8 row) external view returns (uint8);
}
contract EtheriaExchangeXL_v1pt1 {
address public owner;
address public pendingOwner;
string public name = "EtheriaExchangeXL_v1pt1";
Etheria public constant etheria = Etheria(address(0x169332Ae7D143E4B5c6baEdb2FEF77BFBdDB4011));
MapElevationRetriever public constant mapElevationRetriever = MapElevationRetriever(address(0x68549D7Dbb7A956f955Ec1263F55494f05972A6b));
uint128 public minBid = uint128(1 ether); // setting this to 10 finney throws compilation error for some reason
uint256 public feeRate = uint256(100); // in basis points (100 is 1%)
uint256 public collectedFees;
struct Bid {
uint128 amount;
uint8 minCol; // shortened all of these for readability
uint8 maxCol;
uint8 minRow;
uint8 maxRow;
uint8 minEle;
uint8 maxEle;
uint8 minWat;
uint8 maxWat;
uint64 biddersIndex; // renamed from bidderIndex because it's the Index of the bidders array
}
address[] public bidders;
mapping (address => Bid) public bidOf; // renamed these three to be ultra-descriptive
mapping (address => uint256) public pendingWithdrawalOf;
mapping (uint16 => uint128) public askFor;
event OwnershipTransferInitiated(address indexed owner, address indexed pendingOwner); // renamed some of these to conform to past tense verbs
event OwnershipTransferAccepted(address indexed oldOwner, address indexed newOwner);
event BidCreated(address indexed bidder, uint128 indexed amount, uint8 minCol, uint8 maxCol, uint8 minRow, uint8 maxRow, uint8 minEle, uint8 maxEle, uint8 minWat, uint8 maxWat);
event BidAccepted(address indexed seller, address indexed bidder, uint16 indexed index, uint128 amount, uint8 minCol, uint8 maxCol, uint8 minRow, uint8 maxRow, uint8 minEle, uint8 maxEle, uint8 minWat, uint8 maxWat);
event BidCancelled(address indexed bidder, uint128 indexed amount, uint8 minCol, uint8 maxCol, uint8 minRow, uint8 maxRow, uint8 minEle, uint8 maxEle, uint8 minWat, uint8 maxWat);
event AskCreated(address indexed owner, uint256 indexed price, uint16 indexed index);
event AskRemoved(address indexed owner, uint256 indexed price, uint16 indexed index);
event WithdrawalProcessed(address indexed account, address indexed destination, uint256 indexed amount);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "EEXL: Not owner");
_;
}
function transferOwnership(address newOwner) external onlyOwner {
pendingOwner = newOwner;
emit OwnershipTransferInitiated(msg.sender, newOwner);
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "EEXL: Not pending owner");
emit OwnershipTransferAccepted(owner, msg.sender);
owner = msg.sender;
pendingOwner = address(0);
}
function _safeTransferETH(address recipient, uint256 amount) internal {
// Secure transfer of ETH that is much less likely to be broken by future gas-schedule EIPs
(bool success, ) = recipient.call{ value: amount }(""); // syntax: (bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(encoded function and data)
require(success, "EEXL: ETH transfer failed");
}
function collectFees() external onlyOwner {
uint256 amount = collectedFees;
collectedFees = uint256(0);
_safeTransferETH(msg.sender, amount);
}
function setFeeRate(uint256 newFeeRate) external onlyOwner {
// Set the feeRate to newFeeRate, then validate it
require((feeRate = newFeeRate) <= uint256(500), "EEXL: Invalid feeRate"); // feeRate will revert if req fails
}
function setMinBid(uint128 newMinBid) external onlyOwner {
minBid = newMinBid; // doubly beneficial because I could effectively kill new bids with a huge minBid
} // in the event of an exchange upgrade or unforseen problem
function _getIndex(uint8 col, uint8 row) internal pure returns (uint16) {
require(_isValidColOrRow(col) && _isValidColOrRow(row), "EEXL: Invalid col and/or row");
return (uint16(col) * uint16(33)) + uint16(row);
}
function _isValidColOrRow(uint8 value) internal pure returns (bool) {
return (value >= uint8(0)) && (value <= uint8(32)); // while nobody should be checking, eg, getAsk when row/col=0/32, we do want to respond non-erroneously
}
function _isValidElevation(uint8 value) internal pure returns (bool) {
return (value >= uint8(125)) && (value <= uint8(216));
}
function _isWater(uint8 col, uint8 row) internal view returns (bool) {
return mapElevationRetriever.getElevation(col, row) < uint8(125);
}
function _boolToUint8(bool value) internal pure returns (uint8) {
return value ? uint8(1) : uint8(0);
}
function _getSurroundingWaterCount(uint8 col, uint8 row) internal view returns (uint8 waterTiles) {
require((col >= uint8(1)) && (col <= uint8(31)), "EEXL: Water counting requres col 1-31");
require((row >= uint8(1)) && (row <= uint8(31)), "EEXL: Water counting requres col 1-31");
if (row % uint8(2) == uint8(1)) {
waterTiles += _boolToUint8(_isWater(col + uint8(1), row + uint8(1))); // northeast_hex
waterTiles += _boolToUint8(_isWater(col + uint8(1), row - uint8(1))); // southeast_hex
} else {
waterTiles += _boolToUint8(_isWater(col - uint8(1), row - uint8(1))); // southwest_hex
waterTiles += _boolToUint8(_isWater(col - uint8(1), row + uint8(1))); // northwest_hex
}
waterTiles += _boolToUint8(_isWater(col, row - uint8(1))); // southwest_hex or southeast_hex
waterTiles += _boolToUint8(_isWater(col, row + uint8(1))); // northwest_hex or northeast_hex
waterTiles += _boolToUint8(_isWater(col + uint8(1), row)); // east_hex
waterTiles += _boolToUint8(_isWater(col - uint8(1), row)); // west_hex
}
function getBidders() public view returns (address[] memory) {
return bidders;
}
function getAsk(uint8 col, uint8 row) public view returns (uint128) {
return askFor[_getIndex(col, row)];
}
// we provide only the land tileIndices to minimize gas usage // should we have this function at all?
// function getAsks(uint16[] calldata tileIndices) external view returns (uint128[] memory asks) {
// uint256 length = tileIndices.length;
// asks = new uint128[](length);
// for (uint256 i; i < length; ++i) {
// asks[i] = askAt(tileIndices[i]);
// }
// }
function setAsk(uint8 col, uint8 row, uint128 price) external {
require(price > 0, "EEXL: removeAsk instead");
require(etheria.getOwner(col, row) == msg.sender, "EEXL: Not tile owner");
uint16 thisIndex = _getIndex(col, row);
emit AskCreated(msg.sender, askFor[thisIndex] = price, thisIndex);
}
function removeAsk(uint8 col, uint8 row) external {
require(etheria.getOwner(col, row) == msg.sender, "EEXL: Not tile owner");
uint16 thisIndex = _getIndex(col, row);
uint128 price = askFor[thisIndex];
askFor[thisIndex] = 0;
emit AskRemoved(msg.sender, price, thisIndex); // price before it was zeroed
}
function makeBid(uint8 minCol, uint8 maxCol, uint8 minRow, uint8 maxRow, uint8 minEle, uint8 maxEle, uint8 minWat, uint8 maxWat) external payable {
require(msg.sender == tx.origin, "EEXL: not EOA"); // (EOA = Externally owned account) // Etheria doesn't allow tile ownership by contracts, this check prevents black-holing
require(msg.value <= type(uint128).max, "EEXL: value too high");
require(msg.value >= minBid, "EEXL: req bid amt >= minBid");
require(msg.value >= 0, "EEXL: req bid amt >= 0");
require(bidOf[msg.sender].amount == uint128(0), "EEXL: bid exists, cancel first");
require(_isValidColOrRow(minCol), "EEXL: minCol OOB");
require(_isValidColOrRow(maxCol), "EEXL: maxCol OOB");
require(minCol <= maxCol, "EEXL: req minCol <= maxCol");
require(_isValidColOrRow(minRow), "EEXL: minRow OOB");
require(_isValidColOrRow(maxRow), "EEXL: maxRow OOB");
require(minRow <= maxRow, "EEXL: req minRow <= maxRow");
require(_isValidElevation(minEle), "EEXL: minEle OOB"); // these ele checks prevent water bidding, regardless of row/col
require(_isValidElevation(maxEle), "EEXL: maxEle OOB");
require(minEle <= maxEle, "EEXL: req minEle <= maxEle");
require(minWat <= uint8(6), "EEXL: minWat OOB");
require(maxWat <= uint8(6), "EEXL: maxWat OOB");
require(minWat <= maxWat, "EEXL: req minWat <= maxWat");
uint256 biddersArrayLength = bidders.length;
require(biddersArrayLength < type(uint64).max, "EEXL: too many bids");
bidOf[msg.sender] = Bid({
amount: uint128(msg.value),
minCol: minCol,
maxCol: maxCol,
minRow: minRow,
maxRow: maxRow,
minEle: minEle,
maxEle: maxEle,
minWat: minWat,
maxWat: maxWat,
biddersIndex: uint64(biddersArrayLength)
});
bidders.push(msg.sender);
emit BidCreated(msg.sender, uint128(msg.value), minCol, maxCol, minRow, maxRow, minEle, maxEle, minWat, maxWat);
}
function _deleteBid(address bidder, uint64 biddersIndex) internal { // used by cancelBid and acceptBid
address lastBidder = bidders[bidders.length - uint256(1)];
// If bidder not last bidder, overwrite with last bidder
if (bidder != lastBidder) {
bidders[biddersIndex] = lastBidder; // Overwrite the bidder at the index with the last bidder
bidOf[lastBidder].biddersIndex = biddersIndex; // Update the bidder index of the bid of the previously last bidder
}
delete bidOf[bidder];
bidders.pop();
}
function cancelBid() external {
// Cancels the bid, getting the bid's amount, which is then added account's pending withdrawal
Bid storage bid = bidOf[msg.sender];
uint128 amount = bid.amount;
require(amount != uint128(0), "EEXL: No existing bid");
emit BidCancelled(msg.sender, amount, bid.minCol, bid.maxCol, bid.minRow, bid.maxRow, bid.minEle, bid.maxEle, bid.minWat, bid.maxWat);
_deleteBid(msg.sender, bid.biddersIndex);
pendingWithdrawalOf[msg.sender] += uint256(amount);
}
function acceptBid(uint8 col, uint8 row, address bidder, uint256 minAmount) external {
require(etheria.getOwner(col, row) == msg.sender, "EEXL: Not owner"); // etheria.setOwner will fail below if not owner, making this check unnecessary, but I want this here anyway
Bid storage bid = bidOf[bidder];
uint128 amount = bid.amount;
require(
(amount >= minAmount) &&
(col >= bid.minCol) &&
(col <= bid.maxCol) &&
(row >= bid.minRow) &&
(row <= bid.maxRow) &&
(mapElevationRetriever.getElevation(col, row) >= bid.minEle) &&
(mapElevationRetriever.getElevation(col, row) <= bid.maxEle) &&
(_getSurroundingWaterCount(col, row) >= bid.minWat) &&
(_getSurroundingWaterCount(col, row) <= bid.maxWat),
"EEXL: tile doesn't meet bid reqs"
);
emit BidAccepted(msg.sender, bidder, _getIndex(col, row), amount, bid.minCol, bid.maxCol, bid.minRow, bid.maxRow, bid.minEle, bid.maxEle, bid.minWat, bid.maxWat);
_deleteBid(bidder, bid.biddersIndex);
etheria.setOwner(col, row, bidder);
require(etheria.getOwner(col, row) == bidder, "EEXL: failed setting tile owner"); // ok for require after event emission. Events are technically state changes and atomic as well.
uint256 fee = (uint256(amount) * feeRate) / uint256(10_000);
collectedFees += fee;
pendingWithdrawalOf[msg.sender] += (uint256(amount) - fee);
delete askFor[_getIndex(col, row)]; // don't emit AskRemoved here. It's not really a removal
}
function _withdraw(address account, address payable destination) internal {
uint256 amount = pendingWithdrawalOf[account];
require(amount > uint256(0), "EEXL: nothing pending");
pendingWithdrawalOf[account] = uint256(0);
_safeTransferETH(destination, amount);
emit WithdrawalProcessed(account, destination, amount);
}
function withdraw(address payable destination) external {
_withdraw(msg.sender, destination);
}
function withdraw() external {
_withdraw(msg.sender, payable(msg.sender));
}
}
{
"compilationTarget": {
"EtheriaExchangeXL_v1pt1.sol": "EtheriaExchangeXL_v1pt1"
},
"evmVersion": "berlin",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"uint256","name":"price","type":"uint256"},{"indexed":true,"internalType":"uint16","name":"index","type":"uint16"}],"name":"AskCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"uint256","name":"price","type":"uint256"},{"indexed":true,"internalType":"uint16","name":"index","type":"uint16"}],"name":"AskRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"seller","type":"address"},{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":true,"internalType":"uint16","name":"index","type":"uint16"},{"indexed":false,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint8","name":"minCol","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxCol","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minRow","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxRow","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minEle","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxEle","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minWat","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxWat","type":"uint8"}],"name":"BidAccepted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":true,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint8","name":"minCol","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxCol","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minRow","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxRow","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minEle","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxEle","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minWat","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxWat","type":"uint8"}],"name":"BidCancelled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":true,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint8","name":"minCol","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxCol","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minRow","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxRow","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minEle","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxEle","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"minWat","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"maxWat","type":"uint8"}],"name":"BidCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferAccepted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"pendingOwner","type":"address"}],"name":"OwnershipTransferInitiated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"destination","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"WithdrawalProcessed","type":"event"},{"inputs":[{"internalType":"uint8","name":"col","type":"uint8"},{"internalType":"uint8","name":"row","type":"uint8"},{"internalType":"address","name":"bidder","type":"address"},{"internalType":"uint256","name":"minAmount","type":"uint256"}],"name":"acceptBid","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint16","name":"","type":"uint16"}],"name":"askFor","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"bidOf","outputs":[{"internalType":"uint128","name":"amount","type":"uint128"},{"internalType":"uint8","name":"minCol","type":"uint8"},{"internalType":"uint8","name":"maxCol","type":"uint8"},{"internalType":"uint8","name":"minRow","type":"uint8"},{"internalType":"uint8","name":"maxRow","type":"uint8"},{"internalType":"uint8","name":"minEle","type":"uint8"},{"internalType":"uint8","name":"maxEle","type":"uint8"},{"internalType":"uint8","name":"minWat","type":"uint8"},{"internalType":"uint8","name":"maxWat","type":"uint8"},{"internalType":"uint64","name":"biddersIndex","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"bidders","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cancelBid","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"collectFees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"collectedFees","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"etheria","outputs":[{"internalType":"contract Etheria","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"col","type":"uint8"},{"internalType":"uint8","name":"row","type":"uint8"}],"name":"getAsk","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBidders","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"minCol","type":"uint8"},{"internalType":"uint8","name":"maxCol","type":"uint8"},{"internalType":"uint8","name":"minRow","type":"uint8"},{"internalType":"uint8","name":"maxRow","type":"uint8"},{"internalType":"uint8","name":"minEle","type":"uint8"},{"internalType":"uint8","name":"maxEle","type":"uint8"},{"internalType":"uint8","name":"minWat","type":"uint8"},{"internalType":"uint8","name":"maxWat","type":"uint8"}],"name":"makeBid","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"mapElevationRetriever","outputs":[{"internalType":"contract MapElevationRetriever","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minBid","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pendingOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"pendingWithdrawalOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"col","type":"uint8"},{"internalType":"uint8","name":"row","type":"uint8"}],"name":"removeAsk","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"col","type":"uint8"},{"internalType":"uint8","name":"row","type":"uint8"},{"internalType":"uint128","name":"price","type":"uint128"}],"name":"setAsk","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newFeeRate","type":"uint256"}],"name":"setFeeRate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint128","name":"newMinBid","type":"uint128"}],"name":"setMinBid","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"destination","type":"address"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]