Skip to content

Commit

Permalink
Merge pull request #211 from gildlab/2024-12-27-view
Browse files Browse the repository at this point in the history
better json for receipt
  • Loading branch information
hardyjosh authored Jan 3, 2025
2 parents eef1064 + d9d252e commit 1fda23e
Show file tree
Hide file tree
Showing 11 changed files with 751 additions and 257 deletions.
365 changes: 184 additions & 181 deletions .gas-snapshot

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
[submodule "lib/solady"]
path = lib/solady
url = https://github.com/Vectorized/solady
[submodule "lib/rain.string"]
path = lib/rain.string
url = https://github.com/rainlanguage/rain.string
1 change: 1 addition & 0 deletions lib/rain.string
Submodule rain.string added at 452e4b
98 changes: 98 additions & 0 deletions src/concrete/receipt/ERC20PriceOracleReceipt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {Receipt, DATA_URI_BASE64_PREFIX, Base64} from "./Receipt.sol";
import {LibFixedPointDecimalFormat} from "rain.math.fixedpoint/lib/format/LibFixedPointDecimalFormat.sol";
import {ZeroReceiptId} from "../../error/ErrReceipt.sol";
import {
LibFixedPointDecimalArithmeticOpenZeppelin,
Math
} from "rain.math.fixedpoint/lib/LibFixedPointDecimalArithmeticOpenZeppelin.sol";
import {FIXED_POINT_ONE} from "rain.math.fixedpoint/lib/FixedPointDecimalConstants.sol";

/// @dev The default symbol for the reference asset.
string constant DEFAULT_REFERENCE_ASSET_SYMBOL = "USD";

/// @dev The default URL for redeeming receipts.
string constant DEFAULT_REDEEM_URL = "";

/// @dev The default brand name for the receipt.
string constant DEFAULT_BRAND_NAME = "";

/// @dev The default SVG URI for the receipt.
string constant DEFAULT_SVG_URI = "";

contract ERC20PriceOracleReceipt is Receipt {
/// @inheritdoc Receipt
function uri(uint256 id) public view virtual override returns (string memory) {
if (id == 0) {
revert ZeroReceiptId();
}
string memory redeemURL = _redeemURL();
string memory redeemURLPhrase = bytes(redeemURL).length > 0 ? string.concat(" Redeem at ", redeemURL, ".") : "";

string memory brandName = _brandName();
string memory brandNamePhrase = bytes(brandName).length > 0 ? string.concat(brandName, " ") : "";

string memory receiptSVGURI = _receiptSVGURI();
string memory receiptSVGURIPhrase =
bytes(receiptSVGURI).length > 0 ? string.concat("\"image\":\"", receiptSVGURI, "\",") : "";

bytes memory json = bytes(
string.concat(
"{\"decimals\":18,\"description\":\"1 of these receipts can be burned alongside 1 ",
_vaultShareSymbol(),
" to redeem ",
LibFixedPointDecimalFormat.fixedPointToDecimalString(
LibFixedPointDecimalArithmeticOpenZeppelin.fixedPointDiv(FIXED_POINT_ONE, id, Math.Rounding.Down)
),
" of ",
_vaultAssetSymbol(),
".",
redeemURLPhrase,
"\",",
receiptSVGURIPhrase,
"\"name\":\"Receipt for ",
brandNamePhrase,
"lock at ",
LibFixedPointDecimalFormat.fixedPointToDecimalString(id),
" ",
_referenceAssetSymbol(),
" per ",
_vaultAssetSymbol(),
".\"}"
)
);

return string.concat(DATA_URI_BASE64_PREFIX, Base64.encode(json));
}

/// Provides the SVG URI for the receipt. Can be overridden to provide a
/// custom SVG URI. Default is an empty string, which will not include an
/// image in the metadata json.
function _receiptSVGURI() internal view virtual returns (string memory) {
return DEFAULT_SVG_URI;
}

/// Provides the symbol of the reference asset that mint amounts are valued
/// in. Can be overridden to provide a custom reference asset symbol. Default
/// is "USD".
function _referenceAssetSymbol() internal view virtual returns (string memory) {
return DEFAULT_REFERENCE_ASSET_SYMBOL;
}

/// Provides the URL for redeeming receipts. Can be overridden to provide a
/// custom redeem URL. Default is an empty string, which will not include a
/// redeem URL in the metadata json.
function _redeemURL() internal view virtual returns (string memory) {
return DEFAULT_REDEEM_URL;
}

/// Provides the brand name for the receipt. Can be overridden to provide a
/// custom brand name. Default is an empty string, which will not include a
/// brand name in the metadata json.
function _brandName() internal view virtual returns (string memory) {
return DEFAULT_BRAND_NAME;
}
}
62 changes: 47 additions & 15 deletions src/concrete/receipt/Receipt.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,23 @@ import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "rain.factory/interface/IClone

import {IReceiptManagerV1} from "../../interface/IReceiptManagerV1.sol";
import {IReceiptV2} from "../../interface/IReceiptV2.sol";
import {IReceiptVaultV1} from "../../interface/IReceiptVaultV1.sol";
import {OnlyManager} from "../../error/ErrReceipt.sol";
import {ERC1155Upgradeable as ERC1155} from
"openzeppelin-contracts-upgradeable/contracts/token/ERC1155/ERC1155Upgradeable.sol";
import {StringsUpgradeable as Strings} from "openzeppelin-contracts-upgradeable/contracts/utils/StringsUpgradeable.sol";
import {IERC20MetadataUpgradeable as IERC20Metadata} from
"openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
import {Base64Upgradeable as Base64} from "openzeppelin-contracts-upgradeable/contracts/utils/Base64Upgradeable.sol";

/// @dev The prefix for data URIs as base64 encoded JSON.
string constant DATA_URI_BASE64_PREFIX = "data:application/json;base64,";

/// @dev The URI for the metadata of the `Receipt` contract.
/// Decodes to a simple generic receipt metadata object.
/// `{"name":"Receipt","decimals":18,"description":"A receipt for a ReceiptVault."}`
string constant RECEIPT_METADATA_DATA_URI =
"eyJuYW1lIjoiUmVjZWlwdCIsImRlY2ltYWxzIjoxOCwiZGVzY3JpcHRpb24iOiJBIHJlY2VpcHQgZm9yIGEgUmVjZWlwdFZhdWx0LiJ9";
/// @dev The name of a `Receipt` is "<vault share symbol> Receipt".
string constant RECEIPT_NAME_SUFFIX = " Receipt";

/// @dev The symbol for the `Receipt` contract.
string constant RECEIPT_SYMBOL = "RECEIPT";

/// @dev The name for the `Receipt` contract.
string constant RECEIPT_NAME = "Receipt";
/// @dev The symbol of a `Receipt` is "<vault share symbol> RCPT".
string constant RECEIPT_SYMBOL_SUFFIX = " RCPT";

/// @title Receipt
/// @notice The `IReceiptV2` for a `ReceiptVault`. Standard implementation allows
Expand Down Expand Up @@ -54,22 +52,56 @@ contract Receipt is IReceiptV2, ERC1155, ICloneableV2 {
/// implementation in `ReceiptFactory`.
/// Compatible with `ICloneableV2`.
function initialize(bytes memory data) external override initializer returns (bytes32) {
__ERC1155_init(string.concat(DATA_URI_BASE64_PREFIX, RECEIPT_METADATA_DATA_URI));
// `uri` is overridden in this contract so we can just initialize
// `ERC1155` with an empty string.
__ERC1155_init("");

address receiptManager = abi.decode(data, (address));
sManager = IReceiptManagerV1(receiptManager);

return ICLONEABLE_V2_SUCCESS;
}

/// @inheritdoc ERC1155
function uri(uint256) public view virtual override returns (string memory) {
bytes memory json = bytes(
string.concat(
"{\"decimals\":18,\"description\":\"1 of these receipts can be burned alongside 1 ",
_vaultShareSymbol(),
" to redeem ",
_vaultAssetSymbol(),
" from the vault.\",",
"\"name\":\"",
name(),
"\"}"
)
);

return string.concat(DATA_URI_BASE64_PREFIX, Base64.encode(json));
}

/// @inheritdoc IReceiptV2
function name() external pure virtual returns (string memory) {
return RECEIPT_NAME;
function name() public view virtual returns (string memory) {
return string.concat(_vaultShareSymbol(), RECEIPT_NAME_SUFFIX);
}

/// @inheritdoc IReceiptV2
function symbol() external pure virtual returns (string memory) {
return RECEIPT_SYMBOL;
function symbol() external view virtual returns (string memory) {
return string.concat(_vaultShareSymbol(), RECEIPT_SYMBOL_SUFFIX);
}

/// Provides the symbol of the `ReceiptVault` ERC20 share token that manages
/// this `Receipt`. Can be overridden if the manager is not going to be
/// a `ReceiptVault`.
function _vaultShareSymbol() internal view virtual returns (string memory) {
return IERC20Metadata(payable(address(sManager))).symbol();
}

/// Provides the symbol of the ERC20 asset token that the `ReceiptVault`
/// managing this `Receipt` is accepting for mints. Can be overridden if the
/// manager is not going to be a `ReceiptVault`.
function _vaultAssetSymbol() internal view virtual returns (string memory) {
return IERC20Metadata(IReceiptVaultV1(payable(address(sManager))).asset()).symbol();
}

/// @inheritdoc IReceiptV2
Expand Down
3 changes: 3 additions & 0 deletions src/error/ErrReceipt.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ pragma solidity ^0.8.25;

/// Thrown when the manager is not the caller.
error OnlyManager();

/// Thrown when the receipt ID is zero.
error ZeroReceiptId();
65 changes: 54 additions & 11 deletions test/abstract/ReceiptFactoryTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,67 @@ import {ICloneableFactoryV2} from "rain.factory/interface/ICloneableFactoryV2.so
import {CloneFactory} from "rain.factory/concrete/CloneFactory.sol";
import {Test, Vm} from "forge-std/Test.sol";
import {Receipt as ReceiptContract} from "src/concrete/receipt/Receipt.sol";
import {ERC20PriceOracleReceipt} from "src/concrete/receipt/ERC20PriceOracleReceipt.sol";
import {DATA_URI_BASE64_PREFIX} from "src/concrete/receipt/Receipt.sol";
import {Base64} from "solady/utils/Base64.sol";

contract ReceiptFactoryTest is Test {
struct Metadata {
uint8 decimals;
string description;
string name;
}

struct MetadataWithImage {
uint8 decimals;
string description;
string image;
string name;
}

ICloneableFactoryV2 internal immutable iFactory;
ReceiptContract internal immutable receiptImplementation;
ReceiptContract internal immutable iReceiptImplementation;
ERC20PriceOracleReceipt internal immutable iERC20PriceOracleReceiptImplementation;

constructor() {
iFactory = new CloneFactory();
receiptImplementation = new ReceiptContract();
iReceiptImplementation = new ReceiptContract();
iERC20PriceOracleReceiptImplementation = new ERC20PriceOracleReceipt();
}

function decodeMetadataURI(string memory uri) internal pure returns (Metadata memory) {
uint256 uriLength = bytes(uri).length;
assembly ("memory-safe") {
mstore(uri, 29)
}
assertEq(uri, DATA_URI_BASE64_PREFIX);
assembly ("memory-safe") {
uri := add(uri, 29)
mstore(uri, sub(uriLength, 29))
}

string memory uriDecoded = string(Base64.decode(uri));
bytes memory uriJsonData = vm.parseJson(uriDecoded);

Metadata memory metadataJson = abi.decode(uriJsonData, (Metadata));
return metadataJson;
}

/// Creates a new `ReceiptContract` clone with the specified manager.
/// @param manager The address to set as the manager of the new ReceiptContract
/// @return The address of the newly created `ReceiptContract` clone
function createReceipt(address manager) internal returns (ReceiptContract) {
// Clone ReceiptContract using the factory and initialize it with the
// manager.
address clone = iFactory.clone(address(receiptImplementation), abi.encode(manager));
// Return the clone cast to ReceiptContract type
return ReceiptContract(clone);
function decodeMetadataURIWithImage(string memory uri) internal pure returns (MetadataWithImage memory) {
uint256 uriLength = bytes(uri).length;
assembly ("memory-safe") {
mstore(uri, 29)
}
assertEq(uri, DATA_URI_BASE64_PREFIX);
assembly ("memory-safe") {
uri := add(uri, 29)
mstore(uri, sub(uriLength, 29))
}

string memory uriDecoded = string(Base64.decode(uri));
bytes memory uriJsonData = vm.parseJson(uriDecoded);

MetadataWithImage memory metadataJson = abi.decode(uriJsonData, (MetadataWithImage));
return metadataJson;
}
}
29 changes: 29 additions & 0 deletions test/concrete/TestReceiptManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,33 @@ import {IReceiptManagerV1} from "src/interface/IReceiptManagerV1.sol";
/// @param to The transfer attemped to this address.
error UnauthorizedTransfer(address from, address to);

contract TestReceiptManagerAsset {
function symbol() external pure returns (string memory) {
return "TRMAsset";
}

function name() external pure returns (string memory) {
return "TestReceiptManagerAsset";
}
}

/// @title TestReceiptManager
/// @notice TEST contract that can be the manager of an `IReceiptV2` and forward
/// function calls to the manager restricted functions on the receipt. Completely
/// insecure, intended for use only by the test harness to drive tests.
contract TestReceiptManager is IReceiptManagerV1 {
/// The address of the test asset.
address internal iAsset;

/// The address that is authorized to send transfers.
address internal sFrom;
/// The address that is authorized to receive transfers.
address internal sTo;

constructor() {
iAsset = address(new TestReceiptManagerAsset());
}

/// Anon can set the from address.
/// @param from The new `from` address.
function setFrom(address from) external {
Expand Down Expand Up @@ -86,4 +103,16 @@ contract TestReceiptManager is IReceiptManagerV1 {
) external {
receipt.managerTransferFrom(from, to, id, amount, data);
}

function name() external pure returns (string memory) {
return "TestReceiptManager";
}

function symbol() external pure returns (string memory) {
return "TRM";
}

function asset() external view returns (address) {
return iAsset;
}
}
Loading

0 comments on commit 1fda23e

Please sign in to comment.