Skip to content

Commit 919c078

Browse files
Merge pull request #191 from cardstack/cs-3511-create-new-contract-to-managed
Add PrepaidCardMarketV2 contract
2 parents e46d103 + 049688c commit 919c078

25 files changed

+1970
-73
lines changed

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,22 @@ The `SupplierManager` contract is used to register suppliers (entities that brin
2626
The `BridgeUtils` contract manages the point of interaction between the token bridge's home mediator contract and the Card Protocol. When the token bridge encounters an allowed stablecoin that it hasn't encountered before, it will create a new token contract in layer 2 for that token, as part of this, the token bridge will also inform the Card Protocol about the new token contract address, such that the Card Protocol can accept the new CPXD form of the stablecoin as payment for the creation of new prepaid cards, as well as, payments by customers to merchants. Additionally, as part of the token bridging process, the bridged tokens are placed in a gnosis safe that is owned by the *Suppliers* (the initiators of the token bridging process). This allows for easy gas-less (from the perspective of the users of the protocol) transactions. The gnosis safe as part of the token bridging process is actually created by the `SupplierManager` contract that the `BridgeUtils` contract refers to.
2727

2828
### PrepaidCardManager
29-
The `PrepaidCardManager` contract is responsible for creating the gnosis safes that are considered as *Prepaid Cards*. As part of this process, a gnosis safe is created when layer 2 CPXD tokens are sent to the `PrepaidCardManager` Contract (as part of the layer 2 CPXD token's ERC-677 `onTokenTransfer()` function). This gnosis safe represents a *Prepaid Card*. This safe is created with 2 owners:
30-
1. The sender of the transaction, i.e. the *Supplier's* gnosis safe
29+
30+
The `PrepaidCardManager` contract is responsible for creating the gnosis safes that are considered as _Prepaid Cards_. As part of this process, a gnosis safe is created when layer 2 CPXD tokens are sent to the `PrepaidCardManager` Contract (as part of the layer 2 CPXD token's ERC-677 `onTokenTransfer()` function). This gnosis safe represents a _Prepaid Card_.
31+
32+
There are two ways of creating prepaid cards when the tokens are sent to this contract:
33+
34+
1. When `issuer` and `issuerSafe` are provided in the `onTokenTransfer` data field, and the tokens are coming from a trusted caller (the `PrepaidCardMarketV2` contract):
35+
36+
- Prepaid cards will be created using the provided issuer as the issuer, and issuerSafe will be emitted as depot in the `CreatePrepaidCard` event
37+
38+
2. When `issuer` and `issuerSafe` are provided as zero addresses:
39+
40+
- Prepaid cards will be created using the provided owner as the issuer
41+
42+
This safe is created with 2 owners:
43+
44+
1. The provided owner (case 1 explained above, i.e. the _Customer_), OR the sender of the transaction, i.e. the _Supplier's_ gnosis safe (case 2)
3145
2. The `PrepaidCardManager` contract itself.
3246

3347
As well as a threshold of 2 signatures in order to execute gnosis safe transactions. This approach means that the `PrepaidCardManager` contract needs to sign off on all transactions involving *Prepaid Cards*. As such the `PrepaidCardManager` contract allows *Prepaid Cards* to be able "send actions" by calling the `send()` function. The caller of the `PrepaidCardManager.send()` function (which is generally the txn sender of a gnosis safe relay server) specifies:
@@ -54,8 +68,11 @@ The `PrepaidCardManager` contract stores signers that are authorized to sign saf
5468
### PrepaidCardMarket
5569
The PrepaidCardMarket contract is responsible for provisioning already created prepaid card safes to customers. The intent is that Prepaid Card issuers can add their prepaid cards as inventory to this contract, thereby creating a SKU that represents the class of Prepaid Card which becomes available to provision to a customer. The act of adding a Prepaid Card to inventory means that the issuer of the Prepaid Card transfers their ownership of the Prepaid Card safe contract to the PrepaidCardMarket contract, such that the Prepaid Card safe has 2 contract owners: the PrepaidCardManager and the PrepaidCardMarket. Once a Prepaid Card is part of the inventory of the PrepaidCardMarket contract a "provisioner" role (which is a special EOA) is permitted to provision a Prepaid Card safe to a customer's EOA. This process entails leveraging an EIP-1271 contract signature to transfer ownership of the Prepaid Card safe from the PrepaidCardMarket contract to the specified customer EOA. Additionally the issuer of a Prepaid Card may also decide to remove Prepaid Cards from their inventory. This process also leverages EIP-1271 contract signatures to transfer ownership of the Prepaid Card safe from the PrepaidCardMarket contract back to the issuer that originally placed the Prepaid Card safe into their inventory.
5670

57-
A future update to this contract will provide the ability for EOAs to directly purchase Prepaid Card safes from the PrepaidCardMarket using native coin and/or CPXD tokens. In that future scenario we would leverage the "ask price" that can be set against a SKU to be able to purchase Prepaid Card safes directly. To support this eventual feature we have added the ability to set an "ask price" for SKUs ni the inventory, however, capability to directly purchase a Prepaid Card from this contract is still TBD.
71+
A future update to this contract will provide the ability for EOAs to directly purchase Prepaid Card safes from the PrepaidCardMarket using native coin and/or CPXD tokens. In that future scenario we would leverage the "ask price" that can be set against a SKU to be able to purchase Prepaid Card safes directly. To support this eventual feature we have added the ability to set an "ask price" for SKUs in the inventory, however, capability to directly purchase a Prepaid Card from this contract is still TBD.
72+
73+
### PrepaidCardMarketV2
5874

75+
The PrepaidCardMarketV2 market is responsible for provisioning prepaid cards without creating them beforehand. Once the issuer deposits their funds into the contract from their safe, their new balance will be saved and then they can continue to add SKUs and set the SKU asking price. It is then possible for them to call `provisionPrepaidCard(customer, sku)` on the contract, which will determine the price to create the prepaid card, create it by sending tokens to `PrepaidCardManager` (which will set the customer as the owner), and deduct the price to create the prepaid card from the issuer safe's balance recorded in the `PrepaidCardMarketV2` contract.
5976

6077
### ActionDispatcher
6178
The `ActionDispatcher` receives actions that have been issued from the `PrepaidCardManager.send()` as gnosis safe transactions. The `ActionDispatcher` will confirm that the requested USD rate for the §SPEND amount falls within an acceptable range, and then will forward (via an ERC677 `transferAndCall()`) the action to the contract address that has been configured to handle the requested action.
@@ -78,6 +95,10 @@ The `SetPrepaidCardInventoryHandler` is a contract that handles the `setPrepaidC
7895
### RemovePrepaidCardInventoryHandler
7996
The `RemovePrepaidCardInventoryHandler` is a contract that handles the `removePrepaidCardInventory` action. This contract will receive a "removePrepaidCardInventory" action accompanied by ABI encoded fields that include a list of prepaid card safe addresses to be removed from an issuer's inventory in the PrepaidCardMarket contract. This contract will iterate over the list of prepaid card safe addresses and leverage an EIP-1271 contract signature to transfer the prepaid card safes back to the issuer that originally added these prepaid card safes to the inventory, thereby removing these prepaid card safes from the PrepaidCardMarket contract inventory.
8097

98+
### AddPrepaidCardSKUHandler
99+
100+
The `AddPrepaidCardSKUHandler` is a contract that handles the `addPrepaidCardSKU` action. This contract will receive a "addPrepaidCardSKU" action accompanied by ABI encoded fields that include the face value, customization DID, and the addresses of the issuing token, market and issuer's safe. It will then proceed to supply these params in the `addSKU` call to the `PrepaidCardMarketV2` contract. Before the issuer can add SKUs, they first need to have some balance in the `PrepaidCardMarketV2` contract.
101+
81102
### SetPrepaidCardAskHandler
82103
The `SetPrepaidCardAskHandler` is a contract that handles the `setPrepaidCardAsk` action. This contract will receive a "setPrepaidCardAsk" action accompanied by ABI encoded fields that include the SKU for prepaid card inventory and the ask price for a single item in the inventory in units of the issuing token for the prepaid card. Until an ask price is set for a SKU, the prepaid cards that are a part of the SKU cannot be provisioned. This is to prevent the scenario where prepaid cards could be purchased before a price is set when the TBD direct purchase functionality is added to the PrepaidCardMarket contract. Eventually when the TBD direct purchase functionality is added, the ask price will be used to charge EOAs that wish to purchase prepaid card inventory from the PrepaidCardMarket contract.
83104

contracts/IPrepaidCardMarket.sol

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ pragma solidity ^0.8.9;
22
pragma abicoder v1;
33

44
interface IPrepaidCardMarket {
5-
function setItem(address issuer, address prepaidCard) external returns (bool);
5+
// mapping
6+
function asks(bytes32) external view returns (uint256);
67

7-
function removeItems(address issuer, address[] calldata prepaidCards)
8-
external
9-
returns (bool);
8+
// property
9+
function paused() external view returns (bool);
10+
11+
function getQuantity(bytes32 sku) external view returns (uint256);
1012

1113
function setAsk(
1214
address issuer,
@@ -27,4 +29,15 @@ interface IPrepaidCardMarket {
2729
uint256 faceValue,
2830
string memory customizationDID
2931
);
32+
33+
function getSKU(
34+
address issuer,
35+
address token,
36+
uint256 faceValue,
37+
string memory customizationDID
38+
) external view returns (bytes32);
39+
40+
function pause() external;
41+
42+
function unpause() external;
3043
}

contracts/PrepaidCardManager.sol

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeab
99
import "./core/Ownable.sol";
1010

1111
import "./token/IERC677.sol";
12-
import "./IPrepaidCardMarket.sol";
12+
import "./PrepaidCardMarket.sol";
1313
import "./TokenManager.sol";
1414
import "./core/Safe.sol";
1515
import "./core/Versionable.sol";
@@ -67,6 +67,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
6767
);
6868
event GasPolicyAdded(string action, bool useGasPrice);
6969
event ContractSignerRemoved(address signer);
70+
event TrustedCallerForCreatingPrepaidCardsWithIssuerRemoved(address caller);
7071
event PrepaidCardSend(
7172
address prepaidCard,
7273
uint256 spendAmount,
@@ -100,6 +101,8 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
100101
EnumerableSetUpgradeable.AddressSet internal contractSigners;
101102
mapping(string => GasPolicyV2) public gasPoliciesV2;
102103
address public versionManager;
104+
EnumerableSetUpgradeable.AddressSet
105+
internal trustedCallersForCreatingPrepaidCardsWithIssuer;
103106

104107
modifier onlyHandlers() {
105108
require(
@@ -142,6 +145,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
142145
uint256 _minAmount,
143146
uint256 _maxAmount,
144147
address[] calldata _contractSigners,
148+
address[] calldata _trustedCallersForCreatingPrepaidCardsWithIssuer,
145149
address _versionManager
146150
) external onlyOwner {
147151
require(_tokenManager != address(0), "tokenManager not set");
@@ -170,6 +174,15 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
170174

171175
contractSigners.add(_contractSigners[i]);
172176
}
177+
for (
178+
uint256 i = 0;
179+
i < _trustedCallersForCreatingPrepaidCardsWithIssuer.length;
180+
i++
181+
) {
182+
trustedCallersForCreatingPrepaidCardsWithIssuer.add(
183+
_trustedCallersForCreatingPrepaidCardsWithIssuer[i]
184+
);
185+
}
173186
emit Setup();
174187
}
175188

@@ -196,6 +209,18 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
196209
emit ContractSignerRemoved(signer);
197210
}
198211

212+
function removeTrustedCallerForCreatingPrepaidCardsWithIssuer(address caller)
213+
external
214+
onlyOwner
215+
{
216+
require(
217+
trustedCallersForCreatingPrepaidCardsWithIssuer.contains(caller),
218+
"caller not present"
219+
);
220+
trustedCallersForCreatingPrepaidCardsWithIssuer.remove(caller);
221+
emit TrustedCallerForCreatingPrepaidCardsWithIssuerRemoved(caller);
222+
}
223+
199224
/**
200225
* @dev onTokenTransfer(ERC677) - this is the ERC677 token transfer callback.
201226
*
@@ -215,44 +240,81 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
215240
* )
216241
*/
217242
function onTokenTransfer(
218-
address from, // solhint-disable-line no-unused-vars
243+
address from,
219244
uint256 amount,
220245
bytes calldata data
221246
) external returns (bool) {
222247
require(
223248
TokenManager(tokenManager).isValidToken(msg.sender),
224249
"calling token is unaccepted"
225250
);
251+
226252
(
227253
address owner,
228254
uint256[] memory issuingTokenAmounts,
229255
uint256[] memory spendAmounts,
230256
string memory customizationDID,
231-
address marketAddress
232-
) = abi.decode(data, (address, uint256[], uint256[], string, address));
257+
address marketAddress,
258+
address issuer,
259+
address issuerSafe
260+
) = abi.decode(
261+
data,
262+
(address, uint256[], uint256[], string, address, address, address)
263+
);
264+
233265
require(
234266
owner != address(0) && issuingTokenAmounts.length > 0,
235267
"Prepaid card data invalid"
236268
);
237-
require(
238-
issuingTokenAmounts.length == spendAmounts.length,
239-
"the amount arrays have differing lengths"
240-
);
241269

242270
// The spend amounts are for reporting purposes only, there is no on-chain
243271
// effect from this value. Although, it might not be a bad idea that spend
244272
// amounts line up with the issuing token amounts--albiet we'd need to
245273
// introduce a rate lock mechanism if we wanted to validate this
246-
createMultiplePrepaidCards(
247-
owner,
248-
from,
249-
_msgSender(),
250-
amount,
251-
issuingTokenAmounts,
252-
spendAmounts,
253-
customizationDID,
254-
marketAddress
274+
require(
275+
issuingTokenAmounts.length == spendAmounts.length,
276+
"the amount arrays have differing lengths"
255277
);
278+
279+
// When issuer and issuerSafe are blank, it means the call is related to the
280+
// process where a prepaid card is first created, using the provided issuer as
281+
// the owner, and later provisioned and transfered to the customer (customer's
282+
// EOA is the new owner).
283+
284+
if (
285+
(issuer == address(0) && issuerSafe == address(0)) ||
286+
!trustedCallersForCreatingPrepaidCardsWithIssuer.contains(from)
287+
) {
288+
createPrepaidCards(
289+
owner, // issuer
290+
owner,
291+
from, // depot
292+
_msgSender(), // token
293+
amount,
294+
issuingTokenAmounts,
295+
spendAmounts,
296+
customizationDID,
297+
marketAddress
298+
);
299+
} else {
300+
// In case when issuer and issuerSafe are provided, it means the tokens are being
301+
// sent from the PrepaidCardMarketV2 contract where the prepaid cards are being
302+
// created and provisioned in a single step, where the issuer and owner (customer's EOA)
303+
// are provided during the creation of the prepaid cards.
304+
305+
createPrepaidCards(
306+
issuer,
307+
owner,
308+
issuerSafe, // depot
309+
_msgSender(), // token
310+
amount,
311+
issuingTokenAmounts,
312+
spendAmounts,
313+
customizationDID,
314+
marketAddress
315+
);
316+
}
317+
256318
return true;
257319
}
258320

@@ -302,6 +364,17 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
302364
return contractSigners.values();
303365
}
304366

367+
/**
368+
* @dev get the addresses that are allowed to create cards with provided issuer
369+
*/
370+
function getTrustedCallersForCreatingPrepaidCardsWithIssuer()
371+
external
372+
view
373+
returns (address[] memory)
374+
{
375+
return trustedCallersForCreatingPrepaidCardsWithIssuer.values();
376+
}
377+
305378
/**
306379
* @dev returns a boolean indicating if the prepaid card's owner is an EIP-1271 signer
307380
* @param prepaidCard prepaid card address
@@ -523,7 +596,8 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
523596
* @param spendAmounts array of spend amounts that represent the desired face value (for reporting only)
524597
* @param customizationDID the customization DID for the new prepaid cards
525598
*/
526-
function createMultiplePrepaidCards(
599+
function createPrepaidCards(
600+
address issuer,
527601
address owner,
528602
address depot,
529603
address token,
@@ -554,6 +628,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
554628
);
555629
for (uint256 i = 0; i < numberCard; i++) {
556630
createPrepaidCard(
631+
issuer,
557632
owner,
558633
depot,
559634
token,
@@ -574,6 +649,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
574649

575650
/**
576651
* @dev Create Prepaid card
652+
* @param issuer issuer address
577653
* @param owner owner address
578654
* @param token token address
579655
* @param issuingTokenAmount amount of issuing token to use to fund the new prepaid card
@@ -582,6 +658,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
582658
* @return PrepaidCard address
583659
*/
584660
function createPrepaidCard(
661+
address issuer,
585662
address owner,
586663
address depot,
587664
address token,
@@ -598,7 +675,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
598675
address card = createSafe(owners, 2);
599676

600677
// card was created
601-
cardDetails[card].issuer = owner;
678+
cardDetails[card].issuer = issuer;
602679
cardDetails[card].issueToken = token;
603680
cardDetails[card].customizationDID = customizationDID;
604681
cardDetails[card].blockNumber = block.number;
@@ -624,7 +701,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
624701
);
625702

626703
if (marketAddress != address(0)) {
627-
IPrepaidCardMarket(marketAddress).setItem(owner, card);
704+
PrepaidCardMarket(marketAddress).setItem(owner, card);
628705
}
629706

630707
return card;

contracts/PrepaidCardMarket.sol

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,26 @@ contract PrepaidCardMarket is Ownable, Versionable, IPrepaidCardMarket {
115115
emit Setup();
116116
}
117117

118+
// TODO: Remove once we refactor relayer and hub to use pause() and unpause().
119+
// The latter is used to be compatible with OpenZeppelin's defender
120+
// (https://docs.openzeppelin.com/defender/admin#pauseunpause)
118121
function setPaused(bool _paused) external onlyOwner {
119122
paused = _paused;
120123
emit PausedToggled(_paused);
121124
}
122125

126+
function pause() external onlyOwner {
127+
paused = true;
128+
emit PausedToggled(true);
129+
}
130+
131+
function unpause() external onlyOwner {
132+
paused = false;
133+
emit PausedToggled(false);
134+
}
135+
123136
function setItem(address issuer, address prepaidCard)
124137
external
125-
override
126138
onlyHandlersOrPrepaidCardManager
127139
returns (bool)
128140
{
@@ -155,7 +167,6 @@ contract PrepaidCardMarket is Ownable, Versionable, IPrepaidCardMarket {
155167

156168
function removeItems(address issuer, address[] calldata prepaidCards)
157169
external
158-
override
159170
onlyHandlers
160171
returns (bool)
161172
{

0 commit comments

Comments
 (0)