Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Payment Settlement Smart Contract #1

Merged
merged 23 commits into from
Jan 15, 2024
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
39 changes: 39 additions & 0 deletions .github/workflows/smart_contracts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Smart Contracts CI

on:
push:
branches:
- master
pull_request:


jobs:
hardhat:
strategy:
fail-fast: true

name: Hardhat project
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./contracts
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Setup Node 16
uses: actions/setup-node@v3
with:
node-version: "16.x"
cache: "npm"
cache-dependency-path: "contracts/yarn.lock"

- name: Install Node dependencies
run: |
yarn install

- name: Run Hardhat tests
run: |
yarn test
id: hardhat-test
13 changes: 13 additions & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
.env
coverage
coverage.json
typechain
typechain-types

# Hardhat files
cache
artifacts

deployments/localhost

61 changes: 60 additions & 1 deletion contracts/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,60 @@
# GHOPAY
# GHOPAY - Smart Contracts
This directory contains the smart contracts related tests, utilities and deployment addresses for the Payment Settlement.

## How does it work ?
The smart contracts themselves have essentially three tasks:
1. Verify the user signature that was passed in by the Gelato relay in terms of amount and receiver
2. Use user signature to pull in GHO
3. Swap a fraction of the GHO for gas fee token (usually eth / native token) expected by relay
4. Pay the relay
5. Transfer remaining Gho to the receiver


## Installation
`yarn install`

## Run tests
`yarn test`


## Sepolia Deployment

### Warning:
Note that on Sepolia we have deployed the [PaymentSettlementTestHarness](contracts/contracts/test/PaymentSettlementTestHarness.sol) contract which extends the [PaymentSettlement](contracts/contracts/PaymentSettlement.sol) with some unsafe methods added for testing purposes.
One should NEVER deploy this version of the contract on a network were it will handle actual value.


### Run Deploy:
Delete deployment outputs of previous deployment if you want to redeploy:
`rm -rf deployments/sepolia`

Deploy on actual sepolia:
`deploy:sepolia --network sepolia`

Test deploy on local fork:
`deploy:sepolia --network localhost`

## Send Payment (on Sepolia)

You can use the [payment script](contracts/scripts/sendPayment.ts) to send a GHO payment gaslessly via the Gelato relay.
For this you will have to:
1. Adjust the `receiver` and `amount` variables in the script to your liking
2. Make sure you have set the `SEPOLIA_PRIVATE_KEY` environment variable to the private key of an account with sufficient GHO tokens on sepolia
3. Run `yarn send-payment:sepolia` to make the payment
4. Wait for the script to finish. In the success case the last emitted logs should llook something like this:
```
Task Status {
chainId: 11155111,
taskId: '0xaa31e9ec7dc88958e8d263b59575b2c22452985815552c4e189baa11e7141104',
taskState: 'ExecSuccess',
creationDate: '2024-01-14T06:51:54.367Z',
transactionHash: '0xa38b90e8af1f66425961e3e09deca5bc184b94cf10be95535893338ce7dcf1c9',
executionDate: '2024-01-14T06:52:01.839Z',
blockNumber: 5083156,
gasUsed: '295101',
effectiveGasPrice: '153284042'
}
```



6 changes: 6 additions & 0 deletions contracts/constants/addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const ghoAddress = "0xc4bF5CbDaBE595361438F8c6a187bDc330539c60";
export const ghoWhale = "0xCb3404A4288182303fE4df9bb8F9D239447E5781";
export const uniV3PoolAddress ="0xba09E75749973Ce095121dfc02ec8D8364eE1AC9";
export const paymentSettlementAddress = "0x62E028A0FeE5925de63F9bAb1a31Fb922C51386C";
export const wethAddress = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
export const ethAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
259 changes: 259 additions & 0 deletions contracts/contracts/PaymentSettlement.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import {
GelatoRelayContext
} from "@gelatonetwork/relay-context/contracts/GelatoRelayContext.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

import "./interfaces/IUniV3Pool.sol";
import "./interfaces/IWETH.sol";


contract PaymentSettlement is GelatoRelayContext, EIP712, Ownable, Pausable {

using SafeERC20 for IERC20;

// Copied from: https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/TickMath.sol#L13
/// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK)
uint160 internal constant MIN_SQRT_RATIO = 4295128739;
/// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK)
uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;

bytes32 public constant PAY_TYPEHASH =
keccak256("Pay(address receiver,uint256 permitNonce)");

IUniV3Pool internal immutable uniV3Pool;
IWETH public immutable weth;

address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address internal allowedCallback;

struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
struct PermitData {
uint256 deadline;
Signature signature;
}

struct PaymentData {
address token;
address from;
address to;
uint256 amount;
PermitData permitData;
}

event Payment(
address indexed token,
address indexed from,
address indexed to,
uint256 amount
);

modifier checkCallback() {
require(msg.sender == allowedCallback, "AuctionBot: msg.sender != allowedCallback");
_;
allowedCallback = address(0);
}

constructor(address payable _uniV3Pool, address payable _weth) EIP712("PaymentSettlement", "1") {
uniV3Pool = IUniV3Pool(_uniV3Pool);
weth = IWETH(_weth);
}

function pay(
PaymentData calldata _paymentData,
Signature calldata _signature
) external whenNotPaused onlyGelatoRelay {
address feeToken = address(_getFeeToken());
uint256 fee = _getFee();
_settlePayment(_paymentData, _signature, feeToken, fee);
}

// This function is used to verify the data and revert the transaction
// Only call as static call to avoid wasting gas
function verifyData(
PaymentData calldata _paymentData,
Signature calldata _signature,
address _feeToken,
uint256 _fee
) external whenNotPaused returns (bool) {

try this.verifyDataReverting(_paymentData, _signature, _feeToken, _fee) {
revert("PaymentSettlement: verifyDataReverting did not revert");
} catch Error(string memory reason) {
// Check that the right error was thrown
if (keccak256(abi.encodePacked(reason)) != keccak256(abi.encodePacked("PaymentSettlement: verification complete"))) {
return(false);
}
} catch {
return (false);
}

return(true);
}

// This function is used to verify the data and revert the transaction
// This will always revert and is only meant to be called by verifyData
function verifyDataReverting(
PaymentData calldata _paymentData,
Signature calldata _signature,
address _feeToken,
uint256 _fee
) external {
_settlePayment(_paymentData, _signature, _feeToken, _fee);
revert("PaymentSettlement: verification complete");
}


function verifySignature(
PaymentData calldata _paymentData,
Signature calldata _signature
) external view returns (bool) {
bool result = _verifySignature(_paymentData, _signature);
return result;
}

function pause() external onlyOwner whenNotPaused {
_pause();
}

function unpause() external onlyOwner whenPaused {
_unpause();
}

function transferOwner(address _newOwner) external onlyOwner {
_transferOwnership(_newOwner);
}

// @dev Called by UniswapV3Pool after a swap
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes memory data
) external checkCallback {
(address paymentToken) = abi.decode(data, (address));
uint256 amountToRepay = amount1Delta > 0 ? uint256(amount1Delta) : uint256(amount0Delta);
IERC20(paymentToken).safeTransfer(msg.sender, amountToRepay);
}

// ====== INTERNAL FUNCTIONS ======

function _settlePayment(
PaymentData calldata _paymentData,
Signature calldata _signature,
address _feeToken,
uint256 _fee
) internal {

require(_verifySignature(_paymentData, _signature), "PaymentSettlement: invalid signature");

IERC20Permit(_paymentData.token).permit(
_paymentData.from,
address(this),
_paymentData.amount,
_paymentData.permitData.deadline,
_paymentData.permitData.signature.v,
_paymentData.permitData.signature.r,
_paymentData.permitData.signature.s
);

IERC20(_paymentData.token).safeTransferFrom(
_paymentData.from,
address(this),
_paymentData.amount
);

_obtainFeeToken(_paymentData.token, _paymentData.amount, _feeToken, _fee);
_transferRelayFee();

SafeERC20.safeTransfer(
IERC20(_paymentData.token),
_paymentData.to,
IERC20(_paymentData.token).balanceOf(address(this))
);

emit Payment(
_paymentData.token,
_paymentData.from,
_paymentData.to,
_paymentData.amount
);

}


function _obtainFeeToken(
address _paymentToken,
uint256 _paymentAmount,
address _feeToken,
uint256 _fee
) internal {
if(_feeToken == _paymentToken) {
return;
}

bool isFeeTokenETH = _feeToken == ETH;
if(isFeeTokenETH) {
_feeToken = address(weth);
}

IUniV3Pool uniV3Pool = _getUniV3Pool();
IERC20(_paymentToken).approve(address(uniV3Pool), _paymentAmount);

bool zeroForOne = uniV3Pool.token0() == _paymentToken;
int256 amountSpecified = -int256(_fee);
uint160 sqrtPriceLimitX96 = 0;
allowedCallback = address(uniV3Pool);
uniV3Pool.swap(
address(this),
zeroForOne,
amountSpecified,
zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
abi.encode(_paymentToken)
);

if(isFeeTokenETH) {
weth.withdraw(_fee);
}
}

function _verifySignature(
PaymentData calldata _paymentData,
Signature calldata _signature
) internal view returns(bool) {
uint256 userNonce = IERC20Permit(_paymentData.token).nonces(_paymentData.from);

bytes32 paymentStructHash = keccak256(abi.encode(
PAY_TYPEHASH,
_paymentData.to,
userNonce
));
bytes32 hash = _hashTypedDataV4(paymentStructHash);
address signer = ECDSA.recover(hash, _signature.v, _signature.r, _signature.s);
bool result = signer == _paymentData.from;
return(result);
}

function _getUniV3Pool() internal virtual view returns(IUniV3Pool) {
return(uniV3Pool);
}

receive() external payable {
require(msg.sender == address(weth), "PaymentSettlement: invalid msg.sender");
}

}
10 changes: 10 additions & 0 deletions contracts/contracts/interfaces/IERC20Complete.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";

interface IERC20Complete is IERC20, IERC20Permit {
function decimals() external view returns (uint8);
function name() external view returns (string memory);
}
Loading