Adding price to deposits and add token transfers

This commit is contained in:
Filipe Soccol 2022-11-07 16:40:10 -03:00
parent 1298b0d368
commit 0f35eec623
15 changed files with 612 additions and 26 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ coverage.json
#Hardhat files
cache
artifacts/build-info
artifacts/@openzeppelin

View File

@ -0,0 +1,4 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../build-info/5f0db55f399477fd77a196dfab69a373.json"
}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../build-info/e53d155f4d4e8ba3d5f49011e7166818.json"
"buildInfo": "../../build-info/5ce31c4ec8418c4fef71a52ed732efd0.json"
}

File diff suppressed because one or more lines are too long

11
contracts/mockToken.sol Normal file
View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockToken is ERC20 {
constructor(uint256 supply) ERC20("MockBRL", "MBRL") {
_mint(msg.sender, supply);
}
}

View File

@ -1,11 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract P2PIX {
event DepositAdded(address indexed seller, bytes32 depositID, address token, uint256 amount);
event DepositAdded(address indexed seller, bytes32 depositID, address token, uint256 price, uint256 amount);
event DepositClosed(address indexed seller, bytes32 depositID);
event DepositWithdrawn(address indexed seller, bytes32 depositID, uint256 amount);
event DepositPriceChanged(bytes32 indexed depositID, uint256 price);
event LockAdded(address indexed buyer, bytes32 indexed lockID, bytes32 depositID, uint256 amount);
event LockReleased(address indexed buyer, bytes32 lockId);
event LockReturned(address indexed buyer, bytes32 lockId);
@ -14,6 +17,7 @@ contract P2PIX {
address seller;
address token; // ERC20 stable token address
uint256 remaining; // Remaining tokens available
uint256 price; // Price in R$ per token
bool valid; // Could be invalidated by the seller
string pixTarget; // The PIX account for the seller receive transactions
}
@ -23,7 +27,8 @@ contract P2PIX {
address targetAddress; // Where goes the tokens when validated
address relayerAddress; // Relayer address that facilitated this transaction
uint256 relayerPremium; // Amount to be paid for relayer
uint256 amount; // Amount to be transfered to buyer
uint256 amount; // Amount to be tranfered via PIX
uint256 locked; // Amount locked in tokens from deposit
uint256 expirationBlock; // If not paid at this block will be expired
}
@ -55,15 +60,16 @@ contract P2PIX {
function deposit(
address token,
uint256 amount,
uint256 price,
string calldata pixTarget
) public returns (bytes32 depositID){
depositID = keccak256(abi.encodePacked(pixTarget, amount));
require(!mapDeposits[depositID].valid, 'P2PIX: Deposit already exist and it is still valid');
// TODO Prevent seller to use same depositID
// TODO Transfer tokens to this address
Deposit memory d = Deposit(msg.sender, token, amount, true, pixTarget);
IERC20 t = IERC20(token);
t.transferFrom(msg.sender, address(this), amount);
Deposit memory d = Deposit(msg.sender, token, amount, price, true, pixTarget);
mapDeposits[depositID] = d;
emit DepositAdded(msg.sender, depositID, token, amount);
emit DepositAdded(msg.sender, depositID, token, price, amount);
}
// Vendedor pode invalidar da ordem de venda impedindo novos locks na mesma (isso não afeta nenhum lock que esteja ativo).
@ -89,7 +95,7 @@ contract P2PIX {
unlockExpired(expiredLocks);
Deposit storage d = mapDeposits[depositID];
require(d.valid, "P2PIX: Deposit not valid anymore");
require(d.remaining > amount, "P2PIX: Not enough remaining");
require(d.remaining > amount/d.price, "P2PIX: Not enough remaining");
lockID = keccak256(abi.encodePacked(depositID, amount, targetAddress));
require(
mapLocks[lockID].expirationBlock < block.number,
@ -101,6 +107,7 @@ contract P2PIX {
relayerAddress,
relayerPremium,
amount,
amount/d.price,
block.number+defaultLockBlocks
);
mapLocks[lockID] = l;
@ -120,6 +127,7 @@ contract P2PIX {
// TODO Check if lockID exists and is enabled
// TODO **Prevenir que um Pix não relacionado ao APP seja usado pois tem o mesmo destino
Lock storage l = mapLocks[lockID];
Deposit storage d = mapDeposits[l.depositID];
bytes32 message = keccak256(abi.encodePacked(
mapDeposits[l.depositID].pixTarget,
l.amount,
@ -128,13 +136,21 @@ contract P2PIX {
require(!usedTransactions[message], "Transaction already used to unlock payment.");
address signer = ecrecover(message, v, r, s);
require(validBacenSigners[signer], "Signer is not a valid signer.");
// TODO Transfer token to l.target
// TODO Transfer relayer fees to relayer
IERC20 t = IERC20(d.token);
t.transfer(l.targetAddress, l.locked-l.relayerPremium);
if (l.relayerPremium > 0) t.transfer(l.relayerAddress, l.relayerPremium);
l.amount = 0;
usedTransactions[message] = true;
emit LockReleased(l.targetAddress, lockID);
}
// Change price for deposit amount
function changeDepositPrice(bytes32 depositID, uint256 price) public {
Deposit storage d = mapDeposits[depositID];
d.price = price;
emit DepositPriceChanged(depositID, price);
}
// Unlock expired locks
function unlockExpired(bytes32[] calldata lockIDs) internal {
uint256 locksSize = lockIDs.length;
@ -153,11 +169,13 @@ contract P2PIX {
bytes32[] calldata expiredLocks
) public onlySeller(depositID) {
unlockExpired(expiredLocks);
if (mapDeposits[depositID].valid) cancelDeposit(depositID);
// TODO Transfer remaining tokens back to the seller
Deposit storage d = mapDeposits[depositID];
if (d.valid) cancelDeposit(depositID);
IERC20 token = IERC20(d.token);
token.transfer(d.seller, d.remaining);
// Withdraw remaining tokens from mapDeposit[depositID]
uint256 amount = mapDeposits[depositID].remaining;
mapDeposits[depositID].remaining = 0;
uint256 amount = d.remaining;
d.remaining = 0;
emit DepositWithdrawn(msg.sender, depositID, amount);
}

8
deploys/localhost.json Normal file
View File

@ -0,0 +1,8 @@
{
"signers": [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
],
"p2pix": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"token": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
}

View File

@ -3,4 +3,10 @@ require("@nomiclabs/hardhat-waffle");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.17",
networks: {
hardhat: {
blockGasLimit: 30000000,
//hardfork: 'london'
}
}
};

13
package-lock.json generated
View File

@ -8,6 +8,9 @@
"name": "p2pix-smart-contracts",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@openzeppelin/contracts": "^4.7.3"
},
"devDependencies": {
"@nomiclabs/hardhat-waffle": "^2.0.3",
"chai": "^4.3.6",
@ -1756,6 +1759,11 @@
"hardhat": "^2.0.0"
}
},
"node_modules/@openzeppelin/contracts": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz",
"integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw=="
},
"node_modules/@resolver-engine/core": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@resolver-engine/core/-/core-0.3.3.tgz",
@ -17914,6 +17922,11 @@
"@types/web3": "1.0.19"
}
},
"@openzeppelin/contracts": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz",
"integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw=="
},
"@resolver-engine/core": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@resolver-engine/core/-/core-0.3.3.tgz",

View File

@ -20,5 +20,8 @@
"@nomiclabs/hardhat-waffle": "^2.0.3",
"chai": "^4.3.6",
"hardhat": "^2.12.0"
},
"dependencies": {
"@openzeppelin/contracts": "^4.7.3"
}
}

31
scripts/1-deploy-p2pix.js Normal file
View File

@ -0,0 +1,31 @@
const fs = require('fs');
const { network } = require("hardhat");
async function main() {
let deploysJson = {}
try {
const data = fs.readFileSync(`./deploys/${network.name}.json`, {encoding:"utf-8"});
deploysJson = JSON.parse(data);
} catch (err) {
console.log('Error loading Master address: ', err);
process.exit(1);
}
const P2PIX = await ethers.getContractFactory("P2PIX");
const p2pix = await P2PIX.deploy(2, deploysJson.signers);
await p2pix.deployed();
deploysJson.p2pix = p2pix.address
console.log("🚀 P2PIX Deployed:", p2pix.address);
fs.writeFileSync(`./deploys/${network.name}.json`, JSON.stringify(deploysJson, undefined, 2));
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@ -0,0 +1,31 @@
const fs = require('fs');
const { network } = require("hardhat");
async function main() {
let deploysJson = {}
try {
const data = fs.readFileSync(`./deploys/${network.name}.json`, {encoding:"utf-8"});
deploysJson = JSON.parse(data);
} catch (err) {
console.log('Error loading Master address: ', err);
process.exit(1);
}
const ERC20Factory = await ethers.getContractFactory("MockToken");
const erc20 = await ERC20Factory.deploy(ethers.utils.parseEther('20000000', 'wei'));
await erc20.deployed();
deploysJson.token = erc20.address
console.log("🚀 Mock Token Deployed:", erc20.address);
fs.writeFileSync(`./deploys/${network.name}.json`, JSON.stringify(deploysJson, undefined, 2));
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@ -5,33 +5,61 @@ describe("P2PIX deposit test", function () {
let owner, wallet2, wallet3, wallet4;
let p2pix; // Contract instance
let erc20; // Token instance
let depositID;
it("Will deploy contracts", async function () {
[owner, wallet2, wallet3, wallet4] = await ethers.getSigners();
const ERC20Factory = await ethers.getContractFactory("MockToken");
erc20 = await ERC20Factory.deploy(ethers.utils.parseEther('20000000', 'wei'));
await erc20.deployed();
// Check initial balance
expect(await erc20.balanceOf(owner.address)).to.equal(ethers.utils.parseEther('20000000', 'wei'));
const P2PIX = await ethers.getContractFactory("P2PIX");
p2pix = await P2PIX.deploy(2, [owner.address, wallet2.address]);
await p2pix.deployed();
// Verify values at deployment
expect(await p2pix.validBacenSigners(owner.address)).to.equal(true);
expect(await p2pix.validBacenSigners(wallet2.address)).to.equal(true);
});
it("Should allow create a deposit", async function () {
const transaction = await p2pix.deposit(ethers.constants.AddressZero, 1000, 'SELLER PIX KEY');
depositID = ethers.utils.solidityKeccak256(['string', 'uint256'], ['SELLER PIX KEY', 1000])
let transaction = await erc20.approve(p2pix.address,ethers.utils.parseEther('1000'));
await expect(transaction).to.emit(erc20, 'Approval').withArgs(
owner.address,
p2pix.address,
ethers.utils.parseEther('1000')
)
transaction = await p2pix.deposit(
erc20.address,
ethers.utils.parseEther('1000'),
ethers.utils.parseEther('0.99'),
'SELLER PIX KEY'
);
depositID = ethers.utils.solidityKeccak256(['string', 'uint256'], ['SELLER PIX KEY', ethers.utils.parseEther('1000')])
await expect(transaction).to.emit(p2pix, 'DepositAdded').withArgs(
owner.address,
depositID,
ethers.constants.AddressZero,
1000
erc20.address,
ethers.utils.parseEther('0.99'),
ethers.utils.parseEther('1000')
)
})
it("Should prevent create same deposit", async function () {
await expect(p2pix.deposit(ethers.constants.AddressZero, 1000, 'SELLER PIX KEY'))
await expect(p2pix.deposit(
erc20.address,
ethers.utils.parseEther('1000'),
ethers.utils.parseEther('0.99'),
'SELLER PIX KEY'
))
.to.be.revertedWith('P2PIX: Deposit already exist and it is still valid');
})
@ -44,13 +72,27 @@ describe("P2PIX deposit test", function () {
})
it("Should allow recreate the deposit", async function () {
const transaction = await p2pix.deposit(ethers.constants.AddressZero, 1000, 'SELLER PIX KEY');
depositID = ethers.utils.solidityKeccak256(['string', 'uint256'], ['SELLER PIX KEY', 1000])
let transaction = await erc20.approve(p2pix.address,ethers.utils.parseEther('1000'));
await expect(transaction).to.emit(erc20, 'Approval').withArgs(
owner.address,
p2pix.address,
ethers.utils.parseEther('1000')
)
transaction = await p2pix.deposit(
erc20.address,
ethers.utils.parseEther('1000'),
ethers.utils.parseEther('0.99'),
'SELLER PIX KEY'
);
depositID = ethers.utils.solidityKeccak256(['string', 'uint256'], ['SELLER PIX KEY', ethers.utils.parseEther('1000')])
await expect(transaction).to.emit(p2pix, 'DepositAdded').withArgs(
owner.address,
depositID,
ethers.constants.AddressZero,
1000
erc20.address,
ethers.utils.parseEther('0.99'),
ethers.utils.parseEther('1000')
)
})
@ -67,7 +109,7 @@ describe("P2PIX deposit test", function () {
await expect(transaction).to.emit(p2pix, 'DepositWithdrawn').withArgs(
owner.address,
depositID,
1000
ethers.utils.parseEther('1000')
)
})
})

View File

@ -0,0 +1,78 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("P2PIX lock/release test", function () {
let owner, wallet2, wallet3, wallet4;
let p2pix; // Contract instance
let erc20; // Token instance
let depositID;
it("Will deploy contracts", async function () {
[owner, wallet2, wallet3, wallet4] = await ethers.getSigners();
const ERC20Factory = await ethers.getContractFactory("MockToken");
erc20 = await ERC20Factory.deploy(ethers.utils.parseEther('20000000', 'wei'));
await erc20.deployed();
// Check initial balance
expect(await erc20.balanceOf(owner.address)).to.equal(ethers.utils.parseEther('20000000', 'wei'));
const P2PIX = await ethers.getContractFactory("P2PIX");
p2pix = await P2PIX.deploy(2, [owner.address, wallet2.address]);
await p2pix.deployed();
// Verify values at deployment
expect(await p2pix.validBacenSigners(owner.address)).to.equal(true);
expect(await p2pix.validBacenSigners(wallet2.address)).to.equal(true);
});
it("Should allow create a deposit", async function () {
let transaction = await erc20.approve(p2pix.address,ethers.utils.parseEther('1000'));
await expect(transaction).to.emit(erc20, 'Approval').withArgs(
owner.address,
p2pix.address,
ethers.utils.parseEther('1000')
)
transaction = await p2pix.deposit(
erc20.address,
ethers.utils.parseEther('1000'),
ethers.utils.parseEther('0.99'),
'SELLER PIX KEY'
);
depositID = ethers.utils.solidityKeccak256(['string', 'uint256'], ['SELLER PIX KEY', ethers.utils.parseEther('1000')])
await expect(transaction).to.emit(p2pix, 'DepositAdded').withArgs(
owner.address,
depositID,
erc20.address,
ethers.utils.parseEther('0.99'),
ethers.utils.parseEther('1000')
)
})
it("Should allow create a new lock", async function () {
transaction = await p2pix.connect(wallet3).lock(
depositID,
wallet3.address,
ethers.constants.AddressZero,
'0',
ethers.utils.parseEther('100'),
[]
)
const lockID = ethers.utils.solidityKeccak256(['bytes32', 'uint256', 'address'], [
depositID,
ethers.utils.parseEther('100'),
wallet3.address
])
await expect(transaction).to.emit(p2pix, 'LockAdded').withArgs(
wallet3.address,
lockID,
depositID,
ethers.utils.parseEther('100')
)
})
})