Skip to content

Commit

Permalink
feat: ether to smart wallet payment (#780)
Browse files Browse the repository at this point in the history
  • Loading branch information
leoslr committed Feb 28, 2022
1 parent c17a9ac commit ee3436e
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 86 deletions.
125 changes: 61 additions & 64 deletions packages/smart-contracts/src/contracts/EthereumFeeProxy.sol
Original file line number Diff line number Diff line change
@@ -1,78 +1,75 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';

/**
* @title EthereumFeeProxy
* @notice This contract performs an Ethereum transfer with a Fee sent to a third address and stores a reference
*/
contract EthereumFeeProxy is ReentrancyGuard{
// Event to declare a transfer with a reference
event TransferWithReferenceAndFee(
address to,
uint256 amount,
bytes indexed paymentReference,
uint256 feeAmount,
address feeAddress
);
contract EthereumFeeProxy is ReentrancyGuard {
// Event to declare a transfer with a reference
event TransferWithReferenceAndFee(
address to,
uint256 amount,
bytes indexed paymentReference,
uint256 feeAmount,
address feeAddress
);

// Fallback function returns funds to the sender
receive() external payable {
revert("not payable receive");
}
// Fallback function returns funds to the sender
receive() external payable {
revert('not payable receive');
}

/**
* @notice Performs an Ethereum transfer with a reference
* @param _to Transfer recipient
* @param _paymentReference Reference of the payment related
* @param _feeAmount The amount of the payment fee (part of the msg.value)
* @param _feeAddress The fee recipient
*/
function transferWithReferenceAndFee(
address payable _to,
bytes calldata _paymentReference,
uint256 _feeAmount,
address payable _feeAddress
) external payable {
transferExactEthWithReferenceAndFee(
_to,
msg.value - _feeAmount,
_paymentReference,
_feeAmount,
_feeAddress
);
}

/**
* @notice Performs an Ethereum transfer with a reference
* @param _to Transfer recipient
* @param _paymentReference Reference of the payment related
* @param _feeAmount The amount of the payment fee (part of the msg.value)
* @param _feeAddress The fee recipient
*/
function transferWithReferenceAndFee(
address payable _to,
bytes calldata _paymentReference,
uint256 _feeAmount,
address payable _feeAddress
)
external
payable
{
transferExactEthWithReferenceAndFee(
_to,
msg.value - _feeAmount,
_paymentReference,
_feeAmount,
_feeAddress
);
}
/**
* @notice Performs an Ethereum transfer with a reference with an exact amount of eth
* @param _to Transfer recipient
* @param _amount Amount to transfer
* @param _paymentReference Reference of the payment related
* @param _feeAmount The amount of the payment fee (part of the msg.value)
* @param _feeAddress The fee recipient
*/
function transferExactEthWithReferenceAndFee(
address payable _to,
uint256 _amount,
bytes calldata _paymentReference,
uint256 _feeAmount,
address payable _feeAddress
) public payable nonReentrant {
(bool sendSuccess, ) = _to.call{value: _amount}('');
require(sendSuccess, 'Could not pay the recipient');

_feeAddress.transfer(_feeAmount);

/**
* @notice Performs an Ethereum transfer with a reference with an exact amount of eth
* @param _to Transfer recipient
* @param _amount Amount to transfer
* @param _paymentReference Reference of the payment related
* @param _feeAmount The amount of the payment fee (part of the msg.value)
* @param _feeAddress The fee recipient
*/
function transferExactEthWithReferenceAndFee(
address payable _to,
uint256 _amount,
bytes calldata _paymentReference,
uint256 _feeAmount,
address payable _feeAddress
)
nonReentrant
public
payable
{
_to.transfer(_amount);
_feeAddress.transfer(_feeAmount);
// transfer the remaining ethers to the sender
payable(msg.sender).transfer(msg.value - _amount - _feeAmount);
// transfer the remaining ethers to the sender
(bool sendBackSuccess, ) = payable(msg.sender).call{
value: msg.value - _amount - _feeAmount
}('');
require(sendBackSuccess, 'Could not send remaining funds to the payer');

emit TransferWithReferenceAndFee(_to, _amount, _paymentReference, _feeAmount, _feeAddress);
}
}
emit TransferWithReferenceAndFee(_to, _amount, _paymentReference, _feeAmount, _feeAddress);
}
}
38 changes: 19 additions & 19 deletions packages/smart-contracts/src/contracts/EthereumProxy.sol
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


/**
* @title EthereumProxy
* @notice This contract performs an Ethereum transfer and stores a reference
*/
contract EthereumProxy {
// Event to declare a transfer with a reference
event TransferWithReference(address to, uint256 amount, bytes indexed paymentReference);
// Event to declare a transfer with a reference
event TransferWithReference(address to, uint256 amount, bytes indexed paymentReference);

// Fallback function returns funds to the sender
receive() external payable {
revert("not payable receive");
}
// Fallback function returns funds to the sender
receive() external payable {
revert('not payable receive');
}

/**
* @notice Performs an Ethereum transfer with a reference
* @param _to Transfer recipient
* @param _paymentReference Reference of the payment related
*/
function transferWithReference(address payable _to, bytes calldata _paymentReference)
external
payable
{
_to.transfer(msg.value);
emit TransferWithReference(_to, msg.value, _paymentReference);
}
/**
* @notice Performs an Ethereum transfer with a reference
* @param _to Transfer recipient
* @param _paymentReference Reference of the payment related
*/
function transferWithReference(address payable _to, bytes calldata _paymentReference)
external
payable
{
(bool success, ) = _to.call{value: msg.value}('');
require(success, 'Could not pay the recipient');
emit TransferWithReference(_to, msg.value, _paymentReference);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

/// @title EtherPaymentFallback - A contract that has a fallback to accept ether payments
/// @author Richard Meissner - <richard@gnosis.pm>
contract EtherPaymentFallback {
event SafeReceived(address indexed sender, uint256 value);

/// @dev Fallback function accepts Ether transactions.
receive() external payable {
emit SafeReceived(msg.sender, msg.value);
}

constructor() {}
}
36 changes: 36 additions & 0 deletions packages/smart-contracts/src/contracts/test/GnosisProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GnosisSafeProxy {
address internal singleton;

/// @dev Constructor function sets address of singleton contract.
/// @param _singleton Singleton address.
constructor(address _singleton) {
require(_singleton != address(0), 'Invalid singleton address provided');
singleton = _singleton;
}

/// @dev Fallback function forwards all transactions and returns all received return data.
fallback() external payable {
// solhint-disable-next-line no-inline-assembly
assembly {
let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
// 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
if eq(
calldataload(0),
0xa619486e00000000000000000000000000000000000000000000000000000000
) {
mstore(0, _singleton)
return(0, 0x20)
}
calldatacopy(0, 0, calldatasize())
let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
if eq(success, 0) {
revert(0, returndatasize())
}
return(0, returndatasize())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
EthConversionProxy,
ChainlinkConversionPath__factory,
AggregatorMock__factory,
EtherPaymentFallback,
GnosisSafeProxy,
EtherPaymentFallback__factory,
GnosisSafeProxy__factory,
} from '../../src/types';
import { BigNumber, Signer } from 'ethers';
import { expect, use } from 'chai';
Expand Down Expand Up @@ -35,6 +39,8 @@ describe('contract: EthConversionProxy', () => {
let testEthConversionProxy: EthConversionProxy;
let ethFeeProxy: EthereumFeeProxy;
let chainlinkPath: ChainlinkConversionPath;
let gnosisSafeProxy: GnosisSafeProxy;
let etherPaymentFallback: EtherPaymentFallback;
const networkConfig = network.config as HttpNetworkConfig;
const provider = new ethers.providers.JsonRpcProvider(networkConfig.url);

Expand All @@ -49,6 +55,10 @@ describe('contract: EthConversionProxy', () => {
chainlinkPath.address,
ETH_hash,
);
etherPaymentFallback = await new EtherPaymentFallback__factory(signer).deploy();
gnosisSafeProxy = await new GnosisSafeProxy__factory(signer).deploy(
etherPaymentFallback.address,
);
});

describe('transferWithReferenceAndFee', () => {
Expand Down Expand Up @@ -123,7 +133,7 @@ describe('contract: EthConversionProxy', () => {
feeAddress,
0,
{
value: conversionFees.result.add(conversionToPay.result), //.add("100000"),
value: conversionFees.result.add(conversionToPay.result).add('100000'),
},
);

Expand Down Expand Up @@ -159,6 +169,63 @@ describe('contract: EthConversionProxy', () => {
expect(toDiffBalance).to.equals(conversionToPay.result.toString());
expect(feeDiffBalance).to.equals(conversionFees.result.toString());
});

it('allows to transfer ETH for EUR payment and extra msg.value to a gnosis safe', async function () {
const path = [EUR_hash, USD_hash, ETH_hash];

const fromOldBalance = await provider.getBalance(from);
const gnosisSafeOldBalance = await provider.getBalance(gnosisSafeProxy.address);
const feeOldBalance = await provider.getBalance(feeAddress);
const conversionToPay = await chainlinkPath.getConversion(amountInFiat, path);
const conversionFees = await chainlinkPath.getConversion(feesAmountInFiat, path);

const tx = testEthConversionProxy.transferWithReferenceAndFee(
gnosisSafeProxy.address,
amountInFiat,
path,
referenceExample,
feesAmountInFiat,
feeAddress,
0,
{
value: conversionFees.result.add(conversionToPay.result).add('100000'),
},
);

await expect(tx)
.to.emit(testEthConversionProxy, 'TransferWithConversionAndReference')
.withArgs(
amountInFiat,
ethers.utils.getAddress(path[0]),
ethers.utils.keccak256(referenceExample),
feesAmountInFiat,
'0',
);

const fromNewBalance = await provider.getBalance(from);
const gnosisSafeNewBalance = await provider.getBalance(gnosisSafeProxy.address);
const feeNewBalance = await provider.getBalance(feeAddress);
const contractBalance = await provider.getBalance(testEthConversionProxy.address);
const contractFeeBalance = await provider.getBalance(ethFeeProxy.address);

const toDiffBalance = BigNumber.from(gnosisSafeNewBalance)
.sub(gnosisSafeOldBalance)
.toString();
const feeDiffBalance = BigNumber.from(feeNewBalance).sub(feeOldBalance).toString();

expect(contractBalance.toString()).to.equals('0');
expect(contractFeeBalance.toString()).to.equals('0');

// Check balance changes
expect(fromNewBalance).to.be.lt(
fromOldBalance.sub(conversionToPay.result).sub(conversionFees.result),
);
expect(fromNewBalance).to.be.gt(
fromOldBalance.sub(conversionToPay.result).sub(conversionFees.result).mul(95).div(100),
);
expect(toDiffBalance).to.equals(conversionToPay.result.toString());
expect(feeDiffBalance).to.equals(conversionFees.result.toString());
});
});

describe('transferWithReferenceAndFee with errors', () => {
Expand Down
Loading

0 comments on commit ee3436e

Please sign in to comment.