Skip to content

Commit ee978b0

Browse files
authored
v1.0.1 (#3)
1 parent 4727b73 commit ee978b0

File tree

3 files changed

+166
-13
lines changed

3 files changed

+166
-13
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules
2+
.env
3+
yarn-error.log
4+
5+
artifacts
6+
coverage/
7+
coverage.json
8+
.husky
9+
10+
.idea

contracts/central_payments/CentralPaymentsManager.sol

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ pragma solidity 0.8.10;
55
import "../permissions_registry/IPermissionsRegistry.sol";
66
import "../metatx/IMinimalForwarder.sol";
77
import "../metatx/INativeMetaTransaction.sol";
8+
import "../utils/ERC1155/IERC1155Upgradeable.sol";
89

910
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
11+
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
1012

1113
/**
1214
* @title Highlight central payments manager
1315
1416
*/
15-
contract CentralPaymentsManager {
17+
contract CentralPaymentsManager is ReentrancyGuard {
1618
/**
1719
* @dev Packet enabling impersonation of purchaser for currency
1820
*/
@@ -23,6 +25,18 @@ contract CentralPaymentsManager {
2325
uint8 sigV;
2426
}
2527

28+
/**
29+
* @dev Defines community tokens being purchased
30+
*/
31+
struct SaleItem {
32+
uint256[] tokenIds;
33+
uint256[] amounts;
34+
uint256 price;
35+
address community;
36+
address vault;
37+
bytes transferData;
38+
}
39+
2640
/**
2741
* @dev System permissions registry
2842
*/
@@ -94,7 +108,7 @@ contract CentralPaymentsManager {
94108
PurchaserMetaTxPacket calldata purchaseToPlatformPacket,
95109
uint256 price, // only used for emitting sale data (instead of extracting from packets)
96110
uint256[] calldata tokenIds // only used for emitting sale data (instead of extracting from packets)
97-
) external onlyPlatformExecutor currencyWhitelisted(currency) {
111+
) external onlyPlatformExecutor currencyWhitelisted(currency) nonReentrant {
98112
// transfer price amount of currency by hitting executeMetaTx on currency contract
99113
// the amount should be computed properly off-chain
100114
// transfer 97% to the creator
@@ -121,4 +135,52 @@ contract CentralPaymentsManager {
121135
// emit sale params
122136
emit CentralSale(communityTokenTransferReq.to, purchaser, currency, price, tokenIds);
123137
}
138+
139+
/**
140+
* @dev Purchase community tokens by sending payment to creator + platform, then by impersonating executor to transfer community tokens.
141+
* @param currency The ERC20 currency that the purchaser is paying in. Has to support meta transactions.
142+
* @param purchaser The purchaser
143+
* @param saleItem Defines what community tokens are being purchased
144+
* @param purchaseToCreatorPacket Meta tx packet containing call to send portion of purchase to creator
145+
* @param purchaseToPlatformPacket Meta tx packet containing call to send portion of purchase to platform
146+
*/
147+
function purchaseTokenWithMetaTxSupportedCurrencyAndPermissionedExecutor(
148+
address currency,
149+
address purchaser,
150+
SaleItem calldata saleItem,
151+
PurchaserMetaTxPacket calldata purchaseToCreatorPacket,
152+
PurchaserMetaTxPacket calldata purchaseToPlatformPacket
153+
) external onlyPlatformExecutor currencyWhitelisted(currency) nonReentrant {
154+
// transfer price amount of currency by hitting executeMetaTx on currency contract
155+
// the amount should be computed properly off-chain
156+
// transfer 97% to the creator
157+
INativeMetaTransaction(currency).executeMetaTransaction(
158+
purchaser,
159+
purchaseToCreatorPacket.functionSignature,
160+
purchaseToCreatorPacket.sigR,
161+
purchaseToCreatorPacket.sigS,
162+
purchaseToCreatorPacket.sigV
163+
);
164+
165+
// transfer 3% to the vault
166+
INativeMetaTransaction(currency).executeMetaTransaction(
167+
purchaser,
168+
purchaseToPlatformPacket.functionSignature,
169+
purchaseToPlatformPacket.sigR,
170+
purchaseToPlatformPacket.sigS,
171+
purchaseToPlatformPacket.sigV
172+
);
173+
174+
// transfer tokenAmounts of tokenIds on community
175+
IERC1155Upgradeable(saleItem.community).safeBatchTransferFrom(
176+
saleItem.vault,
177+
purchaser,
178+
saleItem.tokenIds,
179+
saleItem.amounts,
180+
saleItem.transferData
181+
);
182+
183+
// emit sale params
184+
emit CentralSale(saleItem.community, purchaser, currency, saleItem.price, saleItem.tokenIds);
185+
}
124186
}

test/CentralPaymentsManager.js

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe("CentralPaymentsManager", function () {
2727
let basicTm;
2828
let weth;
2929
let centralPaymentsManager;
30+
let permissionsRegistry;
3031

3132
let wethMetaTx;
3233

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

@@ -97,7 +98,7 @@ describe("CentralPaymentsManager", function () {
9798
basicTm = deployedBasicTm
9899
})
99100

100-
describe("Purchase", function () {
101+
describe("Purchase with minimal forwarder", function () {
101102
it("Platform executor should be able to execute a well-formed and signed purchase", async function () {
102103
const listing = {
103104
community,
@@ -125,7 +126,7 @@ describe("CentralPaymentsManager", function () {
125126
fanA,
126127
fanA.address,
127128
transferToPlatformData,
128-
firstNonce + 1
129+
firstNonce.toNumber() + 1
129130
);
130131

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

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

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

207-
// when caller isn't executor
223+
// construct signed request from purchaser, sending 97% of price to creator
224+
const transferToCreatorData = await weth.interface.encodeFunctionData("transfer", [creatorA.address, wETHWeiToCreator.toString()]);
225+
const purchaseToCreatorMetaTxPacket = await wethMetaTx.signWETHMetaTxRequest(
226+
fanA,
227+
fanA.address,
228+
transferToCreatorData,
229+
firstNonce
230+
);
231+
232+
// construct signed request from purchaser, sending 3% of price to platform (vault in this case)
233+
const transferToPlatformData = await weth.interface.encodeFunctionData("transfer", [vault.address, wETHWeiToPlatform.toString()]);
234+
const purchaseToPlatformMetaTxPacket = await wethMetaTx.signWETHMetaTxRequest(
235+
fanA,
236+
fanA.address,
237+
transferToPlatformData,
238+
firstNonce.toNumber() + 1
239+
);
208240

209-
// with multiple token amounts
241+
// replicating contract verification - equivalent in our backend
242+
const transferToCreatorMetaTx = {
243+
nonce: firstNonce.toString(),
244+
from: fanA.address,
245+
functionSignature: transferToCreatorData
246+
}
247+
const transferToPlatformMetaTx = {
248+
nonce: (firstNonce + 1).toString(),
249+
from: fanA.address,
250+
functionSignature: transferToPlatformData
251+
}
252+
expect(
253+
wethMetaTx.verify(
254+
fanA.address,
255+
transferToCreatorMetaTx,
256+
ethers.utils.joinSignature({ r: purchaseToCreatorMetaTxPacket.sigR, s: purchaseToCreatorMetaTxPacket.sigS, v: purchaseToCreatorMetaTxPacket.sigV })
257+
)
258+
)
259+
expect(
260+
wethMetaTx.verify(
261+
fanA.address,
262+
transferToPlatformMetaTx,
263+
ethers.utils.joinSignature({ r: purchaseToPlatformMetaTxPacket.sigR, s: purchaseToPlatformMetaTxPacket.sigS, v: purchaseToPlatformMetaTxPacket.sigV })
264+
)
265+
)
266+
267+
// purchase
268+
const expectedPriceInWei = ethers.utils.parseUnits(saleItem.price.toString());
269+
saleItem.price = expectedPriceInWei;
270+
await expect(centralPaymentsManager.purchaseTokenWithMetaTxSupportedCurrencyAndPermissionedExecutor(
271+
weth.address,
272+
fanA.address,
273+
saleItem,
274+
purchaseToCreatorMetaTxPacket,
275+
purchaseToPlatformMetaTxPacket
276+
)).to.emit(centralPaymentsManager, "CentralSale")
277+
.withArgs(
278+
ethers.utils.getAddress(community.address),
279+
fanA.address,
280+
weth.address,
281+
expectedPriceInWei,
282+
saleItem.tokenIds
283+
);
210284

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

214295

0 commit comments

Comments
 (0)