Skip to content

Commit

Permalink
Added contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
KyrylR committed Jan 20, 2024
1 parent f12f6f8 commit 433c13c
Show file tree
Hide file tree
Showing 18 changed files with 14,003 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/contracts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: "contracts"

on:
push:
branches:
- main
pull_request:
branches:
- main
- dev

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3

- name: Setup node
uses: actions/setup-node@v3
with:
node-version: "16.18.x"
cache: npm

- name: Install packages
run: cd contracts && npm install
shell: bash

- name: Run tests
run: npm run test && cd ..
8 changes: 8 additions & 0 deletions contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Deployer private key
PRIVATE_KEY=YOUR PRIVATE KEY

# RPC Endpoints
INFURA_KEY=INFURA PROJECT ID

# Additional keys
ETHERSCAN_KEY=ETHERSCAN API KEY
19 changes: 19 additions & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
node_modules
.env
.DS_Store
.npmrc

# Hardhat files
cache
artifacts
coverage.json
coverage
generated-markups
abi
docs/**/*.md

# Typechain generated files
generated-types

# JetBrains IDE files
.idea
13 changes: 13 additions & 0 deletions contracts/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
.env
.DS_Store
package-lock.json

# Hardhat files
cache
artifacts
coverage.json
coverage

# Typechain generated files
generated-types
21 changes: 21 additions & 0 deletions contracts/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 99,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false
}
},
{
"files": "*.ts",
"options": {
"printWidth": 120,
"tabWidth": 2
}
}
]
}
4 changes: 4 additions & 0 deletions contracts/.solcover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
skipFiles: [],
configureYulOptimizer: true,
};
13 changes: 13 additions & 0 deletions contracts/.solhint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "solhint:recommended",
"rules": {
"reentrancy": "warn",
"prettier/prettier": "warn",
"modifier-name-mixedcase": "warn",
"no-empty-blocks": "warn",
"func-visibility": "warn",
"max-states-count": "warn",
"not-rely-on-time": "warn",
"compiler-version": "warn"
}
}
1 change: 1 addition & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Taprootized Atomic Swaps ETH contracts
205 changes: 205 additions & 0 deletions contracts/contracts/Depositor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";

library PoseidonUnit1L {
function poseidon(uint256[1] calldata) public pure returns (uint256) {}
}

/**
* @title Taprootized Atomic Swaps Contract
* @notice This contract facilitates atomic swaps using between Bitcoin and Ethereum, ensuring privacy and security for both parties.
*
* Functionality:
* - Users can deposit ETH into the contract, specifying a recipient, a Poseidon hash of a secret, and a lock time.
* - Deposits can only be withdrawn by the recipient if they provide the correct secret that matches the hash used at the time of deposit.
* - Deposits are locked for a minimum duration, defined by the contract, ensuring that the depositor cannot reclaim them before this period.
* - If the recipient does not withdraw the funds within the lock time, the depositor can reclaim them.
*
* Limitations:
* - The contract only supports ETH deposits and does not handle ERC20 tokens or other assets.
* - The lock time is fixed and cannot be modified after a deposit is made.
* - The contract does not support incremental deposits to the same secret hash; each deposit must have a unique secret hash.
*/
contract Depositor {
/**
* @notice Represents the minimum time (in seconds) that a deposit must be locked in the contract. Set to one hour.
*/
uint256 public constant HOUR = 60 * 60;

/**
* @notice Struct to store details of each deposit.
* @param sender The Ethereum address of the user who created the deposit.
* @param recipient The Ethereum address of the user eligible to withdraw the deposit using the correct secret.
* @param amount The amount of ETH deposited.
* @param lockTime The UNIX timestamp until which the deposit is locked and cannot be withdrawn.
* @param isWithdrawn Boolean flag indicating whether the deposit has been withdrawn. This helps prevent double spending.
*/
struct Deposit {
address sender;
address recipient;
uint256 amount;
uint256 lockTime;
bool isWithdrawn;
}

/**
* @notice Mapping of Poseidon hash of the secret to Deposit structs.
*/
mapping(bytes32 => Deposit) public deposits;

/**
* @notice Emitted when a new deposit is made.
* @param sender The Ethereum address of the user who created the deposit.
* @param recipient The Ethereum address of the user eligible to withdraw the deposit using the correct secret.
* @param amount The amount of ETH deposited.
* @param lockTime The UNIX timestamp until which the deposit is locked and cannot be withdrawn.
* @param secretHash The Poseidon hash of the secret required to withdraw the deposit.
*/
event Deposited(
address indexed sender,
address indexed recipient,
uint256 amount,
uint256 lockTime,
bytes32 secretHash
);

/**
* @notice Emitted when a deposit is successfully withdrawn.
* @param recipient The Ethereum address of the user who withdrew the deposit.
* @param amount The amount of ETH withdrawn.
* @param secret The secret used to withdraw the deposit.
* @param secretHash The Poseidon hash of the secret used to create the deposit.
*/
event Withdrawn(address indexed recipient, uint256 amount, bytes32 secret, bytes32 secretHash);

/**
* @notice Emitted when deposited funds are restored to the sender after the lock time has expired.
* @param sender The Ethereum address of the sender to whom the funds are restored.
* @param amount The amount of ETH restored.
* @param secretHash The Poseidon hash of the secret originally used for the deposit.
*/
event Restored(address indexed sender, uint256 amount, bytes32 secretHash);

/**
* @notice Error thrown when a deposit is attempted with an amount of 0 ETH.
* @param sentAmount The amount of ETH attempted to be deposited, expected to be greater than 0.
*/
error InsufficientDepositAmount(uint256 sentAmount);

/**
* @notice Error thrown when a deposit with the given secret hash already exists.
* @param secretHash The Poseidon hash of the secret for which a deposit already exists.
*/
error DepositAlreadyExists(bytes32 secretHash);

/**
* @notice Error thrown when the provided lock time for a deposit is too short.
* @param providedLockTime The lock time provided for the deposit.
* @param minimumLockTime The minimum required lock time, typically one hour.
*/
error LockTimeTooShort(uint256 providedLockTime, uint256 minimumLockTime);

/**
* @notice Error thrown when an operation (like withdrawal or restoration) is attempted on a non-existent deposit.
* @param secretHash The Poseidon hash of the secret for which the deposit does not exist.
*/
error DepositDoesNotExist(bytes32 secretHash);

/**
* @notice Error thrown when an attempt is made to withdraw or restore an already withdrawn deposit.
* @param secretHash The Poseidon hash of the secret for the deposit that has already been withdrawn.
*/
error DepositAlreadyWithdrawn(bytes32 secretHash);

/**
* @notice Error thrown when an attempt is made to restore funds before the lock time has expired.
* @param currentTime The current UNIX timestamp.
* @param lockTime The UNIX timestamp until which the deposit is locked.
*/
error TimeLockNotExpired(uint256 currentTime, uint256 lockTime);

/**
* @notice Error thrown when an attempt is made to make a deposit with the zero Ethereum address as the recipient.
*/
error ZeroAddressNotAllowed();

/**
* @notice Allows a user to deposit ETH into the contract with a given recipient, secret hash, and lock time.
* @dev Emits a `Deposited` event upon successful deposit. Checks for zero deposit amount, duplicate deposits,
* short lock times, and zero address recipient.
* @param recipient_ The Ethereum address of the recipient eligible to withdraw the deposit using the correct secret.
* @param secretHash_ The Poseidon hash of the secret required for the recipient to withdraw the deposit.
* @param lockTime_ The duration (in seconds) for which the deposit is locked and cannot be withdrawn.
*/
function deposit(address recipient_, bytes32 secretHash_, uint256 lockTime_) external payable {
if (msg.value == 0) revert InsufficientDepositAmount(msg.value);
if (deposits[secretHash_].amount != 0) revert DepositAlreadyExists(secretHash_);
if (lockTime_ < HOUR) revert LockTimeTooShort(lockTime_, HOUR);
if (recipient_ == address(0)) revert ZeroAddressNotAllowed();

deposits[secretHash_] = Deposit({
sender: msg.sender,
recipient: recipient_,
amount: msg.value,
lockTime: block.timestamp + lockTime_,
isWithdrawn: false
});

emit Deposited(msg.sender, recipient_, msg.value, lockTime_, secretHash_);
}

/**
* @notice Allows the recipient to withdraw a deposit using the correct secret.
* @dev Emits a `Withdrawn` event upon successful withdrawal. Checks for non-existent or already withdrawn deposits.
* Uses the PoseidonUnit1L library to hash the provided secret.
* @param secret_ The prototype of the `secretHash` used in the deposit function.
*/
function withdraw(bytes32 secret_) external {
bytes32 secretHash_ = bytes32(PoseidonUnit1L.poseidon([uint256(secret_)]));

Deposit storage userDeposit = deposits[secretHash_];

uint256 depositAmount_ = userDeposit.amount;
address depositRecipient_ = userDeposit.recipient;

if (depositAmount_ == 0) revert DepositDoesNotExist(secretHash_);
if (userDeposit.isWithdrawn) revert DepositAlreadyWithdrawn(secretHash_);

userDeposit.isWithdrawn = true;

(bool success_, bytes memory data_) = payable(depositRecipient_).call{
value: depositAmount_
}("");
Address.verifyCallResult(success_, data_);

emit Withdrawn(depositRecipient_, depositAmount_, secret_, secretHash_);
}

/**
* @notice Allows the sender to restore a deposit back to themselves after the lock time has expired.
* @dev Emits a `Restored` event upon successful restoration. Checks for non-existent, already withdrawn,
* or not yet expired deposits.
* @param secretHash_ The Poseidon hash of the secret used when the deposit was created.
*/
function restore(bytes32 secretHash_) external {
Deposit storage userDeposit = deposits[secretHash_];

uint256 depositAmount_ = userDeposit.amount;

if (depositAmount_ == 0) revert DepositDoesNotExist(secretHash_);
if (userDeposit.isWithdrawn) revert DepositAlreadyWithdrawn(secretHash_);
if (userDeposit.lockTime > block.timestamp)
revert TimeLockNotExpired(block.timestamp, userDeposit.lockTime);

userDeposit.isWithdrawn = true;

(bool success_, bytes memory data_) = payable(userDeposit.sender).call{
value: depositAmount_
}("");
Address.verifyCallResult(success_, data_);

emit Restored(userDeposit.sender, depositAmount_, secretHash_);
}
}
17 changes: 17 additions & 0 deletions contracts/deploy/1_depositor.migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Deployer, Reporter } from "@solarity/hardhat-migrate";

import { Depositor__factory } from "@ethers-v6";

import { poseidonContract } from "circomlibjs";

export = async (deployer: Deployer) => {
await deployer.deploy({
contractName: "contracts/Depositor.sol:PoseidonUnit1L",
bytecode: poseidonContract.createCode(1),
abi: poseidonContract.generateABI(1),
});

const depositor = await deployer.deploy(Depositor__factory);

Reporter.reportContracts(["Depositor", await depositor.getAddress()]);
};
Loading

0 comments on commit 433c13c

Please sign in to comment.