Skip to content

Commit

Permalink
exchange conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
10xSebastian committed Aug 7, 2023
1 parent e6a92ca commit 25e93bb
Show file tree
Hide file tree
Showing 16 changed files with 297 additions and 111 deletions.
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@ BNB Smart Chain: [](https://bscscan.com/address/)

Polygon (POS): [](https://polygonscan.com/address/)

Avalanche: [](https://snowtrace.io/address/)

Fantom: [](https://ftmscan.com/address/)

Gnosis: [](https://gnosisscan.io/address/)

Arbitrum: [](https://arbiscan.io/address/)

Optimsm: [](https://optimistic.etherscan.io/address/)

## Summary

This set of smart contracts enables decentralized payments.
This smart contract enables decentralized payments with auto-conversion and payment-fee extraction.

The main purpose of this set of smart contracts evolves around the `pay` function,
which allows a sender to pay a receiver with any convertible token
The main purpose of this smart contract evolves around the `pay` function.

This allows for NATIVE to NATIVE, NATIVE to TOKEN, TOKEN to NATIVE and TOKEN_A to TOKEN_B payments.
This smart contract allows for NATIVE to NATIVE, NATIVE to TOKEN, TOKEN to NATIVE, WRAPPED to NATIVE, NATIVE to WRAPPED and TOKEN_A to TOKEN_B payments.

## Functionalities

Expand Down Expand Up @@ -67,7 +76,7 @@ yarn test
Test single files:

```
npx hardhat test test/ethereum/DePayRouterV1.spec.ts --config hardhat.config.ethereum.ts
npx hardhat test test/ethereum/pay_with_exchange_conversion.spec.ts --config hardhat.config.ethereum.ts
```

### Deploy
Expand Down
140 changes: 103 additions & 37 deletions contracts/DePayRouterV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,75 +34,118 @@ contract DePayRouterV2 is Ownable {
uint256 value
);

// The main pay functionality
// perform a payment
function pay(
uint amountIn, // amount paid in from sender
address tokenIn, // address of the token paid in from sender
address exchangeAddress, // address of the exchange required for conversion
bytes memory exchangeCall, // calldata required to perform token conversion
address tokenOut, // address of the token paid out to payment/fee receiver
uint paymentAmount, // amount paid to payment receiver
address paymentReceiver, // address of the payment receiver
uint feeAmount, // amount paid to fee receiver
address feeReceiver, // address of the fee receiver
uint deadline // timestamp after which the payment is supposed to fail/revert

// [
// 0: amountIn,
// 1: paymentAmount,
// 2: feeAmount
// ]
uint256[] calldata amounts,

// [
// 0: tokenInAddress,
// 1: exchangeAddress,
// 2: tokenOutAddress,
// 3: paymentReceiverAddress,
// 4: feeAddress
// ]
address[] calldata addresses,

// pull requires approve, push is pushing the token prior calling
// [
// 0: exchangePullToken,
// 1: receiverPullToken
// ]
bool[] calldata pull,

// [
// 0: exchangeCallData,
// 1: receiverCallData
// ]
bytes[] calldata calls,

// timestamp after which the payment will be declined
uint256 deadline

) external payable returns(bool){

// Make sure payment deadline has not been passed, yet
require(deadline > block.timestamp, "DePay: Payment deadline has passed!");

// Store tokenOut balance prior to conversion & payment
uint balanceBefore;
if(tokenOut == NATIVE) {
balanceBefore = address(this).balance - msg.value;
// Store tokenIn balance prior to payment
uint256 balanceInBefore;
if(addresses[0] == NATIVE) {
balanceInBefore = address(this).balance - msg.value;
} else {
balanceInBefore = IERC20(addresses[0]).balanceOf(address(this));
}

// Store tokenOut balance prior to payment
uint256 balanceOutBefore;
if(addresses[2] == NATIVE) {
balanceOutBefore = address(this).balance - msg.value;
} else {
balanceBefore = IERC20(tokenOut).balanceOf(address(this));
balanceOutBefore = IERC20(addresses[2]).balanceOf(address(this));
}

// Make sure that the sender has paid in the correct token & amount
if(tokenIn == NATIVE) {
require(msg.value >= amountIn, 'DePay: Insufficient amount paid in!');
if(addresses[0] == NATIVE) {
require(msg.value >= amounts[0], 'DePay: Insufficient amount paid in!');
} else {
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
IERC20(addresses[0]).safeTransferFrom(msg.sender, address(this), amounts[0]);
}

// Perform conversion if required
if(exchangeAddress != address(0)) {
require(exchanges[exchangeAddress], 'DePay: Exchange has not been approved!');
if(addresses[1] != address(0)) {
require(exchanges[addresses[1]], 'DePay: Exchange has not been approved!');
bool success;
if(tokenIn == NATIVE) {
(success,) = exchangeAddress.call{value: msg.value}(exchangeCall);
if(addresses[0] == NATIVE) {
(success,) = addresses[1].call{value: msg.value}(calls[0]);
} else {
(success,) = exchangeAddress.call(exchangeCall);
if(pull[0]) {
IERC20(addresses[0]).safeApprove(addresses[1], amounts[0]);
} else { // push
IERC20(addresses[0]).safeTransfer(addresses[1], amounts[0]);
}
(success,) = addresses[1].call(calls[0]);
}
require(success, "DePay: exchange call failed!");
}

// Pay paymentReceiver
if(tokenOut == NATIVE) {
(bool success,) = paymentReceiver.call{value: paymentAmount}(new bytes(0));
if(addresses[2] == NATIVE) {
(bool success,) = addresses[3].call{value: amounts[1]}(new bytes(0));
require(success, 'DePay: NATIVE payment receiver pay out failed!');
emit Transfer(msg.sender, paymentReceiver, paymentAmount);
emit Transfer(msg.sender, addresses[3], amounts[1]);
} else {
IERC20(tokenOut).safeTransfer(paymentReceiver, paymentAmount);
IERC20(addresses[2]).safeTransfer(addresses[3], amounts[1]);
}

// Pay feeReceiver
if(feeReceiver != address(0)) {
if(tokenOut == NATIVE) {
(bool success,) = feeReceiver.call{value: feeAmount}(new bytes(0));
if(addresses[4] != address(0)) {
if(addresses[2] == NATIVE) {
(bool success,) = addresses[4].call{value: amounts[2]}(new bytes(0));
require(success, 'DePay: NATIVE fee receiver pay out failed!');
emit Transfer(msg.sender, paymentReceiver, paymentAmount);
emit Transfer(msg.sender, addresses[4], amounts[2]);
} else {
IERC20(tokenOut).safeTransfer(feeReceiver, feeAmount);
IERC20(addresses[2]).safeTransfer(addresses[4], amounts[2]);
}
}

// Ensure balance remained after conversion & payout
if(tokenOut == NATIVE) {
require(address(this).balance >= balanceBefore, 'DePay: Insufficient balance after payment!');
// Ensure balances of tokenIn remained
if(addresses[0] == NATIVE) {
require(address(this).balance >= balanceInBefore, 'DePay: Insufficient balanceIn after payment!');
} else {
require(IERC20(addresses[0]).balanceOf(address(this)) >= balanceInBefore, 'DePay: Insufficient balanceIn after payment!');
}

// Ensure balances of tokenOut remained
if(addresses[2] == NATIVE) {
require(address(this).balance >= balanceOutBefore, 'DePay: Insufficient balanceOut after payment!');
} else {
require(IERC20(tokenOut).balanceOf(address(this)) >= balanceBefore, 'DePay: Insufficient balance after payment!');
require(IERC20(addresses[2]).balanceOf(address(this)) >= balanceOutBefore, 'DePay: Insufficient balanceOut after payment!');
}

return true;
Expand All @@ -120,4 +163,27 @@ contract DePayRouterV2 is Ownable {
return true;
}

// Decodes call (to exchange or receiver contract) and determines type (push vs. pull)
function decodeCall(bytes memory encoded) internal pure returns (bool pull, bytes memory callData) {
assembly {
// Load pull
pull := iszero(iszero(byte(31, mload(encoded))))

// Load the length of callData, which is found at position encoded + 32.
let callDataLength := mload(add(encoded, 32))

// Allocate memory for callData.
callData := mload(0x40) // fetch the free memory pointer
mstore(0x40, add(callData, add(callDataLength, 0x20))) // adjust the free memory pointer

// Store the length of callData.
mstore(callData, callDataLength)

// Copy callData.
let callDataStart := add(encoded, 0x40) // start of 'bytes' data
for { let end := add(callDataStart, callDataLength) } lt(callDataStart, end) { callDataStart := add(callDataStart, 0x20) } {
mstore(add(callData, sub(callDataStart, add(encoded, 0x40))), mload(callDataStart))
}
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"devDependencies": {
"@depay/web3-blockchains": "^8.4.0",
"@depay/web3-client-evm": "^10.16.3",
"@depay/web3-exchanges-evm": "^13.1.0",
"@depay/web3-exchanges-evm": "^13.2.0",
"@depay/web3-tokens-evm": "^10.1.0",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-etherscan": "^2.1.1",
Expand Down
24 changes: 24 additions & 0 deletions test/_helpers/callData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ethers } from 'hardhat'

export default ({
address,
api,
provider,
method,
params
})=> {
const contract = new ethers.Contract(address, api, provider)
if(contract[method] === undefined){
let fragment = contract.interface.fragments.find((fragment) => {
return fragment.name == method
})
method = `${method}(${fragment.inputs.map((input)=>input.type).join(',')})`;
}
let fragment = contract.interface.fragments.find((fragment) => {
return fragment.name == method || `${fragment.name}(${fragment.inputs.map((input)=>input.type).join(',')})` == method
})
const paramsAsArray = fragment.inputs.map((input) => {
return params[input.name]
})
return contract.interface.encodeFunctionData(method, paramsAsArray)
}
Loading

0 comments on commit 25e93bb

Please sign in to comment.