Repository navigation
Conversation
There was a problem hiding this comment.
Nice work on this. The core is solid
I ran the suite locally at 045956b (SDK 3.4.11). check.sh, dpm build --all, check-coverage.sh (67 tests) and check-sandbox.sh all pass. I then wrote scratch Daml Script tests for the points below, so the inline comments include what they printed. I can share the test module.
Inline, roughly by priority:
SettleBatchaccepts non-TokenAllocationcontracts (critical).- The same pattern in
TokenRules_ExpireAllocations. - Direct
Allocation_Settleskips exact cover. - The vendored DARs conflict with the official Splice DARs.
- Owner-only calls on provider accounts (CIP-112 4.3.2 MUST).
OwnerUnlockemits no event (CIP-112 MUST).- The V1 guidance conflicts with CIP-112 5.1.
- There are no enforceable extension points.
Two small things outside the code:
- The
INFO -,QUESTION -andREVISITannotations need to go before merge. TheQUESTIONintransferImplneeds an answer.
All of this is open for discussion. Some points are design trade-offs, so push back where you see it differently.
| @@ -287,11 +193,20 @@ allocateImpl rules arg = do | |||
| authorizer = allocation.authorizer | |||
| checkActors arg.actors [accountParties admin authorizer] | |||
There was a problem hiding this comment.
Owner-only calls on provider accounts (CIP-112 4.3.2 MUST)
The
ownerMUST be able to call theTransferFactory_TransferandAllocationFactory_Allocatechoices, but it is left open to the registries whether this directly creates anAllocationor executes the transfer, or whether this only creates anAllocationInstructionorTransferInstructionwhich theproviderneeds to accept.
This check (and Transfer.daml:132) requires owner + provider to initiate. In my test the owner-only calls fail and owner + provider succeeds. Joint authorization of the effect is fine; the gap is the owner's initiation path.
Options:
- Return a pending instruction that the provider accepts.
- Use
AccountConfig-style delegation, as TestTokenV2 does. - Document the package as basic-accounts-only for now.
No test uses a distinct provider yet.
There was a problem hiding this comment.
@pepebndc We can discuss the authorization model now that we have an implementation, if we want to be "owner or provider", or "owner and provider". I would go for "owner or provider" cc. @ericnordelo
There was a problem hiding this comment.
Agreed to decide this in the sync. "Owner or provider" also satisfies the CIP-112 MUST, because the owner can then call both factories alone.
There was a problem hiding this comment.
@igingu why not making that configurable using an AccountConfig?
| deployment target requires V1 visibility, add the V1 interface instances to | ||
| the same templates: the V1 DARs are vendored under | ||
| [`dars/vendor/`](../../../dars/vendor/), and the vendored utils ship | ||
| `...V1...DefaultImplUsingV2` helpers for exactly this pattern. |
There was a problem hiding this comment.
V1 on TokenAllocation conflicts with CIP-112 5.1
Assets MUST NOT implement the V1
Allocationinterface onAllocationsthat are not settleable via the V1Allocation_ExecuteTransferflow. In practice this means assets need to maintain two Allocation implementations, one for V2-only flows, one for V1/V2 dual-compatibility flows.
TokenAllocation supports multi-leg, committed and iterated allocations, so it cannot carry V1. The DefaultImplUsingV2 helpers still fit TokenHolding, TokenTransferInstruction and the factories, where V1 is a SHOULD. V1 allocations need a separate, constrained template, as TestTokenV2 does. The same applies to ideas.md item 1.
Because current wallets and exchanges use V1, this is also the largest adoption gap.
There was a problem hiding this comment.
OK to discuss in the sync. If the package stays V2-only, update the README "Standards conformance" paragraph and ideas.md item 1 in a follow-up, because they still suggest adding V1 to TokenAllocation.
| `OpenZeppelin.TokenCIP112V1`. It implements the Token Standard V2 interfaces | ||
| and builds against the 13 vendored Token Standard V2 DARs under | ||
| `dars/vendor/`, with provenance recorded in `dars/manifest.yaml`. | ||
| and builds against the 13 official Token Standard V2 DARs released by Splice |
There was a problem hiding this comment.
Can we simplify the token entries here in general, it reads as if have been being incrementaly modified, but the added section should contain just the aggregation of what is being added as a unit, as this is all part of the same unreleased version, not a diff. I would also try to keep the Changelog entries as concise and short as possible.
| -- pending instruction state exists in this registry because allocation needs | ||
| -- no approval beyond the authorizer's account parties. | ||
| allocateImpl | ||
| : TokenRules |
There was a problem hiding this comment.
Can we make this function depend on an allocation config instead? The advantages are:
- Allowing to reuse this allocation logic in custom templates without needing to create a TokenRules record without the contract
- Allowing the extend the TokenRules state later without affecting the allocation logic:
data AllocationConfig = AllocationConfig with
admin : Party
maxTTL : RelTime
lockGrace : RelTime
There was a problem hiding this comment.
Someone could do something like this (note how they can use our helpers partially)
Code Example
module MyApp.AllocationFactory where
import DA.Time (RelTime, seconds)
import Splice.Api.Token.MetadataV1
import Splice.Api.Token.AllocationInstructionV2 qualified as AI
import Splice.TokenStandard.Utils
import OpenZeppelin.TokenCIP112V1.Registry qualified as OZ
template ExchangeAllocationFactory
with
admin : Party
allowedExecutors : [Party]
maxTTL : RelTime
lockGrace : RelTime
where
signatory admin
ensure maxTTL > seconds 0
&& lockGrace > seconds 0
interface instance AI.AllocationFactory
for ExchangeAllocationFactory where
view = AI.AllocationFactoryView with
admin
meta = emptyMetadata
allocationFactory_publicFetchImpl =
publicFetchDefaultImpl this
allocationFactory_allocateExtraObservers =
allocationFactoryV2_privateAsset_allocateExtraObserversDefaultImpl
allocationFactory_allocateImpl _ arg = do
-- My application's additional rule.
assertMsg "An executor is not approved"
(all
(\executor -> executor `elem` allowedExecutors)
arg.settlement.executors)
-- Construct the library template's payload just to call its helper.
let rules = OZ.TokenRules with
admin
maxTTL
lockGrace
-- Reuse the library's allocation mechanics.
OZ.allocateImpl rules arg
|
Hey, I’ve been looking at how integrators could use this package as a data dependency while owning the contracts they deploy. TLDR: What I'm proposing is simpler than it looks here to implement, but sort of hard to explain. The idea is to use interface exercising to separate templates from workflows, and to isolate templates on their own packages so users can choose to use them as a direct dependency or a fork, with the option of using the rest of the library always as a direct dependency, with the option of opting out at any point if templates are decoupled. More detailsI think we should make the concrete templates replaceable, while keeping our current templates as convenient defaults. The main benefit is letting consumers customize their contracts and manage their own upgrade lineage while continuing to reuse our workflow logic. 1. Separate workflow logic from contract creationI’d make helpers receive creation functions instead of directly creating TokenHolding, TokenTransferInstruction, or TokenAllocation. For example, the holding constructor could have this signature: Our default implementation would supply the equivalent constructor for TokenHolding. This works across compiled DAR data dependencies, so consumers can import our helpers without copying their implementation. A dedicated on-ledger holding factory isn’t necessary for this approach. 2. Use existing Splice interfaces at contract boundariesWhere the standard already provides the required operation, helpers should work with interface contract IDs and interface fetches/exercises. That lets the same workflow code interact with different consumer implementations. It also lets unchanged callers use compatible newer template implementations through dynamic package selection. We still need an explicit policy for accepted implementations: implementing a standard interface alone does not establish that a contract is trusted. Existing concrete-template guards need a deliberate replacement, rather than simply being removed. 3. Carry the abstraction through pending workflowsCustom holding creation needs to work when a workflow finishes later, including acceptance, rejection, withdrawal, settlement, and release. Functions cannot be stored in contract payloads. A consumer-owned transfer instruction can store ordinary configuration and supply its constructor again when executing a later choice: This is why I think replaceable transfer instructions matter even when the consumer only wants a custom holding: the instruction determines which holding implementation gets created when the transfer completes or refunds. The same reasoning applies to allocations, including any successor allocation created during iteration. Our helpers should retain the shared validation and accounting; the consumer wrapper supplies its configuration and implementation choices. 4. Separate template ownership from spending behaviorCreation callbacks allow custom fields, additional choices, and consumer-owned template evolution. They do not automatically support arbitrary asset behavior. For example, fetching and archiving through the holding interface does not execute a custom Spend or Freeze choice. If we intend to support custom spending semantics, we should expose an explicit consumption operation, through a callback or a dedicated interface choice. Otherwise, we should clearly document the holding behavior our helpers assume. ’d keep v1’s supported behavior explicit and introduce additional operations when there is a concrete integration requiring them. 5. Structure packages around the intended upgrade independenceI’d separate:
Consumer-owned templates give integrators their own upgrade lineage. Interface dispatch allows callers to follow compatible implementations, but normal SCU restrictions still apply. There is also a packaging distinction: a function-only utility dependency can be replaceable in ways that a dependency defining templates, interfaces, or serializable types is not. Separate modules within one package do not provide that separation. So we should promise independent evolution of consumer implementations, while being precise about the shared API dependencies they must retain. Existing contracts created from our templates would still require a migration path to move into unrelated consumer templates. 6. What I verified in a PoCUsing SDK 3.4.11, LF 2.1, and the actual Splice V2 interfaces, I verified:
The runtime scripts and standalone upgrade checks passed. Storing a callback in a contract and making an incompatible field change were correctly rejected. This verifies the composition mechanism in a simulated ledger; it is not a full CIP-112 conformance or multi-participant deployment test. For v1, I’d prioritize constructor callbacks, existing Splice interface boundaries, and thin consumer-owned workflow wrappers, alongside the ready-to-use defaults. That gives us a concrete extension path without requiring a general-purpose factory framework. |
…ementations. Add HoldingOps with hooks for creating/consuming a holding, custom logging, as well as adding extra observers.
|
@ericnordelo I have updated the PR to split the existing functionality into two packages: In order for templates to be able to customise part of the shape of a holding, as well as key implementation steps, I added a We had a discussion about using this method of passing hooks versus using and exercising on interfaces. Based on my limited research, there aren't many significant benefits to switching to interfaces, so I kept the same shape of the code. |
| their own ships two production packages: | ||
|
|
||
| ```text | ||
| <component>-workflows-v1 Choice bodies as functions, parameterised over the |
There was a problem hiding this comment.
| <component>-workflows-v1 Choice bodies as functions, parameterised over the | |
| <component>-utils-v1 Choice bodies as functions, parameterised over the |
All other relevant refs and names must be updated, as per #55
| nonconsuming choice TokenRules_Mint : ContractId HoldingV2.Holding | ||
| -- ^ Issue holdings into an account, emitting the standard mint event. | ||
| with | ||
| account : HoldingV2.Account | ||
| instrumentId : Text | ||
| amount : Decimal | ||
| reason : Text | ||
| controller config.admin :: accountParties config.admin account | ||
| do mintImpl (rulesOps config) config.admin account instrumentId amount reason |
There was a problem hiding this comment.
Minting doesn't work if admin and account are on different participants; even on same participants, account must give the participant node submission rights.
The current minting flow requires the admin to always mint first to itself, and then initiate a regular two-step transfer.
The same goes for TokenRules_Burn flow, only in reverse (first transfer to admin, then admin invokes burn)
Was this design intentional?
No description provided.