-
Notifications
You must be signed in to change notification settings - Fork 3
feat: MCMS Treasury helper contract for fees #786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JohnChangUK
wants to merge
4
commits into
main
Choose a base branch
from
feat/mcms-fee-treasury
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
693 changes: 693 additions & 0 deletions
693
contracts/bindings/generated/ccip/feetreasury/feetreasury.go
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| sdk-version: 3.4.11 | ||
| name: ccip-fee-treasury | ||
| source: daml | ||
| version: 1.0.0 | ||
| build-options: | ||
| - "-Wno-crypto-text-is-alpha" | ||
| - "-Werror=unused-dependency" | ||
| data-dependencies: | ||
| - ../../dependencies/splice/splice-api-token-metadata-v1-1.0.0.dar | ||
| - ../../dependencies/splice/splice-api-token-holding-v1-1.0.0.dar | ||
| - ../../dependencies/splice/splice-api-token-transfer-instruction-v1-1.0.0.dar | ||
| dependencies: | ||
| - daml-prim | ||
| - daml-stdlib | ||
| - ../../chainlink/api/.daml/dist/chainlink-api-2.0.0.dar | ||
| - ../../mcms/api/.daml/dist/mcms-api-1.0.0.dar | ||
| - ../codec/.daml/dist/ccip-codec-v2-2.0.0.dar |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| -- Two-phase design: | ||
| -- 1. feeOwner -> MCMSFeeTreasury.AuthorizeFeeWithdrawal | ||
| -- Creates a FeeWithdrawalAuthorization capturing the approved policy (recipient, | ||
| -- instrument, amount cap, expiry). | ||
| -- 2. anyone -> FeeWithdrawalAuthorization.ExecuteFeeWithdrawal | ||
| -- Supplies live ContractIds (holdings, TransferFactory) + ExtraArgs (disclosed | ||
| -- mining-round context). Not subject to MCMS validateTargetCidsSize cap. | ||
| module CCIP.FeeTreasury where | ||
|
|
||
| import DA.Crypto.Text (byteCount) | ||
| import DA.Map qualified as Map | ||
| import DA.Time (addRelTime, seconds) | ||
|
|
||
| import Splice.Api.Token.HoldingV1 (Holding, InstrumentId(..)) | ||
| import Splice.Api.Token.MetadataV1 (ExtraArgs, emptyMetadata) | ||
| import Splice.Api.Token.TransferInstructionV1 ( | ||
| Transfer(..), | ||
| TransferFactory, | ||
| TransferFactory_Transfer(..), | ||
| TransferInstructionResult, | ||
| TransferInstructionResult_Output(..), | ||
| ) | ||
|
|
||
| import Chainlink.InstanceAddress qualified as RawInstanceAddress | ||
| import Chainlink.InstanceAddress (assertValidInstanceId) | ||
|
|
||
| import MCMS.MCMSReceiver (MCMSReceiver(..), MCMSReceiverView(..)) | ||
|
|
||
| import CCIP.FeeTreasuryTypes (AuthorizeFeeWithdrawalParams(..)) | ||
| import CCIP.FeeTreasuryCodecGen (decodeAuthorizeFeeWithdrawalParamsAt) | ||
|
|
||
| template MCMSFeeTreasury | ||
| with | ||
| instanceId : Text | ||
| feeOwner : Party | ||
| mcmsController : Party | ||
| where | ||
| signatory feeOwner | ||
| observer mcmsController | ||
|
|
||
| ensure assertValidInstanceId instanceId | ||
|
|
||
| -- Nonconsuming: authorizing does not mutate the treasury. The single-use | ||
| -- capability is the created FeeWithdrawalAuthorization contract itself (its | ||
| -- ContractId), consumed by ExecuteFeeWithdrawal, WithdrawAuthorization, or | ||
| -- CleanupExpiredAuthorization. | ||
| nonconsuming choice AuthorizeFeeWithdrawal : ContractId FeeWithdrawalAuthorization | ||
| with | ||
| params : AuthorizeFeeWithdrawalParams | ||
| controller feeOwner | ||
| do | ||
| now <- getTime | ||
| let expiresAt = now `addRelTime` seconds params.validitySecs | ||
| create FeeWithdrawalAuthorization with | ||
| instanceId = params.authorizationId | ||
| feeOwner | ||
| mcmsController | ||
| recipient = params.recipient | ||
| instrumentId = params.instrumentId | ||
| maxAmount = params.maxAmount | ||
| expiresAt | ||
|
|
||
| interface instance MCMSReceiver for MCMSFeeTreasury where | ||
| view = MCMSReceiverView with | ||
| mcmsController | ||
| instanceId | ||
|
|
||
| mcmsEntrypoint functionName operationData contractIds = do | ||
| let instAddr = RawInstanceAddress.makeText instanceId feeOwner | ||
| selfCid <- case Map.lookup instAddr contractIds of | ||
| None -> abort $ "E_SELF_CID_NOT_IN_MAP: " <> instAddr | ||
| Some cid -> pure (coerceContractId cid : ContractId MCMSFeeTreasury) | ||
|
|
||
| case functionName of | ||
| "AuthorizeFeeWithdrawal" -> do | ||
| case decodeAuthorizeFeeWithdrawalParamsAt operationData 0 of | ||
| None -> abort "E_INVALID_PARAMS: AuthorizeFeeWithdrawal" | ||
| Some (params, finalOffset) -> do | ||
| assertMsg "E_TRAILING_BYTES: AuthorizeFeeWithdrawal" (finalOffset == byteCount operationData) | ||
| _ <- exercise selfCid AuthorizeFeeWithdrawal with params | ||
| -- AuthorizeFeeWithdrawal is nonconsuming: the treasury CID is | ||
| -- unchanged, so the contractIds map is returned as-is. | ||
| pure contractIds | ||
| _ -> abort $ "E_UNKNOWN_FUNCTION: " <> functionName | ||
|
|
||
|
|
||
| -- | Single-use MCMS-approved authorization to withdraw fees from feeOwner. | ||
| -- Anyone can submit ExecuteFeeWithdrawal — security comes from the locked policy | ||
| -- (recipient, instrument, amount cap, expiry), not from restricting who calls it. | ||
| -- instanceId makes this authorization independently addressable by MCMS for cancellation. | ||
| template FeeWithdrawalAuthorization | ||
| with | ||
| instanceId : Text | ||
| feeOwner : Party | ||
| mcmsController : Party | ||
| recipient : Party | ||
| instrumentId : InstrumentId | ||
| maxAmount : Decimal | ||
| expiresAt : Time | ||
| where | ||
| signatory feeOwner | ||
| observer recipient, mcmsController | ||
|
|
||
| ensure assertValidInstanceId instanceId | ||
|
|
||
| -- | Execute the MCMS-approved withdrawal with live CIDs and disclosed context. | ||
| choice ExecuteFeeWithdrawal : TransferInstructionResult | ||
| with | ||
| submitter : Party | ||
| transferFactoryCid : ContractId TransferFactory | ||
| inputHoldingCids : [ContractId Holding] | ||
| amount : Decimal | ||
| extraArgs : ExtraArgs | ||
| requestedAt : Time | ||
| controller submitter | ||
| do | ||
| now <- getTime | ||
| assertMsg "feetreasury: requestedAt is in the future" (requestedAt <= now) | ||
| assertMsg "feetreasury: withdrawal authorization expired" (now < expiresAt) | ||
| assertMsg "feetreasury: amount exceeds authorized cap" (amount <= maxAmount) | ||
| assertMsg "feetreasury: amount must be positive" (amount > 0.0) | ||
| assertMsg "feetreasury: input holdings must not be empty" (not (null inputHoldingCids)) | ||
|
|
||
| result <- exercise transferFactoryCid TransferFactory_Transfer with | ||
| expectedAdmin = instrumentId.admin | ||
| transfer = Transfer with | ||
| sender = feeOwner | ||
| receiver = recipient | ||
| amount | ||
| instrumentId | ||
| requestedAt | ||
| executeBefore = expiresAt | ||
| inputHoldingCids | ||
| meta = emptyMetadata | ||
| extraArgs | ||
|
|
||
| case result.output of | ||
| TransferInstructionResult_Failed -> | ||
| abort "feetreasury: token transfer failed" | ||
| _ -> pure result | ||
|
|
||
| -- | Cancel an unused authorization (e.g. after expiry or policy change). | ||
| choice WithdrawAuthorization : () | ||
| controller feeOwner | ||
| do pure () | ||
|
|
||
| -- | Archive an expired authorization without requiring feeOwner authority. | ||
| choice CleanupExpiredAuthorization : () | ||
| with | ||
| submitter : Party | ||
| controller submitter | ||
| do | ||
| now <- getTime | ||
| assertMsg "feetreasury: withdrawal authorization not expired" (now >= expiresAt) | ||
|
|
||
| interface instance MCMSReceiver for FeeWithdrawalAuthorization where | ||
| view = MCMSReceiverView with | ||
| mcmsController | ||
| instanceId | ||
|
|
||
| mcmsEntrypoint functionName operationData contractIds = do | ||
| let instAddr = RawInstanceAddress.makeText instanceId feeOwner | ||
| selfCid <- case Map.lookup instAddr contractIds of | ||
| None -> abort $ "E_SELF_CID_NOT_IN_MAP: " <> instAddr | ||
| Some cid -> pure (coerceContractId cid : ContractId FeeWithdrawalAuthorization) | ||
|
|
||
| case functionName of | ||
| "WithdrawAuthorization" -> do | ||
| assertMsg "E_TRAILING_BYTES: WithdrawAuthorization" (byteCount operationData == 0) | ||
| _ <- exercise selfCid WithdrawAuthorization | ||
| pure (Map.delete instAddr contractIds) | ||
| _ -> abort $ "E_UNKNOWN_FUNCTION: " <> functionName | ||
45 changes: 45 additions & 0 deletions
45
contracts/ccip/fee-treasury/daml/CCIP/FeeTreasuryCodecGen.daml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| -- AUTO-GENERATED by go-daml codegen. DO NOT EDIT. | ||
| module CCIP.FeeTreasuryCodecGen where | ||
|
|
||
| import DA.Crypto.Text (BytesHex) | ||
| import DA.Optional (fromSome) | ||
|
|
||
| import CCIP.FeeTreasuryTypes (AuthorizeFeeWithdrawalParams(..)) | ||
|
|
||
| import MCMS.Codec ( | ||
| decodeDecimalAt, | ||
| decodeInt64At, | ||
| decodePartyAt, | ||
| decodeTextAt, | ||
| encodeDecimal, | ||
| encodeInt64, | ||
| encodeParty, | ||
| encodeText | ||
| ) | ||
|
|
||
| import CCIP.CodecV2 (decodeInstrumentId, encodeInstrumentId) | ||
|
|
||
|
|
||
| -- =============================================== | ||
| -- | AuthorizeFeeWithdrawalParams | ||
| -- =============================================== | ||
|
|
||
| encodeAuthorizeFeeWithdrawalParams : AuthorizeFeeWithdrawalParams -> BytesHex | ||
| encodeAuthorizeFeeWithdrawalParams params = | ||
| encodeText params.authorizationId | ||
| <> encodeParty params.recipient | ||
| <> encodeInstrumentId params.instrumentId | ||
| <> encodeDecimal params.maxAmount | ||
| <> fromSome (encodeInt64 params.validitySecs) | ||
|
|
||
| decodeAuthorizeFeeWithdrawalParamsAt : BytesHex -> Int -> Optional (AuthorizeFeeWithdrawalParams, Int) | ||
| decodeAuthorizeFeeWithdrawalParamsAt encoded offset = do | ||
| (authorizationId, offset) <- decodeTextAt encoded offset | ||
| (recipient, offset) <- decodePartyAt encoded offset | ||
| (instrumentId, offset) <- decodeInstrumentId encoded offset | ||
| (maxAmount, offset) <- decodeDecimalAt encoded offset | ||
| (validitySecs, offset) <- decodeInt64At encoded offset | ||
| Some (AuthorizeFeeWithdrawalParams{..}, offset) | ||
|
|
||
|
|
||
|
|
20 changes: 20 additions & 0 deletions
20
contracts/ccip/fee-treasury/daml/CCIP/FeeTreasuryTypes.daml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| -- | Parameter types for MCMSFeeTreasury MCMS entrypoint functions. | ||
| -- AUTO-GENERATED codecs are derived from this file by `make generate-daml-codecs`. | ||
| module CCIP.FeeTreasuryTypes where | ||
|
|
||
| import Splice.Api.Token.HoldingV1 (InstrumentId) | ||
|
|
||
| -- | Params for the "AuthorizeFeeWithdrawal" MCMS function. | ||
| -- All fields are scalars so they can be safely encoded in the signed merkle leaf. | ||
| -- authorizationId becomes the authorization contract's stable MCMS instanceId, | ||
| -- allowing a later governed operation to cancel that specific authorization. | ||
| -- ContractIds (holdings, factory, mining-round context) are NOT included here — | ||
| -- they must be supplied at execution time via ExecuteFeeWithdrawal choice args. | ||
| data AuthorizeFeeWithdrawalParams = AuthorizeFeeWithdrawalParams | ||
| with | ||
| authorizationId : Text | ||
| recipient : Party | ||
| instrumentId : InstrumentId | ||
| maxAmount : Decimal | ||
| validitySecs : Int | ||
| deriving (Eq, Show) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
contracts/ccip/test/daml/CCIP/FeeTreasuryTest/FeeTreasuryCodecTest.daml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| module CCIP.FeeTreasuryTest.FeeTreasuryCodecTest where | ||
|
|
||
| import Daml.Script | ||
| import DA.Assert ((===)) | ||
| import DA.Optional (fromSome) | ||
|
|
||
| import Splice.Api.Token.HoldingV1 (InstrumentId(..)) | ||
|
|
||
| import CCIP.FeeTreasuryTypes (AuthorizeFeeWithdrawalParams(..)) | ||
| import CCIP.FeeTreasuryCodecGen ( | ||
| encodeAuthorizeFeeWithdrawalParams, | ||
| decodeAuthorizeFeeWithdrawalParamsAt, | ||
| ) | ||
|
|
||
| testAuthorizeFeeWithdrawalParamsRoundtrip : Script () | ||
| testAuthorizeFeeWithdrawalParamsRoundtrip = script do | ||
| recipient <- allocateParty "recipient" | ||
| admin <- allocateParty "admin" | ||
|
|
||
| let instrumentId = InstrumentId admin "FeeToken" | ||
| params = AuthorizeFeeWithdrawalParams with | ||
| authorizationId = "fee-withdrawal-codec-test" | ||
| recipient | ||
| instrumentId | ||
| maxAmount = 1000.0 | ||
| validitySecs = 3600 | ||
|
|
||
| encoded = encodeAuthorizeFeeWithdrawalParams params | ||
| decoded = fst (fromSome (decodeAuthorizeFeeWithdrawalParamsAt encoded 0)) | ||
|
|
||
| decoded === params | ||
|
|
||
| -- Trailing-bytes check: decoding at wrong offset returns None | ||
| decodeAuthorizeFeeWithdrawalParamsAt encoded 1 === None |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feeOwner==ccipOwner, right? In this case this would need an MCMSEntrypoint as well, in order to be able to call it via MCMSThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets either update this or add another choice that anyone can call after the authorization has expired. Kind of like a cleanup function callable by anyone to not leave expired authorizations hanging around.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
FeeWithdrawalAuthorizationnow has a signedauthorizationIdand implementsMCMSReceiver, so MCMS can target and cancel a specific authorization throughWithdrawAuthorizationCleanupExpiredAuthorization, which any disclosed submitter can exercise oncenow >= expiresAt