Skip to content

Commit

Permalink
feat: view contract and payout refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
jaybuidl committed Jan 30, 2025
1 parent 08a5201 commit ea3ac2d
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 58 deletions.
144 changes: 86 additions & 58 deletions contracts/src/EscrowUniversal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pragma solidity 0.8.18;
import {IArbitrableV2, IArbitratorV2} from "@kleros/kleros-v2-contracts/arbitration/interfaces/IArbitrableV2.sol";
import "@kleros/kleros-v2-contracts/arbitration/interfaces/IDisputeTemplateRegistry.sol";
import {SafeERC20, IERC20} from "./libraries/SafeERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "./interfaces/IEscrow.sol";

/// @title EscrowUniversal for a sale paid in native currency or ERC20 tokens without platform fees.
Expand Down Expand Up @@ -467,12 +468,13 @@ contract EscrowUniversal is IEscrow, IArbitrableV2 {
/// @param _ruling Ruling given by the arbitrator. 1 : Reimburse the seller. 2 : Pay the buyer.
function executeRuling(uint256 _transactionID, uint256 _ruling) internal {
Transaction storage transaction = transactions[_transactionID];
uint256 amount = transaction.amount;
uint256 settlementBuyer = transaction.settlementBuyer;
uint256 settlementSeller = transaction.settlementSeller;
uint256 buyerFee = transaction.buyerFee;
uint256 sellerFee = transaction.sellerFee;
bool nativePayment = transaction.token == NATIVE;
address payable buyer = transaction.buyer;
address payable seller = transaction.seller;

(uint256 buyerPayout, uint256 buyerPayoutToken, uint256 sellerPayout, uint256 sellerPayoutToken) = getPayouts(
_transactionID,
Party(_ruling)
);

transaction.amount = 0;
transaction.settlementBuyer = 0;
Expand All @@ -481,58 +483,17 @@ contract EscrowUniversal is IEscrow, IArbitrableV2 {
transaction.sellerFee = 0;
transaction.status = Status.TransactionResolved;

// Give the arbitration fee back.
if (_ruling == uint256(Party.Buyer)) {
transaction.buyer.send(buyerFee);
// If there was a settlement amount proposed, we use that to make the partial payment and refund the rest.
if (settlementBuyer != 0) {
if (nativePayment) {
transaction.buyer.send(amount - settlementBuyer); // It is the user responsibility to accept ETH.
transaction.seller.send(settlementBuyer);
} else {
transaction.token.safeTransfer(transaction.buyer, amount - settlementBuyer);
transaction.token.safeTransfer(transaction.seller, settlementBuyer);
}
} else {
if (nativePayment) {
transaction.buyer.send(amount); // It is the user responsibility to accept ETH.
} else {
if (!transaction.token.safeTransfer(transaction.buyer, amount)) revert TokenTransferFailed();
}
}
} else if (_ruling == uint256(Party.Seller)) {
transaction.seller.send(sellerFee);
// If there was a settlement amount proposed, we use that to make the partial payment and refund the rest to buyer.
if (settlementSeller != 0) {
if (nativePayment) {
transaction.buyer.send(amount - settlementSeller); // It is the user responsibility to accept ETH.
transaction.seller.send(settlementSeller);
} else {
transaction.token.safeTransfer(transaction.buyer, amount - settlementSeller);
transaction.token.safeTransfer(transaction.seller, settlementSeller);
}
} else {
if (nativePayment) {
transaction.seller.send(amount); // It is the user responsibility to accept ETH.
} else {
if (!transaction.token.safeTransfer(transaction.seller, amount)) revert TokenTransferFailed();
}
}
} else {
uint256 splitArbitrationFee = buyerFee / 2;
transaction.buyer.send(splitArbitrationFee);
transaction.seller.send(splitArbitrationFee);

// Tokens should not reenter or allow recipients to refuse the transfer.
// In case of an uneven token amount, one basic token unit can be burnt.
uint256 splitAmount = amount / 2;
if (nativePayment) {
transaction.buyer.send(splitAmount); // It is the user responsibility to accept ETH.
transaction.seller.send(splitAmount);
} else {
transaction.token.safeTransfer(transaction.buyer, splitAmount);
transaction.token.safeTransfer(transaction.seller, splitAmount);
}
if (buyerPayout > 0) {
buyer.send(buyerPayout); // It is the user responsibility to accept ETH.
}
if (sellerPayout > 0) {
seller.send(sellerPayout); // It is the user responsibility to accept ETH.
}
if (buyerPayoutToken > 0) {
transaction.token.safeTransfer(buyer, buyerPayoutToken); // Tokens should not reenter or allow recipients to refuse the transfer.
}
if (sellerPayoutToken > 0) {
transaction.token.safeTransfer(seller, sellerPayoutToken); // Tokens should not reenter or allow recipients to refuse the transfer.
}

emit TransactionResolved(_transactionID, Resolution.RulingEnforced);
Expand All @@ -546,4 +507,71 @@ contract EscrowUniversal is IEscrow, IArbitrableV2 {
function getTransactionCount() external view override returns (uint256) {
return transactions.length;
}

/// @dev Get the payout depending on the winning party.
/// @dev The cost for the buyer is the seller payout non-inclusive of any arbitration fees.
/// @param _transactionID The index of the transaction.
/// @param _winningParty The winning party.
/// @return buyerPayout The payout for the buyer.
/// @return buyerPayoutToken The payout for the buyer in tokens.
/// @return sellerPayout The payout for the seller.
/// @return sellerPayoutToken The payout for the seller in tokens.
function getPayouts(
uint256 _transactionID,
Party _winningParty
)
public
view
returns (uint256 buyerPayout, uint256 buyerPayoutToken, uint256 sellerPayout, uint256 sellerPayoutToken)
{
Transaction storage transaction = transactions[_transactionID];
uint256 amount = transaction.amount;
uint256 settlementBuyer = transaction.settlementBuyer;
uint256 settlementSeller = transaction.settlementSeller;
uint256 buyerFee = transaction.buyerFee;
uint256 sellerFee = transaction.sellerFee;
bool nativePayment = transaction.token == NATIVE;
if (_winningParty == Party.Buyer) {
// The Seller gets the settlement amount proposed by the Buyer if any, otherwise nothing.
// The Buyer gets the remaining amount of the transaction back if any.
// The Buyer gets the arbitration fee back.
uint256 settledAmount = settlementBuyer;
if (nativePayment) {
buyerPayout = buyerFee + amount - settledAmount;
sellerPayout = settledAmount;
} else {
buyerPayout = buyerFee;
buyerPayoutToken = amount - settledAmount;
sellerPayoutToken = settledAmount;
}
} else if (_winningParty == Party.Seller) {
// The Seller gets his proposed settlement amount if any, otherwise the transaction amount.
// The Buyer gets the remaining amount of the transaction back if any.
// The Seller gets the arbitration fee back.
uint256 settledAmount = settlementSeller != 0 ? settlementSeller : amount;
if (nativePayment) {
buyerPayout = amount - settledAmount;
sellerPayout = sellerFee + settledAmount;
} else {
buyerPayoutToken = amount - settledAmount;
sellerPayout = sellerFee;
sellerPayoutToken = settledAmount;
}
} else {
// No party wins, we split the arbitration fee and the transaction amount.
// The arbitration fee has been paid twice, once by the Buyer and once by the Seller in equal amount once arbitration starts.
// In case of an uneven token amount, one basic token unit can be burnt.
uint256 splitArbitrationFee = buyerFee / 2; // buyerFee equals sellerFee.
buyerPayout = splitArbitrationFee;
sellerPayout = splitArbitrationFee;
uint256 splitAmount = amount / 2;
if (nativePayment) {
buyerPayout += splitAmount;
sellerPayout += splitAmount;
} else {
buyerPayoutToken = splitAmount;
sellerPayoutToken = splitAmount;
}
}
}
}
159 changes: 159 additions & 0 deletions contracts/src/EscrowView.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {EscrowUniversal, Transaction, NATIVE, Party, Status, IERC20} from "./EscrowUniversal.sol";

contract EscrowView {
EscrowUniversal public immutable escrow;

constructor(address _escrow) {
escrow = EscrowUniversal(_escrow);
}

/// @notice Get the payout messages for a transaction once a dispute is created.
/// @notice The amounts pre-dispute are imprecise as the arbitration fees are not paid yet by either or both parties.
function getPayoutMessages(
uint256 _transactionID
) external view returns (string memory noWinner, string memory buyerWins, string memory sellerWins) {
(, , uint256 amount, , , , , , , , , IERC20 token) = escrow.transactions(_transactionID);

(uint256 noWinnerPayout, uint256 noWinnerPayoutToken, , ) = escrow.getPayouts(_transactionID, Party.None);
(, , uint256 buyerWinsCost, uint256 buyerWinsCostToken) = escrow.getPayouts(_transactionID, Party.Buyer);
(, , uint256 sellerWinsCost, uint256 sellerWinsCostToken) = escrow.getPayouts(_transactionID, Party.Seller);

string memory amountStr = formatEth(amount);
string memory amountTokenStr = formatToken(amount, address(token));

if (token == NATIVE) {
noWinner = string.concat(
"Buyer and Seller get ",
formatEth(noWinnerPayout),
" back and pay half of the arbitration fees each."
);
buyerWins = string.concat(
"Buyer pays ",
formatEth(buyerWinsCost),
" instead of ",
amountStr,
", Seller pays for arbitration."
);
sellerWins = string.concat(
"Buyer pays ",
formatEth(sellerWinsCost),
" instead of ",
amountStr,
", Buyer pays for arbitration."
);
} else {
noWinner = string.concat(
"Buyer and Seller get ",
formatToken(noWinnerPayoutToken, address(token)),
" back and pay half of the arbitration fees each."
);
buyerWins = string.concat(
"Buyer pays ",
formatToken(buyerWinsCostToken, address(token)),
" instead of ",
amountTokenStr,
", Seller pays for arbitration."
);
sellerWins = string.concat(
"Buyer pays ",
formatToken(sellerWinsCostToken, address(token)),
" instead of ",
amountTokenStr,
", Buyer pays for arbitration."
);
}
}

function formatEth(uint256 _amountWei) public pure returns (string memory) {
uint256 ethWhole = _amountWei / 1 ether;
uint256 ethFraction = (_amountWei % 1 ether) / 1e15; // Get the first 3 decimal digits

// Convert the whole and fractional parts to strings
string memory ethWholeStr = Strings.toString(ethWhole);

// If the fractional part is zero, return only the whole part
if (ethFraction == 0) {
return string.concat(ethWholeStr, " ETH");
}

// Convert the fractional part to string with leading zeros if necessary
string memory ethFractionStr = Strings.toString(ethFraction);

// Pad the fractional part with leading zeros to ensure three digits
while (bytes(ethFractionStr).length < 3) {
ethFractionStr = string.concat("0", ethFractionStr);
}

// Remove trailing zeros from the fractional part
bytes memory fractionBytes = bytes(ethFractionStr);
uint256 fractionLength = fractionBytes.length;
while (fractionLength > 0 && fractionBytes[fractionLength - 1] == "0") {
fractionLength--;
}

if (fractionLength == 0) {
return ethWholeStr;
} else {
bytes memory fractionTrimmed = new bytes(fractionLength);
for (uint256 i = 0; i < fractionLength; i++) {
fractionTrimmed[i] = fractionBytes[i];
}
return string.concat(ethWholeStr, ".", string(fractionTrimmed), " ETH");
}
}

function formatToken(uint256 _amountWei, address _token) public view returns (string memory) {
IERC20Metadata token = IERC20Metadata(_token);
uint8 decimals = token.decimals();
string memory symbol = token.symbol();

uint256 tenToDecimals = uint256(10) ** uint256(decimals);
uint256 tokenWhole = _amountWei / tenToDecimals;

uint256 tokenFraction;
uint8 fractionDigits;

if (decimals >= 3) {
uint256 divider = uint256(10) ** uint256(decimals - 3);
tokenFraction = (_amountWei % tenToDecimals) / divider;
fractionDigits = 3;
} else {
tokenFraction = _amountWei % tenToDecimals;
fractionDigits = decimals;
}

string memory tokenWholeStr = Strings.toString(tokenWhole);

if (tokenFraction == 0) {
return string.concat(tokenWholeStr, " ", symbol);
}

string memory tokenFractionStr = Strings.toString(tokenFraction);

while (bytes(tokenFractionStr).length < fractionDigits) {
tokenFractionStr = string.concat("0", tokenFractionStr);
}

bytes memory fractionBytes = bytes(tokenFractionStr);
uint256 fractionLength = fractionBytes.length;
while (fractionLength > 0 && fractionBytes[fractionLength - 1] == bytes1("0")) {
fractionLength--;
}

if (fractionLength == 0) {
return string.concat(tokenWholeStr, " ", symbol);
} else {
bytes memory fractionTrimmed = new bytes(fractionLength);
for (uint256 i = 0; i < fractionLength; i++) {
fractionTrimmed[i] = fractionBytes[i];
}
return string.concat(tokenWholeStr, ".", string(fractionTrimmed), " ", symbol);
}
}
}

0 comments on commit ea3ac2d

Please sign in to comment.