Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e5725b8
initial proof of concept
nullun Aug 19, 2024
d837ec2
afrz transactions are used to configure global freeze
nullun Aug 20, 2024
d6a6e25
minor text/key name changes
nullun Aug 21, 2024
436d2c2
Update goal and API responses
nullun Mar 28, 2025
849f2d4
Remove gfrz field from afrz transaction
nullun Apr 3, 2025
c4ea51a
Remove unused flag in 'goal asset freeze'
nullun Apr 3, 2025
1d1044d
Change GlobalFreeze codec from '0' to 'G'
nullun Apr 3, 2025
e0954a4
update "asset_holding_get" and "asset_params_get" opcodes
ori-shem-tov Apr 3, 2025
7232431
Merge pull request #2 from ori-shem-tov/feature/asa-global-freeze
nullun Apr 3, 2025
7d41877
Update and regen langspec and readme
nullun Apr 4, 2025
3e7d594
Fix 'asset_holding_get AssetFrozen' backwards compatibility, if asset…
nullun Apr 4, 2025
a4716ea
Allow asset holding to be unfrozen whilst asset params are globally f…
nullun Apr 8, 2025
f8db258
Updates following review
nullun Apr 11, 2025
4f19688
use existing vfuture consensus
nullun Apr 14, 2025
3af7cc0
use existing pattern for returning default values
nullun Apr 14, 2025
e11abe8
Rename function and provide context
nullun Apr 14, 2025
b82ceae
API: Expose frozen boolean and remove last freeze uint64
nullun Apr 15, 2025
d1bf5b1
merge latest master into feature branch
nullun Apr 15, 2025
7e6911a
Add wellFormed() for all asset transaction types
nullun Apr 15, 2025
20c3770
TEAL: Hide uint64 fields, expose boolean
nullun Apr 15, 2025
f121e8b
Update ledger field keys
nullun Apr 15, 2025
292a68f
Reverted opdoc to v11, removing all TEAL v12 output
nullun Apr 15, 2025
592554d
Added various tests
nullun May 13, 2025
d8f4bf5
Merge branch 'master' into feature/asa-global-freeze
nullun May 14, 2025
78593ec
regenerate algod api
nullun May 14, 2025
6cdd2b1
Additional test for unfreezing an individual
nullun May 14, 2025
b5c5d5d
Fix the gated empty FreezeAccount wellFormed check
nullun May 14, 2025
980bed3
added two new fields LastGlobalFreeze and LastFreezeChange
nullun May 19, 2025
09be117
Set new field values 1 in 5 times
nullun May 21, 2025
f0d8ea7
Merge branch 'master' into feature/asa-global-freeze
nullun May 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/goal/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,10 @@ func printAccountInfo(client libgoal.Client, address string, onlyShowAssetIDs bo
frozen := ""
if assetHolding.IsFrozen {
frozen = " (frozen)"
} else if assetParams.Params.LastGlobalFreeze != nil {
if *assetParams.Params.LastGlobalFreeze > assetHolding.LastFreezeChange {
frozen = " (globally frozen)"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies that we're not "lying" to REST APIs like we're doing to AVM code. We don't set assetHolding.IsFrozen when the asset is globally frozen? I think we probably should, I don't see a good reason to treat them differently.
We could add assetHolding.IsLocallyFrozen, just as from AVM.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API now returns booleans, abstracting away the uint64 txncounter values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I forgot to mention, when retrieving an account it only includes their local frozen state. I would have to lookup all asset params for every asset they hold to change their frozen state with respect to the LastGlobalFreeze.

}

fmt.Fprintf(report, "\tID %d, %s, balance %s %s%s\n", assetHolding.AssetID, assetName, amount, unitName, frozen)
Expand Down
8 changes: 7 additions & 1 deletion cmd/goal/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ func init() {
freezeAssetCmd.Flags().StringVar(&account, "account", "", "Account address to freeze/unfreeze")
freezeAssetCmd.Flags().BoolVar(&assetFrozen, "freeze", false, "Freeze or unfreeze")
freezeAssetCmd.MarkFlagRequired("freezer")
freezeAssetCmd.MarkFlagRequired("account")
freezeAssetCmd.MarkFlagRequired("freeze")

optinAssetCmd.Flags().StringVar(&assetUnitName, "asset", "", "Unit name of the asset being accepted")
Expand Down Expand Up @@ -776,6 +775,12 @@ var infoAssetCmd = &cobra.Command{
}
return *b
}
derefUint64 := func(u *uint64) uint64 {
if u == nil {
return 0
}
return *u
}

lookupAssetID(cmd, creator, client)

Expand Down Expand Up @@ -806,6 +811,7 @@ var infoAssetCmd = &cobra.Command{
fmt.Printf("Issued: %s %s\n", assetDecimalsFmt(asset.Params.Total-res.Amount, asset.Params.Decimals), derefString(asset.Params.UnitName))
fmt.Printf("Decimals: %d\n", asset.Params.Decimals)
fmt.Printf("Default frozen: %v\n", derefBool(asset.Params.DefaultFrozen))
fmt.Printf("Global freeze: %v\n", derefUint64(asset.Params.LastGlobalFreeze))
fmt.Printf("Manager address: %s\n", derefString(asset.Params.Manager))
if reserveEmpty {
fmt.Printf("Reserve address: %s (Empty. Defaulting to creator)\n", derefString(asset.Params.Reserve))
Expand Down
2 changes: 1 addition & 1 deletion cmd/opdoc/opdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ func create(file string) *os.File {
}

func main() {
const docVersion = uint64(11)
const docVersion = uint64(12)

opGroups := make(map[string][]string, len(logic.OpSpecs))
for grp, names := range logic.OpGroups {
Expand Down
18 changes: 17 additions & 1 deletion config/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,11 @@ type ConsensusParams struct {

// Heartbeat support
Heartbeat bool

// Allow for afrz transactions to target the zero address, freezing that
// asset for all holders. Subsequent unfreezing of an individuals holdings
// allows them to transfer the asset again.
AssetGlobalFreeze bool
}

// ProposerPayoutRules puts several related consensus parameters in one place. The same
Expand Down Expand Up @@ -1546,9 +1551,20 @@ func initConsensusProtocols() {
// our current max is 250000
v39.ApprovedUpgrades[protocol.ConsensusV40] = 208000

v41 := v40
v41.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{}

v41.AssetGlobalFreeze = true

Consensus[protocol.ConsensusV41] = v41

// v40 can be upgraded to v41, with an update delay of 7d.
// See previous for calculation.
// v40.ApprovedUpgrades[protocol.ConsensusV41] = 208000

// ConsensusFuture is used to test features that are implemented
// but not yet released in a production protocol version.
vFuture := v40
vFuture := v41
vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{}

vFuture.LogicSigVersion = 12 // When moving this to a release, put a new higher LogicSigVersion here
Expand Down
11 changes: 10 additions & 1 deletion daemon/algod/api/algod.oas2.json
Original file line number Diff line number Diff line change
Expand Up @@ -3309,7 +3309,8 @@
"required": [
"asset-id",
"amount",
"is-frozen"
"is-frozen",
"last-freeze-change"
],
"properties": {
"amount": {
Expand All @@ -3325,6 +3326,10 @@
"is-frozen": {
"description": "\\[f\\] whether or not the holding is frozen.",
"type": "boolean"
},
"last-freeze-change": {
"description": "\\[l\\] the last time the holding was frozen or unfrozen.",
"type": "integer"
}
}
},
Expand Down Expand Up @@ -3359,6 +3364,10 @@
"description": "\\[f\\] Address of account used to freeze holdings of this asset. If empty, freezing is not permitted.",
"type": "string"
},
"last-global-freeze": {
"description": "\\[l\\] When (as transaction counter) transfers of this asset were globally frozen. 0 if unfrozen.",
"type": "integer"
},
"manager": {
"description": "\\[m\\] Address of account used to manage the keys of this asset and to destroy it.",
"type": "string"
Expand Down
11 changes: 10 additions & 1 deletion daemon/algod/api/algod.oas3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1550,12 +1550,17 @@
"is-frozen": {
"description": "\\[f\\] whether or not the holding is frozen.",
"type": "boolean"
},
"last-freeze-change": {
"description": "\\[l\\] the last time the holding was frozen or unfrozen.",
"type": "integer"
}
},
"required": [
"amount",
"asset-id",
"is-frozen"
"is-frozen",
"last-freeze-change"
],
"type": "object"
},
Expand Down Expand Up @@ -1604,6 +1609,10 @@
"description": "\\[f\\] Address of account used to freeze holdings of this asset. If empty, freezing is not permitted.",
"type": "string"
},
"last-global-freeze": {
"description": "\\[l\\] When (as transaction counter) transfers of this asset were globally frozen. 0 if unfrozen.",
"type": "integer"
},
"manager": {
"description": "\\[m\\] Address of account used to manage the keys of this asset and to destroy it.",
"type": "string"
Expand Down
69 changes: 39 additions & 30 deletions daemon/algod/api/server/v2/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ import (
// AssetHolding converts between basics.AssetHolding and model.AssetHolding
func AssetHolding(ah basics.AssetHolding, ai basics.AssetIndex) model.AssetHolding {
return model.AssetHolding{
Amount: ah.Amount,
AssetID: uint64(ai),
IsFrozen: ah.Frozen,
Amount: ah.Amount,
AssetID: uint64(ai),
IsFrozen: ah.Frozen,
LastFreezeChange: ah.LastFreezeChange,
}
}

Expand Down Expand Up @@ -255,6 +256,10 @@ func AccountToAccountData(a *model.Account) (basics.AccountData, error) {
if ca.Params.DefaultFrozen != nil {
defaultFrozen = *ca.Params.DefaultFrozen
}
var lastGlobalFreeze uint64
if ca.Params.LastGlobalFreeze != nil {
lastGlobalFreeze = *ca.Params.LastGlobalFreeze
}
var url string
if ca.Params.Url != nil {
url = *ca.Params.Url
Expand All @@ -269,17 +274,18 @@ func AccountToAccountData(a *model.Account) (basics.AccountData, error) {
}

assetParams[basics.AssetIndex(ca.Index)] = basics.AssetParams{
Total: ca.Params.Total,
Decimals: uint32(ca.Params.Decimals),
DefaultFrozen: defaultFrozen,
UnitName: unitName,
AssetName: name,
URL: url,
MetadataHash: metadataHash,
Manager: manager,
Reserve: reserve,
Freeze: freeze,
Clawback: clawback,
Total: ca.Params.Total,
Decimals: uint32(ca.Params.Decimals),
DefaultFrozen: defaultFrozen,
LastGlobalFreeze: lastGlobalFreeze,
UnitName: unitName,
AssetName: name,
URL: url,
MetadataHash: metadataHash,
Manager: manager,
Reserve: reserve,
Freeze: freeze,
Clawback: clawback,
}
}
}
Expand All @@ -288,8 +294,9 @@ func AccountToAccountData(a *model.Account) (basics.AccountData, error) {
assets = make(map[basics.AssetIndex]basics.AssetHolding, len(*a.Assets))
for _, h := range *a.Assets {
assets[basics.AssetIndex(h.AssetID)] = basics.AssetHolding{
Amount: h.Amount,
Frozen: h.IsFrozen,
Amount: h.Amount,
Frozen: h.IsFrozen,
LastFreezeChange: h.LastFreezeChange,
}
}
}
Expand Down Expand Up @@ -483,21 +490,23 @@ func AppLocalState(state basics.AppLocalState, appIdx basics.AppIndex) model.App
// AssetParamsToAsset converts basics.AssetParams to model.Asset
func AssetParamsToAsset(creator string, idx basics.AssetIndex, params *basics.AssetParams) model.Asset {
frozen := params.DefaultFrozen
lastGlobalFreeze := params.LastGlobalFreeze
assetParams := model.AssetParams{
Creator: creator,
Total: params.Total,
Decimals: uint64(params.Decimals),
DefaultFrozen: &frozen,
Name: omitEmpty(printableUTF8OrEmpty(params.AssetName)),
NameB64: sliceOrNil([]byte(params.AssetName)),
UnitName: omitEmpty(printableUTF8OrEmpty(params.UnitName)),
UnitNameB64: sliceOrNil([]byte(params.UnitName)),
Url: omitEmpty(printableUTF8OrEmpty(params.URL)),
UrlB64: sliceOrNil([]byte(params.URL)),
Clawback: addrOrNil(params.Clawback),
Freeze: addrOrNil(params.Freeze),
Manager: addrOrNil(params.Manager),
Reserve: addrOrNil(params.Reserve),
Creator: creator,
Total: params.Total,
Decimals: uint64(params.Decimals),
DefaultFrozen: &frozen,
LastGlobalFreeze: &lastGlobalFreeze,
Copy link
Contributor

@jannotti jannotti Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will do it.

Suggested change
LastGlobalFreeze: &lastGlobalFreeze,
LastGlobalFreeze: omitEmpty(params.LastGlobalFreeze),

I think DefaultFrozen: omitEmpty(params.DefaultFrozen) would also be an improvement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, if you change DefaultFrozen: omitEmpty(params.DefaultFrozen) then the false is not omitted from the response. I don't understand why. It should be setting the value to nil when false, which I expect to get omitted. Anyway, don't do that.
But, assuming it has the behavior we want, it would still be nice to use omitEmpty on params.LastGlobalFreeze.

We'll need to extend rest-assets-endpoint.sh to have e2e tests of the new stuff either way.

Name: omitEmpty(printableUTF8OrEmpty(params.AssetName)),
NameB64: sliceOrNil([]byte(params.AssetName)),
UnitName: omitEmpty(printableUTF8OrEmpty(params.UnitName)),
UnitNameB64: sliceOrNil([]byte(params.UnitName)),
Url: omitEmpty(printableUTF8OrEmpty(params.URL)),
UrlB64: sliceOrNil([]byte(params.URL)),
Clawback: addrOrNil(params.Clawback),
Freeze: addrOrNil(params.Freeze),
Manager: addrOrNil(params.Manager),
Reserve: addrOrNil(params.Reserve),
}
if params.MetadataHash != ([32]byte{}) {
metadataHash := slices.Clone(params.MetadataHash[:])
Expand Down
Loading