-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Payment Settlement Smart Contract (#1)
* 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
Showing
21 changed files
with
6,527 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} | ||
``` | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.