We use SnapshotMap a lot over at DAO DAO, and there's one aspect of it that has always bothered me. I think it leads to pretty confusing behavior and is likely to cause bugs, potentially security issues, if not deeply understood by the developers using it.
In my understanding, when a new value V_new is saved at height n, a snapshot is created at height n that contains the current value V_old in a field called old, and the value in the primary map is overridden with V_new. load and may_load read directly from the primary map, finding V_new, behaving as expected. But may_load_at_height actually returns V_old when called with the height at which V_new was saved.
Thus, may_load differs from may_load_at_height if used in the same block right after a value is stored. IMO, one would assume that calling save at the current height followed by may_load_at_height at the current height would retrieve the value just saved, but that is not the case.
The only justification I could see for this behavior is that SnapshotMap intends to be deterministic for the whole block, always returning the value that existed at the very beginning of the block. Thus, if another contract queries a contract that uses a SnapshotMap during a block where the map is being updated, it will always get the same value, regardless of the ordering of transactions in the block. Though I'm not totally convinced by this argument as one already cannot expect deterministic ordering when interacting with other contracts, and other state storage exists besides SnapshotMap. And I'm not sure if this benefit is worth the tradeoff of less intuitive security critical code.
Is there another reason it was designed this way? Please convince me this is necessary 😁
There are a few ways I think this could be fixed pretty easily, though I don't have full knowledge of this monorepo, and maybe these would break other things. Let me know what you think.
We use
SnapshotMap
a lot over at DAO DAO, and there's one aspect of it that has always bothered me. I think it leads to pretty confusing behavior and is likely to cause bugs, potentially security issues, if not deeply understood by the developers using it.In my understanding, when a new value
V_new
is saved at heightn
, a snapshot is created at heightn
that contains the current valueV_old
in a field calledold
, and the value in theprimary
map is overridden withV_new
.load
andmay_load
read directly from theprimary
map, findingV_new
, behaving as expected. Butmay_load_at_height
actually returnsV_old
when called with the height at whichV_new
was saved.Thus,
may_load
differs frommay_load_at_height
if used in the same block right after a value is stored. IMO, one would assume that callingsave
at the current height followed bymay_load_at_height
at the current height would retrieve the value just saved, but that is not the case.The only justification I could see for this behavior is that
SnapshotMap
intends to be deterministic for the whole block, always returning the value that existed at the very beginning of the block. Thus, if another contract queries a contract that uses aSnapshotMap
during a block where the map is being updated, it will always get the same value, regardless of the ordering of transactions in the block. Though I'm not totally convinced by this argument as one already cannot expect deterministic ordering when interacting with other contracts, and other state storage exists besidesSnapshotMap
. And I'm not sure if this benefit is worth the tradeoff of less intuitive security critical code.Is there another reason it was designed this way? Please convince me this is necessary 😁
There are a few ways I think this could be fixed pretty easily, though I don't have full knowledge of this monorepo, and maybe these would break other things. Let me know what you think.
write_change
https://github.com/CosmWasm/cw-storage-plus/blob/cac9687e29579c61eeacffafc131614c9f43baaa/src/snapshot/map.rs#L117-L126 useheight - 1
instead ofheight
, since that is the last block at which theold
value is accurate.may_load_at_height
https://github.com/CosmWasm/cw-storage-plus/blob/cac9687e29579c61eeacffafc131614c9f43baaa/src/snapshot/map.rs#L154-L170 useheight + 1
when callingself.snapshots.may_load_at_height
, since it will try to find the next change after the requested height, whoseold
value would be the correct value forheight
.Snapshot
https://github.com/CosmWasm/cw-storage-plus/blob/cac9687e29579c61eeacffafc131614c9f43baaa/src/snapshot/mod.rs#L151-L180 change the starting lower bound frominclusive
toexclusive
when looking in the changelog for theold
value.As far as I can tell, 2 and 3 above are identical, except 3 may fix the problem in other places if
Snapshot
is used the same way.