Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "lib/safe-contracts"]
path = lib/safe-contracts
url = https://github.com/gnosis/safe-contracts
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
3 changes: 2 additions & 1 deletion echidna/EscrowMulticall/test-EscrowMulticall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "../../src/EscrowMulticall.sol";
import "../../src/HatsSecurityContext.sol";
import "../../src/SystemSettings.sol";
import "hevm/Hevm.sol";
import "../../src/IPurchaseTracker.sol";

/**
* @title test_EscrowMulticallInvariants
Expand Down Expand Up @@ -64,7 +65,7 @@ contract test_EscrowMulticallInvariants {

// Deploy multiple PaymentEscrow contracts
for (uint256 i = 0; i < 3; i++) {
PaymentEscrow e = new PaymentEscrow(securityContext, systemSettings, false);
PaymentEscrow e = new PaymentEscrow(securityContext, systemSettings, false, IPurchaseTracker(address(0)));
escrows.push(e);
}

Expand Down
4 changes: 3 additions & 1 deletion echidna/PaymentEscrow/test-PaymentEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "../../src/IHatsSecurityContext.sol";
import "../../src/ISystemSettings.sol";
import "../../src/PaymentInput.sol";
import "hevm/Hevm.sol";
import "../../src/IPurchaseTracker.sol";

/**
* @title test_PaymentEscrow
Expand Down Expand Up @@ -65,7 +66,8 @@ contract test_PaymentEscrow {
escrow = new PaymentEscrow(
IHatsSecurityContext(address(securityContext)),
ISystemSettings(address(systemSettings)),
false
false,
IPurchaseTracker(address(0))
);
}

Expand Down
4 changes: 3 additions & 1 deletion echidna/Roles/test-Roles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import "../../src/PaymentInput.sol";
import "../../src/hats/EligibilityModule.sol";
import "../../src/hats/ToggleModule.sol";
import "hevm/Hevm.sol";
import "../../src/IPurchaseTracker.sol";
// Roles
bytes32 constant DAO_ROLE = keccak256("DAO_ROLE");
bytes32 constant SYSTEM_ROLE = keccak256("SYSTEM_ROLE");
Expand Down Expand Up @@ -158,7 +159,8 @@ contract test_RoleInvariants {
escrow = new PaymentEscrow(
IHatsSecurityContext(address(hatsContext)),
ISystemSettings(address(systemSettings)),
false // autoReleaseFlag
false, // autoReleaseFlag
IPurchaseTracker(address(0))
);
}

Expand Down
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 3b20d6
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ lib/ERC1155/=lib/hats-protocol/lib/ERC1155/
@hats-protocol/=lib/hats-protocol/src
hevm/=lib/properties/contracts/util/
@gnosis.pm/safe-contracts/=lib/safe-contracts/
@gnosis.pm/zodiac/=lib/zodiac/
@gnosis.pm/zodiac/=lib/zodiac/
forge-std/=lib/forge-std/src/
6 changes: 6 additions & 0 deletions src/IPurchaseTracker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

interface IPurchaseTracker {
function recordPurchase(bytes32 paymentId, address seller, address buyer, uint256 amount) external;
}
66 changes: 25 additions & 41 deletions src/PaymentEscrow.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "./HasSecurityContext.sol";
import "./ISystemSettings.sol";
import "./HasSecurityContext.sol";
import "./ISystemSettings.sol";
import "./CarefulMath.sol";
import "./PaymentInput.sol";
import "./IEscrowContract.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./Roles.sol";
import "./IPurchaseTracker.sol";

/**
* @title PaymentEscrow
Expand All @@ -17,7 +18,6 @@ import "./Roles.sol";
*
* @author John R. Kosinski
* LoadPipe 2024
* All rights reserved. Unauthorized use prohibited.
*/
contract PaymentEscrow is HasSecurityContext, IEscrowContract
{
Expand All @@ -26,8 +26,9 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
bool private autoReleaseFlag;
bool public paused;

//EVENTS
IPurchaseTracker public purchaseTracker;

// EVENTS
event PaymentReceived (
bytes32 indexed paymentId,
address indexed to,
Expand All @@ -39,7 +40,6 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
event ReleaseAssentGiven (
bytes32 indexed paymentId,
address assentingAddress,
//TODO: make enum
uint8 assentType // 1 = payer, 2 = receiver, 3 = arbiter
);

Expand Down Expand Up @@ -77,21 +77,22 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
}

/**
* Constructor.
*
* Emits:
* - {HasSecurityContext-SecurityContextSet}
*
* Reverts:
* - {ZeroAddressArgument} if the securityContext address is 0x0.
*
* @param securityContext Contract which will define & manage secure access for this contract.
* @param settings_ Address of contract that holds system settings.
* @notice Constructor.
* @param securityContext Contract which will define & manage secure access for this contract.
* @param settings_ Address of contract that holds system settings.
* @param autoRelease Determines whether new payments automatically have the receiver’s assent.
* @param _purchaseTracker Address of the PurchaseTracker singleton.
*/
constructor(IHatsSecurityContext securityContext, ISystemSettings settings_, bool autoRelease) {
constructor(
IHatsSecurityContext securityContext,
ISystemSettings settings_,
bool autoRelease,
IPurchaseTracker _purchaseTracker
) {
_setSecurityContext(securityContext);
settings = settings_;
autoReleaseFlag = autoRelease;
purchaseTracker = _purchaseTracker;
}

/**
Expand All @@ -118,14 +119,9 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
}

/**
* Allows multiple payments to be processed.
*
* Reverts:
* - 'InsufficientAmount': if amount of native ETH sent is not equal to the declared amount.
* - 'TokenPaymentFailed': if token transfer fails for any reason (e.g. insufficial allowance)
* - 'DuplicatePayment': if payment id exists already
*
* Emits:
* @notice Processes a new payment.
*
* Emits:
* - {PaymentEscrow-PaymentReceived}
*
* @param paymentInput Payment inputs
Expand Down Expand Up @@ -162,9 +158,7 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
}

/**
* Returns the payment data specified by id.
*
* @param paymentId A unique payment id
* @notice Returns the payment data for a given id.
*/
function getPayment(bytes32 paymentId) public view returns (Payment memory) {
return payments[paymentId];
Expand Down Expand Up @@ -240,17 +234,13 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
Payment storage payment = payments[paymentId];
require(payment.released == false, "Payment already released");
if (payment.amount > 0 && payment.amountRefunded <= payment.amount) {

//who has permission to refund? either the receiver or the arbiter
if (payment.receiver != msg.sender && !securityContext.hasRole(Roles.ARBITER_ROLE, msg.sender))
revert("Unauthorized");

uint256 activeAmount = payment.amount - payment.amountRefunded;

if (amount > activeAmount)
revert("AmountExceeded");

//transfer amount back to payer
if (amount > 0) {
if (_transferAmount(payment.id, payment.payer, payment.currency, amount)) {
payment.amountRefunded += amount;
Expand Down Expand Up @@ -286,9 +276,6 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
}


//NON-PUBLIC METHODS

// Helper function to calculate fee and remaining amount
function _calculateFeeAndAmount(uint256 amount) internal view returns (uint256 fee, uint256 amountToPay) {
fee = 0;
uint256 feeBps = _getFeeBps();
Expand All @@ -301,7 +288,6 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
amountToPay = amount - fee;
}

// Helper function to handle fee transfer
function _handleFeeTransfer(bytes32 paymentId, address currency, uint256 fee) internal returns (bool) {
if (fee == 0) return true;
return _transferAmount(paymentId, _getvaultAddress(), currency, fee);
Expand All @@ -316,21 +302,22 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
uint256 amount = payment.amount - payment.amountRefunded;
(uint256 fee, uint256 amountToPay) = _calculateFeeAndAmount(amount);

// If there's no amount to pay but there is a fee, or if the transfer succeeds
if ((amountToPay == 0 && fee > 0) ||
_transferAmount(payment.id, payment.receiver, payment.currency, amountToPay)) {

// Handle fee transfer
if (_handleFeeTransfer(payment.id, payment.currency, fee)) {
payment.released = true;
emit EscrowReleased(paymentId, amountToPay, fee);

if (address(purchaseTracker) != address(0)) {
purchaseTracker.recordPurchase(paymentId, payment.receiver, payment.payer, amountToPay);
}
}
}
}

function _transferAmount(bytes32 paymentId, address to, address tokenAddressOrZero, uint256 amount) internal returns (bool) {
bool success = false;

if (amount > 0) {
if (tokenAddressOrZero == address(0)) {
(success,) = payable(to).call{value: amount}("");
Expand All @@ -347,21 +334,18 @@ contract PaymentEscrow is HasSecurityContext, IEscrowContract
revert("PaymentTransferFailed");
}
}

return success;
}

function _getFeeBps() internal view returns (uint256) {
if (address(settings) != address(0))
return settings.feeBps();

return 0;
}

function _getvaultAddress() internal view returns (address) {
if (address(settings) != address(0))
return settings.vaultAddress();

return address(0);
}

Expand Down
7 changes: 4 additions & 3 deletions test-foundry/EscrowMulticallTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { TestToken } from "../src/TestToken.sol";
import { IHatsSecurityContext } from "../src/IHatsSecurityContext.sol";
import "../src/hats/EligibilityModule.sol";
import "../src/hats/ToggleModule.sol";
import "../src/IPurchaseTracker.sol";

contract EscrowMulticallTest is Test {
Hats hats;
Expand Down Expand Up @@ -123,10 +124,10 @@ contract EscrowMulticallTest is Test {
testToken = new TestToken("XYZ", "ZYX");
systemSettings = new SystemSettings(IHatsSecurityContext(address(securityContext)), vaultAddress, 0);

escrow = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false);
escrow = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false, IPurchaseTracker(address(0)));
escrow1 = escrow;
escrow2 = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false);
escrow3 = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false);
escrow2 = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false, IPurchaseTracker(address(0)));
escrow3 = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false, IPurchaseTracker(address(0)));

multicall = new EscrowMulticall();

Expand Down
4 changes: 3 additions & 1 deletion test-foundry/HatsTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ISystemSettings } from "../src/ISystemSettings.sol";
import { PaymentInput, Payment } from "../src/PaymentInput.sol";
import { FailingToken } from "../src/FailingToken.sol";
import { console } from "forge-std/console.sol";
import "../src/IPurchaseTracker.sol";

// Dummy Eligibility and Toggle Modules
contract DummyEligibilityModule {
Expand Down Expand Up @@ -206,7 +207,8 @@ contract PaymentEscrowHatsTest is Test {
escrow = new PaymentEscrow(
IHatsSecurityContext(address(hatsSecurityContext)),
ISystemSettings(address(systemSettings)),
false // autoReleaseFlag
false, // autoReleaseFlag
IPurchaseTracker(address(0))
);

vm.stopPrank();
Expand Down
6 changes: 4 additions & 2 deletions test-foundry/PaymentEscrowTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {console} from "forge-std/console.sol";
import {FailingToken} from "../src/FailingToken.sol";
import "../src/hats/EligibilityModule.sol";
import "../src/hats/ToggleModule.sol";
import "../src/IPurchaseTracker.sol";

contract PaymentEscrowTest is Test {
Hats internal hats;
Expand Down Expand Up @@ -139,7 +140,7 @@ contract PaymentEscrowTest is Test {

testToken = new TestToken("XYZ", "ZYX");
systemSettings = new SystemSettings(IHatsSecurityContext(address(securityContext)), vaultAddress, 0);
escrow = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false);
escrow = new PaymentEscrow(IHatsSecurityContext(address(securityContext)), ISystemSettings(address(systemSettings)), false, IPurchaseTracker(address(0)));

testToken.mint(nonOwner, 10_000_000_000);
testToken.mint(payer1, 10_000_000_000);
Expand Down Expand Up @@ -1615,7 +1616,8 @@ contract PaymentEscrowTest is Test {
PaymentEscrow escrowAutoRelease = new PaymentEscrow(
IHatsSecurityContext(address(securityContext)),
ISystemSettings(address(systemSettings)),
true // autoReleaseFlag = true
true, // autoReleaseFlag = true
IPurchaseTracker(address(0))
);
vm.stopPrank();

Expand Down