Skip to content

Commit

Permalink
v1.0.1 (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
beshup authored Jul 2, 2022
1 parent 4727b73 commit ee978b0
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 13 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
.env
yarn-error.log

artifacts
coverage/
coverage.json
.husky

.idea
66 changes: 64 additions & 2 deletions contracts/central_payments/CentralPaymentsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ pragma solidity 0.8.10;
import "../permissions_registry/IPermissionsRegistry.sol";
import "../metatx/IMinimalForwarder.sol";
import "../metatx/INativeMetaTransaction.sol";
import "../utils/ERC1155/IERC1155Upgradeable.sol";

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
* @title Highlight central payments manager
* @author [email protected]
*/
contract CentralPaymentsManager {
contract CentralPaymentsManager is ReentrancyGuard {
/**
* @dev Packet enabling impersonation of purchaser for currency
*/
Expand All @@ -23,6 +25,18 @@ contract CentralPaymentsManager {
uint8 sigV;
}

/**
* @dev Defines community tokens being purchased
*/
struct SaleItem {
uint256[] tokenIds;
uint256[] amounts;
uint256 price;
address community;
address vault;
bytes transferData;
}

/**
* @dev System permissions registry
*/
Expand Down Expand Up @@ -94,7 +108,7 @@ contract CentralPaymentsManager {
PurchaserMetaTxPacket calldata purchaseToPlatformPacket,
uint256 price, // only used for emitting sale data (instead of extracting from packets)
uint256[] calldata tokenIds // only used for emitting sale data (instead of extracting from packets)
) external onlyPlatformExecutor currencyWhitelisted(currency) {
) external onlyPlatformExecutor currencyWhitelisted(currency) nonReentrant {
// transfer price amount of currency by hitting executeMetaTx on currency contract
// the amount should be computed properly off-chain
// transfer 97% to the creator
Expand All @@ -121,4 +135,52 @@ contract CentralPaymentsManager {
// emit sale params
emit CentralSale(communityTokenTransferReq.to, purchaser, currency, price, tokenIds);
}

/**
* @dev Purchase community tokens by sending payment to creator + platform, then by impersonating executor to transfer community tokens.
* @param currency The ERC20 currency that the purchaser is paying in. Has to support meta transactions.
* @param purchaser The purchaser
* @param saleItem Defines what community tokens are being purchased
* @param purchaseToCreatorPacket Meta tx packet containing call to send portion of purchase to creator
* @param purchaseToPlatformPacket Meta tx packet containing call to send portion of purchase to platform
*/
function purchaseTokenWithMetaTxSupportedCurrencyAndPermissionedExecutor(
address currency,
address purchaser,
SaleItem calldata saleItem,
PurchaserMetaTxPacket calldata purchaseToCreatorPacket,
PurchaserMetaTxPacket calldata purchaseToPlatformPacket
) external onlyPlatformExecutor currencyWhitelisted(currency) nonReentrant {
// transfer price amount of currency by hitting executeMetaTx on currency contract
// the amount should be computed properly off-chain
// transfer 97% to the creator
INativeMetaTransaction(currency).executeMetaTransaction(
purchaser,
purchaseToCreatorPacket.functionSignature,
purchaseToCreatorPacket.sigR,
purchaseToCreatorPacket.sigS,
purchaseToCreatorPacket.sigV
);

// transfer 3% to the vault
INativeMetaTransaction(currency).executeMetaTransaction(
purchaser,
purchaseToPlatformPacket.functionSignature,
purchaseToPlatformPacket.sigR,
purchaseToPlatformPacket.sigS,
purchaseToPlatformPacket.sigV
);

// transfer tokenAmounts of tokenIds on community
IERC1155Upgradeable(saleItem.community).safeBatchTransferFrom(
saleItem.vault,
purchaser,
saleItem.tokenIds,
saleItem.amounts,
saleItem.transferData
);

// emit sale params
emit CentralSale(saleItem.community, purchaser, currency, saleItem.price, saleItem.tokenIds);
}
}
103 changes: 92 additions & 11 deletions test/CentralPaymentsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe("CentralPaymentsManager", function () {
let basicTm;
let weth;
let centralPaymentsManager;
let permissionsRegistry;

let wethMetaTx;

Expand Down Expand Up @@ -66,7 +67,7 @@ describe("CentralPaymentsManager", function () {
// deploy weth, fanA will start with 100 wETH, also whitelist wETH on permissions registry
weth = await WETH.deploy(highlightBeaconAdmin.address);
await weth.deployed();
const permissionsRegistry = new ethers.Contract(await factory.permissionsRegistry(), IPermissionsRegistryABI, permissionsRegistryAdmin);
permissionsRegistry = new ethers.Contract(await factory.permissionsRegistry(), IPermissionsRegistryABI, permissionsRegistryAdmin);
const tx = await permissionsRegistry.whitelistCurrency(weth.address);
await tx.wait();

Expand Down Expand Up @@ -97,7 +98,7 @@ describe("CentralPaymentsManager", function () {
basicTm = deployedBasicTm
})

describe("Purchase", function () {
describe("Purchase with minimal forwarder", function () {
it("Platform executor should be able to execute a well-formed and signed purchase", async function () {
const listing = {
community,
Expand Down Expand Up @@ -125,7 +126,7 @@ describe("CentralPaymentsManager", function () {
fanA,
fanA.address,
transferToPlatformData,
firstNonce + 1
firstNonce.toNumber() + 1
);

// construct signed request from executor, moving tokens out of vault to purchaser
Expand Down Expand Up @@ -197,18 +198,98 @@ describe("CentralPaymentsManager", function () {
})
})

// meta tx related failures / successes
// low gas
// wrong signer
// wrong nonce
describe("Purchase with permissioned CentralPaymentsManager", function () {
beforeEach(async function () {
// whitelist the central payments manager as an executor on the PermissionsRegistry which is what makes this approach possible
expect(await permissionsRegistry.addPlatformExecutor(centralPaymentsManager.address))
.to.emit(permissionsRegistry, "PlatformExecutorAdded")
.withArgs(centralPaymentsManager.address)
})

// when currency isn't whitelisted
it("Platform executor should be able to execute a well-formed and signed purchase", async function () {
const saleItem = {
community: community.address,
tokenIds: [1, 101],
amounts: [1, 1],
price: 2, // wETH
vault: vault.address,
transferData: ethers.utils.arrayify("0x")
}
const wETHWei = ethers.utils.parseUnits(saleItem.price.toString(), 18);
const wETHWeiToCreator = wETHWei.mul(97).div(100);
const wETHWeiToPlatform = wETHWei.mul(3).div(100);
const firstNonce = await wethMetaTx.contract.getNonce(fanA.address);

// when caller isn't executor
// construct signed request from purchaser, sending 97% of price to creator
const transferToCreatorData = await weth.interface.encodeFunctionData("transfer", [creatorA.address, wETHWeiToCreator.toString()]);
const purchaseToCreatorMetaTxPacket = await wethMetaTx.signWETHMetaTxRequest(
fanA,
fanA.address,
transferToCreatorData,
firstNonce
);

// construct signed request from purchaser, sending 3% of price to platform (vault in this case)
const transferToPlatformData = await weth.interface.encodeFunctionData("transfer", [vault.address, wETHWeiToPlatform.toString()]);
const purchaseToPlatformMetaTxPacket = await wethMetaTx.signWETHMetaTxRequest(
fanA,
fanA.address,
transferToPlatformData,
firstNonce.toNumber() + 1
);

// with multiple token amounts
// replicating contract verification - equivalent in our backend
const transferToCreatorMetaTx = {
nonce: firstNonce.toString(),
from: fanA.address,
functionSignature: transferToCreatorData
}
const transferToPlatformMetaTx = {
nonce: (firstNonce + 1).toString(),
from: fanA.address,
functionSignature: transferToPlatformData
}
expect(
wethMetaTx.verify(
fanA.address,
transferToCreatorMetaTx,
ethers.utils.joinSignature({ r: purchaseToCreatorMetaTxPacket.sigR, s: purchaseToCreatorMetaTxPacket.sigS, v: purchaseToCreatorMetaTxPacket.sigV })
)
)
expect(
wethMetaTx.verify(
fanA.address,
transferToPlatformMetaTx,
ethers.utils.joinSignature({ r: purchaseToPlatformMetaTxPacket.sigR, s: purchaseToPlatformMetaTxPacket.sigS, v: purchaseToPlatformMetaTxPacket.sigV })
)
)

// purchase
const expectedPriceInWei = ethers.utils.parseUnits(saleItem.price.toString());
saleItem.price = expectedPriceInWei;
await expect(centralPaymentsManager.purchaseTokenWithMetaTxSupportedCurrencyAndPermissionedExecutor(
weth.address,
fanA.address,
saleItem,
purchaseToCreatorMetaTxPacket,
purchaseToPlatformMetaTxPacket
)).to.emit(centralPaymentsManager, "CentralSale")
.withArgs(
ethers.utils.getAddress(community.address),
fanA.address,
weth.address,
expectedPriceInWei,
saleItem.tokenIds
);

// invalid after verification from currency! since there's no external verify function on wETH
// validate:
expect((await weth.balanceOf(creatorA.address)).toString()).to.equal(wETHWeiToCreator.mul(2));
expect((await weth.balanceOf(vault.address)).toString()).to.equal(wETHWeiToPlatform.mul(2));
expect(await weth.balanceOf(fanA.address)).to.equal(ethers.BigNumber.from("100000000000000000000").sub(wETHWei.mul(2)));
expect(await community.balanceOfBatch([fanA.address, fanA.address], saleItem.tokenIds)).to.eql(saleItem.amounts.map(amount => ethers.BigNumber.from(amount)));
// if passed in approval for OS, OS is approved
})
})
})


0 comments on commit ee978b0

Please sign in to comment.