Skip to content

Commit

Permalink
Implement Payment Settlement Smart Contract (#1)
Browse files Browse the repository at this point in the history
* Payment Settlement contract

* Contract tests

* Add implementation of swap to payment token

* Payment Settlement tests

* Add functionality to change uni router to test contract

* Goerli deploy script

* Goerli deploy output

* Goerli verification command

* rm

* Add script to send payment

* Adjust to using uniV3Pool directly

* Fix tests

* Sepolia deploy script

* Sepolia deployment output

* Adjusting to new gho address and uni pool

* New Sepolia deployment

* Test case for two payments in a row

* Fix issue with two approvals in a row

* Deploy approval fix

* Refactor scripts and adjust README

* Add Ci for smart contracts

* Fix ci

* Remove irrelevant goerli deployment

---------
  • Loading branch information
ckoopmann authored Jan 15, 2024
1 parent f5a5315 commit 2bac714
Show file tree
Hide file tree
Showing 21 changed files with 6,527 additions and 1 deletion.
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

0 comments on commit 2bac714

Please sign in to comment.