Skip to content

Instantly share code, notes, and snippets.

@antoniovassell
Created May 11, 2018 17:21
Show Gist options
  • Save antoniovassell/c2da58d1296d56d1519947abdea31933 to your computer and use it in GitHub Desktop.
Save antoniovassell/c2da58d1296d56d1519947abdea31933 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.4.23+commit.124ca40d.js&optimize=false&gist=
pragma solidity ^0.4.23;
/// MVP Contract to handle trading of eth - fiat currency.
contract LocalEthExchange {
// Trade statuses
bytes32 constant TRADE_STATUS_STARTED = 'TRADE_STATUS_STARTED';
bytes32 constant TRADE_STATUS_CANCELLED = 'TRADE_STATUS_CANCELLED';
bytes32 constant TRADE_STATUS_PAYMENT_DONE = 'TRADE_STATUS_PAYMENT_DONE';
bytes32 constant TRADE_STATUS_COMPLETED = 'TRADE_STATUS_COMPLETED';
bytes32 constant TRADE_STATUS_DISPUTE = 'TRADE_STATUS_DISPUTE';
// Amount of items we can retrieve at once
uint constant PAGING_LIMIT = 10;
// Objects that we are storing
struct User {
address userAddress;
bytes32 name;
}
struct Listing {
bytes32 listingId;
address user;
bytes32 description;
uint256 created;
}
struct Trade {
bytes32 tradeId;
address seller;
address buyer;
uint256 value;
bytes32 offerId;
bytes32 status;
uint256 created;
}
struct Offer {
bytes32 offerId;
address offerer;
bytes32 listingId;
uint256 value;
bytes32 description;
bool accepted;
uint256 created;
}
// Mappings of the list of objects that we are storing
// using their ids as keys
mapping(bytes32 => Listing) public listings;
mapping(address => User) users;
mapping(bytes32 => Trade) trades;
mapping(bytes32 => Offer) offers;
// Llist of ids of objects to allow us to iterate (we can't iterate with maps)
bytes32[] public listingIds;
bytes32[] public offerIds;
bytes32[] public tradeIds;
// Listing ID will be used as the key
mapping(bytes32 => bytes32[]) associatedOffers;
/// Events that clients can listen for
// When a user adds a new listing
event NewListingEvent(
bytes32 listingId,
address user,
bytes32 description,
uint256 created
);
// When a buyer adds a new offer to a listing
event NewOfferEvent(
bytes32 offerId,
address offerer,
bytes32 listingId,
uint256 value,
bytes32 description,
uint256 created
);
// When a seller accepts an offer
event AcceptOfferEvent(
bytes32 offerId,
bytes32 tradeId,
address buyer,
address seller,
uint256 value,
uint256 created
);
// When a buyer confirms that they made a payment
event BuyerConfirmEvent(
bytes32 tradeId,
address buyer,
address seller,
uint256 created
);
// When a seller confirm that the buyer paid and contract should release
// escrow funds to buyer
event SellerConfirmEvent(
bytes32 tradeId,
address buyer,
address seller,
uint256 created
);
// When a user cancels a trade
event TradeCancelledEvent(
bytes32 tradeId,
address buyer,
address seller,
uint256 created
);
/// Listing management
// Allows a user to add a new listing
function addListing(bytes32 listingId, address lister, bytes32 description) public {
require(msg.sender == lister, 'User address should be equal to the sender address');
uint256 listingTime = getTime();
listings[listingId] = Listing(listingId, lister, description, listingTime);
listingIds.push(listingId);
emit NewListingEvent(listingId, lister, description, listingTime);
}
// Gets a listing given a listing ID
function getListing(bytes32 _listingId) public constant returns (bytes32 listingId, address user, bytes32 description, uint256 created) {
// todo: get listing
Listing storage listing = listings[_listingId];
return (listing.listingId, listing.user, listing.description, listing.created);
}
/// Returns the total count of listings
function getListingCount() private view returns(uint listingsCount) {
return listingIds.length;
}
/// Get a list of indexed listings that are added to the contract
/// this stands as a pager function to allow a set number of items
/// since we cant return a dynamic array
function getListings(uint startingIndex) public view returns (
uint totalCount,
bytes32[PAGING_LIMIT] _listingIds,
address[PAGING_LIMIT] _users,
bytes32[PAGING_LIMIT] _descriptions,
uint256[PAGING_LIMIT] _created
) {
// Checking for a valid starting index
require(startingIndex >= 0, 'Starting Index should be greater than or equal to 0');
uint listingsCount = getListingCount();
require(startingIndex <= listingsCount, 'Starting index is out of range');
// Determine the max amount of items we should return (maybe only 3 items are currently added)
uint maxCount = listingsCount - startingIndex;
// Ensure max count is less than our overall limit of items
if (maxCount > PAGING_LIMIT) {
maxCount = PAGING_LIMIT;
}
// Index to be used to add items that we are going to return, it should start from zero
// despite users starting index being different
uint writeIndex = 0;
// Loop through starting from the index that the user stated
for (uint index = startingIndex; index < maxCount; index++) {
// Get the associated offer based on the indexed offer ID
bytes32 listingId = listingIds[index];
Listing storage listing = listings[listingId];
// todo: Can check if offer is actual exist here
// Set properties
_listingIds[writeIndex] = listing.listingId;
_users[writeIndex] = listing.user;
_descriptions[writeIndex] = listing.description;
_created[writeIndex] = listing.created;
writeIndex++;
}
// Return values in which each property is a static array
// since we can't return structs currently
return (listingsCount, _listingIds, _users, _descriptions, _created);
}
/// Remove a listing, only the user who created the listing can delete
function removeListing(bytes32 listingId) public {
Listing storage listing = listings[listingId];
require(listing.user != 0, 'Listing not found');
require(msg.sender == listing.user, 'Listing owner address should be equal to the sender address');
delete listings[listingId];
}
/// Returns how much offers the contrat has indexed (how much offers has been added)
function getOffersCount() private view returns(uint offersCount) {
return offerIds.length;
}
/// Allows user to add an offer
function addOffer(bytes32 offerId, address offerer, bytes32 listingId, uint256 value, bytes32 description) public {
require(offerer == msg.sender, 'Offerer address should be equal to sender address');
// todo: check that the offerer is not the seller, minor
uint256 created = getTime();
offers[offerId] = Offer(offerId, offerer, listingId, value, description, false, created);
offerIds.push(offerId);
emit NewOfferEvent(offerId,
offerer,
listingId,
value,
description,
created
);
}
/// Get a list of indexed offers that are added to the contract
/// this stands as a pager function to allow a set number of items
/// since we cant return a dynamic array
function getOffers(uint startingIndex) public view returns (
uint totalCount,
bytes32[PAGING_LIMIT] _offerIds,
bytes32[PAGING_LIMIT] _listingIds,
address[PAGING_LIMIT] _offerers,
uint256[PAGING_LIMIT] _values,
bytes32[PAGING_LIMIT] _descriptions,
uint256[PAGING_LIMIT] _created
) {
// Checking for a valid starting index
require(startingIndex >= 0, 'Starting Index should be greater than or equal to 0');
uint offersCount = getOffersCount();
require(startingIndex <= offersCount, 'Starting index is out of range');
// Determine the max amount of items we should return (maybe only 3 items are currently added)
uint maxCount = offersCount - startingIndex;
// Ensure max count is less than our overall limit of items
if (maxCount > PAGING_LIMIT) {
maxCount = PAGING_LIMIT;
}
// Index to be used to add items that we are going to return, it should start from zero
// despite users starting index being different
uint writeIndex = 0;
// Loop through starting from the index that the user stated
for (uint index = startingIndex; index < maxCount; index++) {
// Get the associated offer based on the indexed offer ID
bytes32 offerId = offerIds[index];
Offer storage offer = offers[offerId];
// todo: Can check if offer is actual exist here
// Set properties
_offerIds[writeIndex] = offer.offerId;
_listingIds[writeIndex] = offer.listingId;
_offerers[writeIndex] = offer.offerer;
_values[writeIndex] = offer.value;
_descriptions[writeIndex] = offer.description;
_created[writeIndex] = offer.created;
writeIndex++;
}
// Return values in which each property is a static array
// since we can't return structs currently
return (offersCount, _offerIds, _listingIds, _offerers, _values, _descriptions, _created);
}
function removeOffer(bytes32 offerId) public {
Offer storage offer = offers[offerId];
require(offer.offerId != 0, 'Offer not found');
delete offers[offerId];
}
/// When the user accepts an offer, the value of the offer will be deducted from their address/account
/// and placed in escrow
/// Money is automatically placed in the contracts account once this function executes successfully
function acceptOffer(bytes32 offerId, uint256 value, bytes32 tradeId) public payable {
// Lets start the trade/escrow
// Get the existing offer based on the id
Offer storage offer = offers[offerId];
require(offer.offerId != 0, 'Offer not found');
// Get associated listing
Listing storage listing = listings[offer.listingId];
require(listing.listingId != 0, 'Listing not found');
require(listing.user == msg.sender, 'Can only accept offer if user address is equal to sender address');
require(msg.value == value, 'Value should be equal to what was transfered');
offer.accepted = true;
offers[offerId] = offer;
// todo: assuming the seller is the one who created the listing for now
// todo: assuming trade doesn't already exist
// create new trade here
addTrade(tradeId, msg.sender, offer.offerer, offer.value, offerId);
}
/// Trade management
/// Adds a new trade after a user accepts an offer
function addTrade(bytes32 tradeId, address seller, address buyer, uint256 value, bytes32 offerId) private {
// Get current timestamp
uint256 created = getTime();
// Creates a new trade
trades[tradeId] = Trade(
tradeId,
seller,
buyer,
value,
offerId,
TRADE_STATUS_STARTED,
created
);
// ... And add it to the tradeIds index to enable us to loop
// over later
tradeIds.push(tradeId);
// Emits an event so that clients listening to the contract will be alerted
emit AcceptOfferEvent(
offerId,
tradeId,
buyer,
seller,
value,
created
);
}
// Gets the count of trades that had been added to the contract
function getTradesCount() private view returns(uint tradesCount) {
return tradeIds.length;
}
/// Get a list of indexed offers that are added to the contract
/// this stands as a pager function to allow a set number of items
/// since we cant return a dynamic array
function getTrades(uint startingIndex) public view returns (
uint totalCount,
bytes32[PAGING_LIMIT] _tradeIds,
bytes32[PAGING_LIMIT] _offerIds,
address[PAGING_LIMIT] _buyers,
address[PAGING_LIMIT] _sellers,
uint256[PAGING_LIMIT] _values,
bytes32[PAGING_LIMIT] _statues,
uint256[PAGING_LIMIT] _created
) {
// Checking for a valid starting index
require(startingIndex >= 0, 'Starting Index should be greater than or equal to 0');
uint tradesCount = getTradesCount();
require(startingIndex <= tradesCount, 'Starting index is out of range');
// Determine the max amount of items we should return (maybe only 3 items are currently added)
uint maxCount = tradesCount - startingIndex;
// Ensure max count is less than our overall limit of items
if (maxCount > PAGING_LIMIT) {
maxCount = PAGING_LIMIT;
}
// Loop through starting from the index that the user stated
uint writeIndex = 0;
for (uint index = startingIndex; index < maxCount; index++) {
// Get the associated trade based on the indexed trade ID
bytes32 tradeId = tradeIds[index];
Trade storage trade = trades[tradeId];
// todo: Can check if offer is actual exist here
// Set properties
_tradeIds[writeIndex] = trade.tradeId;
_offerIds[writeIndex] = trade.offerId;
_buyers[writeIndex] = trade.buyer;
_sellers[writeIndex] = trade.seller;
_values[writeIndex] = trade.value;
_statues[writeIndex] = trade.status;
_created[writeIndex] = trade.created;
writeIndex++;
}
// Return values in which each property is a static array
// since we can't return structs currently
return (tradesCount, _tradeIds, _offerIds, _buyers, _sellers, _values, _statues, _created);
}
/// Allows user to cancel a trade given a trade ID
function cancelTrade(bytes32 tradeId) public {
// Get trade by ID
Trade storage trade = trades[tradeId];
// Check that trade was found/exist
require(trade.tradeId != 0, 'Could not find trade');
// Only the buyer or seller can cancel a trade
require(msg.sender == trade.seller || msg.sender == trade.buyer, 'You have to be the seller or buyer to cancel trade');
// User cannot cancel trade if its in a certain state
// A better design would be with a statemachine
require(trade.status != TRADE_STATUS_CANCELLED, 'Trade is already cancelled');
require(trade.status != TRADE_STATUS_DISPUTE, 'Cannot cancel a trade in dispute');
require(trade.status != TRADE_STATUS_COMPLETED, 'Cannot cancel a trade that is already completed');
// Seller can't cancel trade if Buyer already confirm payment
if (trade.status == TRADE_STATUS_PAYMENT_DONE) {
require(msg.sender != trade.seller, 'Cannot cancel a trade when payment is already confirmed by buyer');
}
// Set trade status to cancel if we reach this far
trade.status = TRADE_STATUS_CANCELLED;
trades[tradeId] = trade;
// todo: validate contract has enough value to actually do a transfer,
// and alert that this could be something wrong
// Transfer back eth/token to seller
trade.seller.transfer(trade.value);
// Emit event that trade has been cancelled
emit TradeCancelledEvent(
tradeId,
trade.buyer,
trade.seller,
getTime()
);
}
/// Buyers confirm that they made payment for a given tradeId
function buyerConfirmPayment(bytes32 tradeId) public {
// Get trade
Trade storage trade = trades[tradeId];
// Verify if it exist
require(trade.tradeId != 0, 'Trade not found');
// Only the buyer should be able to confirm payment
require(msg.sender == trade.buyer, 'Buyer address should be equal to sender address');
// Verify trade is in the correct status before buyer confirms payment
require(trade.status != TRADE_STATUS_CANCELLED, 'Cannot confirm payment when trade is cancelled');
require(trade.status != TRADE_STATUS_DISPUTE, 'Cannot confirm payment when trade is in dispute');
require(trade.status != TRADE_STATUS_COMPLETED, 'Cannot confirm payment when seller already confirmed payment');
// Update trade status if we reach this far
trade.status = TRADE_STATUS_PAYMENT_DONE;
trades[tradeId] = trade;
// emit Event that buyer confirmed payment
emit BuyerConfirmEvent(
tradeId,
trade.buyer,
trade.seller,
getTime()
);
}
/// Seller can confirm that a payment was made and release funds to buyer
/// for a trade given a trade ID
function sellerReleasePayment(bytes32 tradeId) public {
// Get trade
Trade storage trade = trades[tradeId];
// Verify that trade exist
require(trade.tradeId != 0, 'Trade not found');
// Only the seller can release payment
require(msg.sender == trade.seller, 'Seller address must equal to sender address');
// Validate that trade is in the correct status to be able to
require(trade.status != TRADE_STATUS_CANCELLED, 'Cannot confirm payment when trade is cancelled');
require(trade.status != TRADE_STATUS_COMPLETED, 'Cannot confirm payment when trade already completed');
// Update trade status
trade.status = TRADE_STATUS_COMPLETED;
trades[tradeId] = trade;
// todo: validate that contract has enough balance to actually do this
// if not, something is wrong
// Transfer what ever that was suppose to be traded to the buyer, trade is completed
trade.buyer.transfer(trade.value);
emit SellerConfirmEvent(
tradeId,
trade.buyer,
trade.seller,
getTime()
);
}
/// Get a trade given a trade ID
function getTrade(bytes32 _tradeId) constant public returns (bytes32 tradeId, address seller, address buyer, uint256 value, bytes32 offerId, bytes32 status) {
Trade storage trade = trades[_tradeId];
require(trade.tradeId != 0, 'Trade not found');
return (trade.tradeId, trade.seller, trade.buyer, trade.value, trade.offerId, trade.status);
}
/// Utility functions
/// Get the current timestamp
function getTime() private constant returns(uint256 currentTime) {
return now;
}
/// Get the contract's balance
function getContractBalance() public view returns(uint256 balance) {
return address(this).balance;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment