Skip to content

Commit

Permalink
Merge pull request #191 from cardstack/cs-3511-create-new-contract-to…
Browse files Browse the repository at this point in the history
…-managed

Add PrepaidCardMarketV2 contract
  • Loading branch information
alex-cardstack committed May 16, 2022
2 parents e46d103 + 049688c commit 919c078
Show file tree
Hide file tree
Showing 25 changed files with 1,970 additions and 73 deletions.
27 changes: 24 additions & 3 deletions README.md
Expand Up @@ -26,8 +26,22 @@ The `SupplierManager` contract is used to register suppliers (entities that brin
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.

### PrepaidCardManager
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:
1. The sender of the transaction, i.e. the *Supplier's* gnosis safe

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_.

There are two ways of creating prepaid cards when the tokens are sent to this contract:

1. When `issuer` and `issuerSafe` are provided in the `onTokenTransfer` data field, and the tokens are coming from a trusted caller (the `PrepaidCardMarketV2` contract):

- Prepaid cards will be created using the provided issuer as the issuer, and issuerSafe will be emitted as depot in the `CreatePrepaidCard` event

2. When `issuer` and `issuerSafe` are provided as zero addresses:

- Prepaid cards will be created using the provided owner as the issuer

This safe is created with 2 owners:

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)
2. The `PrepaidCardManager` contract itself.

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:
Expand All @@ -54,8 +68,11 @@ The `PrepaidCardManager` contract stores signers that are authorized to sign saf
### PrepaidCardMarket
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.

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.
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.

### PrepaidCardMarketV2

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.

### ActionDispatcher
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.
Expand All @@ -78,6 +95,10 @@ The `SetPrepaidCardInventoryHandler` is a contract that handles the `setPrepaidC
### RemovePrepaidCardInventoryHandler
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.

### AddPrepaidCardSKUHandler

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.

### SetPrepaidCardAskHandler
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.

Expand Down
21 changes: 17 additions & 4 deletions contracts/IPrepaidCardMarket.sol
Expand Up @@ -2,11 +2,13 @@ pragma solidity ^0.8.9;
pragma abicoder v1;

interface IPrepaidCardMarket {
function setItem(address issuer, address prepaidCard) external returns (bool);
// mapping
function asks(bytes32) external view returns (uint256);

function removeItems(address issuer, address[] calldata prepaidCards)
external
returns (bool);
// property
function paused() external view returns (bool);

function getQuantity(bytes32 sku) external view returns (uint256);

function setAsk(
address issuer,
Expand All @@ -27,4 +29,15 @@ interface IPrepaidCardMarket {
uint256 faceValue,
string memory customizationDID
);

function getSKU(
address issuer,
address token,
uint256 faceValue,
string memory customizationDID
) external view returns (bytes32);

function pause() external;

function unpause() external;
}
117 changes: 97 additions & 20 deletions contracts/PrepaidCardManager.sol
Expand Up @@ -9,7 +9,7 @@ import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeab
import "./core/Ownable.sol";

import "./token/IERC677.sol";
import "./IPrepaidCardMarket.sol";
import "./PrepaidCardMarket.sol";
import "./TokenManager.sol";
import "./core/Safe.sol";
import "./core/Versionable.sol";
Expand Down Expand Up @@ -67,6 +67,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
);
event GasPolicyAdded(string action, bool useGasPrice);
event ContractSignerRemoved(address signer);
event TrustedCallerForCreatingPrepaidCardsWithIssuerRemoved(address caller);
event PrepaidCardSend(
address prepaidCard,
uint256 spendAmount,
Expand Down Expand Up @@ -100,6 +101,8 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
EnumerableSetUpgradeable.AddressSet internal contractSigners;
mapping(string => GasPolicyV2) public gasPoliciesV2;
address public versionManager;
EnumerableSetUpgradeable.AddressSet
internal trustedCallersForCreatingPrepaidCardsWithIssuer;

modifier onlyHandlers() {
require(
Expand Down Expand Up @@ -142,6 +145,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
uint256 _minAmount,
uint256 _maxAmount,
address[] calldata _contractSigners,
address[] calldata _trustedCallersForCreatingPrepaidCardsWithIssuer,
address _versionManager
) external onlyOwner {
require(_tokenManager != address(0), "tokenManager not set");
Expand Down Expand Up @@ -170,6 +174,15 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {

contractSigners.add(_contractSigners[i]);
}
for (
uint256 i = 0;
i < _trustedCallersForCreatingPrepaidCardsWithIssuer.length;
i++
) {
trustedCallersForCreatingPrepaidCardsWithIssuer.add(
_trustedCallersForCreatingPrepaidCardsWithIssuer[i]
);
}
emit Setup();
}

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

function removeTrustedCallerForCreatingPrepaidCardsWithIssuer(address caller)
external
onlyOwner
{
require(
trustedCallersForCreatingPrepaidCardsWithIssuer.contains(caller),
"caller not present"
);
trustedCallersForCreatingPrepaidCardsWithIssuer.remove(caller);
emit TrustedCallerForCreatingPrepaidCardsWithIssuerRemoved(caller);
}

/**
* @dev onTokenTransfer(ERC677) - this is the ERC677 token transfer callback.
*
Expand All @@ -215,44 +240,81 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
* )
*/
function onTokenTransfer(
address from, // solhint-disable-line no-unused-vars
address from,
uint256 amount,
bytes calldata data
) external returns (bool) {
require(
TokenManager(tokenManager).isValidToken(msg.sender),
"calling token is unaccepted"
);

(
address owner,
uint256[] memory issuingTokenAmounts,
uint256[] memory spendAmounts,
string memory customizationDID,
address marketAddress
) = abi.decode(data, (address, uint256[], uint256[], string, address));
address marketAddress,
address issuer,
address issuerSafe
) = abi.decode(
data,
(address, uint256[], uint256[], string, address, address, address)
);

require(
owner != address(0) && issuingTokenAmounts.length > 0,
"Prepaid card data invalid"
);
require(
issuingTokenAmounts.length == spendAmounts.length,
"the amount arrays have differing lengths"
);

// The spend amounts are for reporting purposes only, there is no on-chain
// effect from this value. Although, it might not be a bad idea that spend
// amounts line up with the issuing token amounts--albiet we'd need to
// introduce a rate lock mechanism if we wanted to validate this
createMultiplePrepaidCards(
owner,
from,
_msgSender(),
amount,
issuingTokenAmounts,
spendAmounts,
customizationDID,
marketAddress
require(
issuingTokenAmounts.length == spendAmounts.length,
"the amount arrays have differing lengths"
);

// When issuer and issuerSafe are blank, it means the call is related to the
// process where a prepaid card is first created, using the provided issuer as
// the owner, and later provisioned and transfered to the customer (customer's
// EOA is the new owner).

if (
(issuer == address(0) && issuerSafe == address(0)) ||
!trustedCallersForCreatingPrepaidCardsWithIssuer.contains(from)
) {
createPrepaidCards(
owner, // issuer
owner,
from, // depot
_msgSender(), // token
amount,
issuingTokenAmounts,
spendAmounts,
customizationDID,
marketAddress
);
} else {
// In case when issuer and issuerSafe are provided, it means the tokens are being
// sent from the PrepaidCardMarketV2 contract where the prepaid cards are being
// created and provisioned in a single step, where the issuer and owner (customer's EOA)
// are provided during the creation of the prepaid cards.

createPrepaidCards(
issuer,
owner,
issuerSafe, // depot
_msgSender(), // token
amount,
issuingTokenAmounts,
spendAmounts,
customizationDID,
marketAddress
);
}

return true;
}

Expand Down Expand Up @@ -302,6 +364,17 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
return contractSigners.values();
}

/**
* @dev get the addresses that are allowed to create cards with provided issuer
*/
function getTrustedCallersForCreatingPrepaidCardsWithIssuer()
external
view
returns (address[] memory)
{
return trustedCallersForCreatingPrepaidCardsWithIssuer.values();
}

/**
* @dev returns a boolean indicating if the prepaid card's owner is an EIP-1271 signer
* @param prepaidCard prepaid card address
Expand Down Expand Up @@ -523,7 +596,8 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
* @param spendAmounts array of spend amounts that represent the desired face value (for reporting only)
* @param customizationDID the customization DID for the new prepaid cards
*/
function createMultiplePrepaidCards(
function createPrepaidCards(
address issuer,
address owner,
address depot,
address token,
Expand Down Expand Up @@ -554,6 +628,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
);
for (uint256 i = 0; i < numberCard; i++) {
createPrepaidCard(
issuer,
owner,
depot,
token,
Expand All @@ -574,6 +649,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {

/**
* @dev Create Prepaid card
* @param issuer issuer address
* @param owner owner address
* @param token token address
* @param issuingTokenAmount amount of issuing token to use to fund the new prepaid card
Expand All @@ -582,6 +658,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
* @return PrepaidCard address
*/
function createPrepaidCard(
address issuer,
address owner,
address depot,
address token,
Expand All @@ -598,7 +675,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
address card = createSafe(owners, 2);

// card was created
cardDetails[card].issuer = owner;
cardDetails[card].issuer = issuer;
cardDetails[card].issueToken = token;
cardDetails[card].customizationDID = customizationDID;
cardDetails[card].blockNumber = block.number;
Expand All @@ -624,7 +701,7 @@ contract PrepaidCardManager is Ownable, Versionable, Safe {
);

if (marketAddress != address(0)) {
IPrepaidCardMarket(marketAddress).setItem(owner, card);
PrepaidCardMarket(marketAddress).setItem(owner, card);
}

return card;
Expand Down
15 changes: 13 additions & 2 deletions contracts/PrepaidCardMarket.sol
Expand Up @@ -115,14 +115,26 @@ contract PrepaidCardMarket is Ownable, Versionable, IPrepaidCardMarket {
emit Setup();
}

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

function pause() external onlyOwner {
paused = true;
emit PausedToggled(true);
}

function unpause() external onlyOwner {
paused = false;
emit PausedToggled(false);
}

function setItem(address issuer, address prepaidCard)
external
override
onlyHandlersOrPrepaidCardManager
returns (bool)
{
Expand Down Expand Up @@ -155,7 +167,6 @@ contract PrepaidCardMarket is Ownable, Versionable, IPrepaidCardMarket {

function removeItems(address issuer, address[] calldata prepaidCards)
external
override
onlyHandlers
returns (bool)
{
Expand Down

0 comments on commit 919c078

Please sign in to comment.