Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
294 changes: 11 additions & 283 deletions contracts/EtomicSwap.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.30;
pragma solidity ^0.8.33;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract EtomicSwap {
using SafeERC20 for IERC20;

contract EtomicSwap is ERC165, IERC1155Receiver, IERC721Receiver {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we better keep erc165?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ERC165 is for interface detection - lets other contracts query "do you support interface X?".

We had it because the old contract implemented IERC721Receiver/IERC1155Receiver for NFT support. Standard ERC20 doesn't require ERC165 (it predates it), and ERC721/ERC1155 are the ones that mandate it for their receiver callbacks.

With NFTs removed, there's no interface to advertise. Our contract uses safeTransferFrom to pull tokens (not receive via callback), so no receiver interface is needed.

enum PaymentState {
Uninitialized,
PaymentSent,
Expand Down Expand Up @@ -65,8 +63,9 @@ contract EtomicSwap is ERC165, IERC1155Receiver, IERC721Receiver {
address receiver,
bytes20 secretHash,
uint64 lockTime
) external payable {
) external {
require(receiver != address(0), "Receiver cannot be the zero address");
require(tokenAddress != address(0), "Token address cannot be zero");
require(amount > 0, "Payment amount must be greater than 0");
require(
payments[id].state == PaymentState.Uninitialized,
Expand All @@ -89,12 +88,7 @@ contract EtomicSwap is ERC165, IERC1155Receiver, IERC721Receiver {
emit PaymentSent(id);

// Now performing the external interaction
IERC20 token = IERC20(tokenAddress);
// Ensure that the token transfer from the sender to the contract is successful
require(
token.transferFrom(msg.sender, address(this), amount),
"ERC20 transfer failed: Insufficient balance or allowance"
);
IERC20(tokenAddress).safeTransferFrom(msg.sender, address(this), amount);
}

function receiverSpend(
Expand Down Expand Up @@ -130,90 +124,10 @@ contract EtomicSwap is ERC165, IERC1155Receiver, IERC721Receiver {
if (tokenAddress == address(0)) {
payable(msg.sender).transfer(amount);
} else {
IERC20 token = IERC20(tokenAddress);
require(
token.transfer(msg.sender, amount),
"ERC20 transfer failed: Contract may lack balance or token transfer was rejected"
);
IERC20(tokenAddress).safeTransfer(msg.sender, amount);
}
}

function receiverSpendErc721(
bytes32 id,
bytes32 secret,
address tokenAddress,
uint256 tokenId,
address sender
) external {
// Check if the payment state is PaymentSent
require(
payments[id].state == PaymentState.PaymentSent,
"Invalid payment state. Must be PaymentSent"
);
// Check if the function caller is an externally owned account (EOA)
require(msg.sender == tx.origin, "Caller must be an EOA");

bytes20 paymentHash = ripemd160(
abi.encodePacked(
msg.sender,
sender,
ripemd160(abi.encodePacked(sha256(abi.encodePacked(secret)))),
tokenAddress,
tokenId
)
);
require(paymentHash == payments[id].paymentHash, "Invalid paymentHash");

// Effects
payments[id].state = PaymentState.ReceiverSpent;

// Event Emission
emit ReceiverSpent(id, secret);

// Interactions
IERC721 token = IERC721(tokenAddress);
token.safeTransferFrom(address(this), msg.sender, tokenId);
}

function receiverSpendErc1155(
bytes32 id,
uint256 amount,
bytes32 secret,
address tokenAddress,
uint256 tokenId,
address sender
) external {
// Check if the payment state is PaymentSent
require(
payments[id].state == PaymentState.PaymentSent,
"Invalid payment state. Must be PaymentSent"
);
// Check if the function caller is an externally owned account (EOA)
require(msg.sender == tx.origin, "Caller must be an EOA");

bytes20 paymentHash = ripemd160(
abi.encodePacked(
msg.sender,
sender,
ripemd160(abi.encodePacked(sha256(abi.encodePacked(secret)))),
tokenAddress,
tokenId,
amount
)
);
require(paymentHash == payments[id].paymentHash, "Invalid paymentHash");

// Effects
payments[id].state = PaymentState.ReceiverSpent;

// Event Emission
emit ReceiverSpent(id, secret);

// Interactions
IERC1155 token = IERC1155(tokenAddress);
token.safeTransferFrom(address(this), msg.sender, tokenId, amount, "");
}

function senderRefund(
bytes32 id,
uint256 amount,
Expand All @@ -225,6 +139,7 @@ contract EtomicSwap is ERC165, IERC1155Receiver, IERC721Receiver {
payments[id].state == PaymentState.PaymentSent,
"Invalid payment state. Must be PaymentSent"
);

bytes20 paymentHash = ripemd160(
abi.encodePacked(
receiver,
Expand All @@ -247,194 +162,7 @@ contract EtomicSwap is ERC165, IERC1155Receiver, IERC721Receiver {
if (tokenAddress == address(0)) {
payable(msg.sender).transfer(amount);
} else {
IERC20 token = IERC20(tokenAddress);
require(token.transfer(msg.sender, amount));
}
}

function senderRefundErc721(
bytes32 id,
bytes20 secretHash,
address tokenAddress,
uint256 tokenId,
address receiver
) external {
require(
payments[id].state == PaymentState.PaymentSent,
"Invalid payment state. Must be PaymentSent"
);
bytes20 paymentHash = ripemd160(
abi.encodePacked(
receiver,
msg.sender,
secretHash,
tokenAddress,
tokenId
)
);
require(paymentHash == payments[id].paymentHash, "Invalid paymentHash");
require(
block.timestamp >= payments[id].lockTime,
"Current timestamp didn't exceed payment lock time"
);

payments[id].state = PaymentState.SenderRefunded;

emit SenderRefunded(id);

IERC721 token = IERC721(tokenAddress);
token.safeTransferFrom(address(this), msg.sender, tokenId);
}

function senderRefundErc1155(
bytes32 id,
uint256 amount,
bytes20 secretHash,
address tokenAddress,
uint256 tokenId,
address receiver
) external {
require(
payments[id].state == PaymentState.PaymentSent,
"Invalid payment state. Must be PaymentSent"
);
bytes20 paymentHash = ripemd160(
abi.encodePacked(
receiver,
msg.sender,
secretHash,
tokenAddress,
tokenId,
amount
)
);
require(paymentHash == payments[id].paymentHash, "Invalid paymentHash");
require(
block.timestamp >= payments[id].lockTime,
"Current timestamp didn't exceed payment lock time"
);

payments[id].state = PaymentState.SenderRefunded;

emit SenderRefunded(id);

IERC1155 token = IERC1155(tokenAddress);
token.safeTransferFrom(address(this), msg.sender, tokenId, amount, "");
}

function onERC1155Received(
address operator,
address from,
uint256 tokenId,
uint256 value,
bytes calldata data
) external override returns (bytes4) {
// Decode the data to extract HTLC parameters
(
bytes32 id,
address receiver,
address tokenAddress,
bytes20 secretHash,
uint64 lockTime
) = abi.decode(data, (bytes32, address, address, bytes20, uint64));

require(receiver != address(0), "Receiver must not be zero address");
require(tokenAddress != address(0), "Token must not be zero address");
require(
msg.sender == tokenAddress,
"Token address does not match sender"
);
require(operator == from, "Operator must be the sender");
require(value > 0, "Value must be greater than 0");
require(
payments[id].state == PaymentState.Uninitialized,
"ERC1155 payment must be Uninitialized"
);
require(!isContract(receiver), "Receiver cannot be a contract");

bytes20 paymentHash = ripemd160(
abi.encodePacked(
receiver,
from,
secretHash,
tokenAddress,
tokenId,
value
)
);

payments[id] = Payment(paymentHash, lockTime, PaymentState.PaymentSent);
emit PaymentSent(id);

// Return this magic value to confirm receipt of ERC1155 token
return this.onERC1155Received.selector;
}

function onERC1155BatchReceived(
address, /* operator */
address, /* from */
uint256[] calldata, /* ids */
uint256[] calldata, /* values */
bytes calldata /* data */
) external pure override returns (bytes4) {
revert("Batch transfers not supported");
}

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC165, IERC165)
returns (bool)
{
return
interfaceId == type(IERC1155Receiver).interfaceId ||
super.supportsInterface(interfaceId);
}

function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external override returns (bytes4) {
// Decode the data to extract HTLC parameters
(
bytes32 id,
address receiver,
address tokenAddress,
bytes20 secretHash,
uint64 lockTime
) = abi.decode(data, (bytes32, address, address, bytes20, uint64));

require(receiver != address(0), "Receiver must not be zero address");
require(tokenAddress != address(0), "Token must not be zero address");
require(
msg.sender == tokenAddress,
"Token address does not match sender"
);
require(operator == from, "Operator must be the sender");
require(
payments[id].state == PaymentState.Uninitialized,
"ERC721 payment must be Uninitialized"
);
require(!isContract(receiver), "Receiver cannot be a contract");

bytes20 paymentHash = ripemd160(
abi.encodePacked(receiver, from, secretHash, tokenAddress, tokenId)
);

payments[id] = Payment(paymentHash, lockTime, PaymentState.PaymentSent);
emit PaymentSent(id);

// Return this magic value to confirm receipt of ERC721 token
return this.onERC721Received.selector;
}

function isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
IERC20(tokenAddress).safeTransfer(msg.sender, amount);
}
return size > 0;
}
}
10 changes: 9 additions & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
require("@nomicfoundation/hardhat-ethers");

module.exports = {
solidity: "0.8.30",
solidity: {
version: "0.8.33",
settings: {
optimizer: {
enabled: true,
runs: 10000

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: what is runs: 10000

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runs parameter tells the optimizer how many times each opcode will execute over the contract's lifetime - it's a trade-off between deployment cost and execution cost.

10000 is a common choice for frequently-used contracts. Higher values have diminishing returns since most inlining decisions are already made.

Ref: Solidity Optimizer Docs

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. I favoured execution cost (what the users pay) vs the deployment cost which is what we will pay to deploy the contract.

}
}
},
networks: {
hardhat: {
chainId: 1337, // or another number that Remix will accept
Expand Down
13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
{
"name": "ETOMIC_SWAP",
"version": "1.0.0",
"version": "1.1.0",
"description": "Etomic swap smart contracts and helpers",
"main": "index.js",
"scripts": {
"test": "npx hardhat test"
},
"author": "[email protected]",
"license": "ISC",
"dependencies": {
"chai": "^4.3.10",
"chai-as-promised": "^7.1.1",
"@openzeppelin/contracts": "^5.0.0",
"ripemd160": "^2.0.1"
"@openzeppelin/contracts": "^5.4.0"
},
"devDependencies": {
"hardhat": "^2.22.18",
"@nomicfoundation/hardhat-ethers": "^3.0.8",
"ethers": "^6.10.0"
"ethers": "^6.10.0",
"chai": "^4.3.10",
"chai-as-promised": "^7.1.1",
"ripemd160": "^2.0.3"
}
}
Loading