diff --git a/contracts/contracts/ccip/ccipsend_executor/contract.tolk b/contracts/contracts/ccip/ccipsend_executor/contract.tolk index 9a1ef87fe..87ea05c1a 100644 --- a/contracts/contracts/ccip/ccipsend_executor/contract.tolk +++ b/contracts/contracts/ccip/ccipsend_executor/contract.tolk @@ -8,7 +8,7 @@ import "../onramp/types" import "../common/messages" import "../../lib/utils" -const CONTRACT_VERSION = "0.0.3"; +const CONTRACT_VERSION = "0.0.4"; const FACILITY_NAME = "com.chainlink.ton.ccip.CCIPSendExecutor"; fun onInternalMessage(in: InMessage) { @@ -160,6 +160,16 @@ get fun typeAndVersion(): (slice, slice) { return ("com.chainlink.ton.ccip.CCIPSendExecutor", CONTRACT_VERSION); } +// Returns the current code of the contract. +get fun code(): cell { + return contract.getCode(); +} + +// Returns the sha256 hash of the current code of the contract. +get fun codeHash(): int { + return contract.getCode().hash(); +} + get fun facilityId(): uint16 { return getFacilityId(stringCrc32("com.chainlink.ton.ccip.CCIPSendExecutor")); } diff --git a/contracts/contracts/ccip/ccipsend_executor/messages.tolk b/contracts/contracts/ccip/ccipsend_executor/messages.tolk index 795bf7271..4a1578a94 100644 --- a/contracts/contracts/ccip/ccipsend_executor/messages.tolk +++ b/contracts/contracts/ccip/ccipsend_executor/messages.tolk @@ -1,4 +1,6 @@ import "types" +import "storage" +import "../../lib/versioning/upgradeable" import "../common/messages" import "../onramp/messages" diff --git a/contracts/contracts/ccip/fee_quoter/contract.tolk b/contracts/contracts/ccip/fee_quoter/contract.tolk index efb64470f..b3ed6a84f 100644 --- a/contracts/contracts/ccip/fee_quoter/contract.tolk +++ b/contracts/contracts/ccip/fee_quoter/contract.tolk @@ -11,8 +11,9 @@ import "../../deployable/types"; import "../../lib/utils"; import "../router/messages" import "../ccipsend_executor/messages" +import "../../lib/versioning/upgradeable" -const CONTRACT_VERSION = "0.0.3"; +const CONTRACT_VERSION = "0.0.4"; const FACILITY_NAME = "com.chainlink.ton.ccip.FeeQuoter"; fun onInternalMessage(in: InMessage) { @@ -43,6 +44,14 @@ fun onInternalMessage(in: InMessage) { st.store(); } FeeQuoter_GetValidatedFee => { getValidatedFee(msg, in.senderAddress) } + Upgradeable_Upgrade => { + var st = lazy Storage.load(); + st.ownable.requireOwner(in.senderAddress); + Upgradeable{ + migrate: migrate, + version: version, + }.upgrade(msg); + } else => { // ignore empty messages, "wrong opcode" for others assert (in.body.isEmpty()) throw 0xFFFF @@ -523,3 +532,9 @@ get fun destChainSelectors(): tuple { var d = st.destChainConfigs; return keysLispList(d)!; } + +@method_id(1000) +fun migrate(storage: cell): cell { return beginCell().endCell(); } + +@method_id(1001) +fun version(): slice { return CONTRACT_VERSION; } \ No newline at end of file diff --git a/contracts/contracts/ccip/fee_quoter/messages.tolk b/contracts/contracts/ccip/fee_quoter/messages.tolk index d2ef9cc2c..33802b2a2 100644 --- a/contracts/contracts/ccip/fee_quoter/messages.tolk +++ b/contracts/contracts/ccip/fee_quoter/messages.tolk @@ -1,12 +1,14 @@ import "types" import "../router/messages" +import "../../lib/versioning/upgradeable" type FeeQuoter_InMessage = | FeeQuoter_UpdatePrices | FeeQuoter_UpdateFeeTokens | FeeQuoter_UpdateTokenTransferFeeConfigs | FeeQuoter_UpdateDestChainConfigs - | FeeQuoter_GetValidatedFee; // marked as cell since we never attempt to load the metadata + | FeeQuoter_GetValidatedFee // marked as cell since we never attempt to load the metadata + | Upgradeable_Upgrade; struct (0x20000001) FeeQuoter_UpdatePrices { updates: PriceUpdates diff --git a/contracts/contracts/ccip/offramp/contract.tolk b/contracts/contracts/ccip/offramp/contract.tolk index f5cc2090d..6a75aea95 100644 --- a/contracts/contracts/ccip/offramp/contract.tolk +++ b/contracts/contracts/ccip/offramp/contract.tolk @@ -4,6 +4,7 @@ import "types" import "storage" import "events" +import "../../lib/versioning/upgradeable" import "../common/types.tolk" import "../../deployable/types.tolk" import "../../lib/access/ownable_2step.tolk" @@ -16,7 +17,7 @@ import "../merkle_root/messages" import "../../lib/receiver/messages" import "../merkle_root/storage" -const CONTRACT_VERSION = "0.0.3"; +const CONTRACT_VERSION = "0.0.4"; fun onInternalMessage(in:InMessage) { val msg = lazy OffRamp_InMessage.fromSlice(in.body); @@ -45,6 +46,9 @@ fun onInternalMessage(in:InMessage) { OffRamp_NotifyFailure => { _notifyFailure(msg, in.senderAddress); } + Upgradeable_Upgrade => { + _upgrade(msg, in.senderAddress); + } else => { // ignore empty messages, "wrong opcode" for others assert (in.body.isEmpty()) throw 0xFFFF @@ -62,7 +66,22 @@ fun onBouncedMessage(in: InMessageBounced) { _bouncedCCIPReceive(msg.rootId, in.senderAddress); } } -} +} + +fun _upgrade(msg: Upgradeable_Upgrade, sender: address) { + val st = Storage.load(); + st.ownable.requireOwner(sender); + Upgradeable{ + migrate: migrate, + version: version, + }.upgrade(msg); +} + +@method_id(1000) +fun migrate(storage: cell): cell { return beginCell().endCell(); } + +@method_id(1001) +fun version(): slice { return CONTRACT_VERSION; } fun _bouncedCCIPReceive(rootId: uint224, sender: address) { val st = Storage.load(); diff --git a/contracts/contracts/ccip/offramp/messages.tolk b/contracts/contracts/ccip/offramp/messages.tolk index 2c180a888..0d3e24f2d 100644 --- a/contracts/contracts/ccip/offramp/messages.tolk +++ b/contracts/contracts/ccip/offramp/messages.tolk @@ -3,6 +3,7 @@ import "../../lib/ocr/types.tolk" import "../../lib/ocr/multi_ocr3_base.tolk" import "../../lib/receiver/messages.tolk" import "../common/types.tolk" +import "../../lib/versioning/upgradeable" type OffRamp_InMessage = | OffRamp_Commit @@ -12,7 +13,8 @@ type OffRamp_InMessage = | OCR3Base_SetOCR3Config | OffRamp_CCIPReceiveConfirm | OffRamp_NotifyFailure - | OffRamp_NotifySuccess; + | OffRamp_NotifySuccess + | Upgradeable_Upgrade; type OffRamp_BouncedMessage = Receiver_CCIPReceive; diff --git a/contracts/contracts/ccip/onramp/contract.tolk b/contracts/contracts/ccip/onramp/contract.tolk index 5a6b7b016..896d8f9d3 100644 --- a/contracts/contracts/ccip/onramp/contract.tolk +++ b/contracts/contracts/ccip/onramp/contract.tolk @@ -17,8 +17,9 @@ import "../ccipsend_executor/messages" import "../router/messages" import "../../lib/jetton/messages" import "../../lib/jetton/messages_extended" +import "../../lib/versioning/upgradeable" -const CONTRACT_VERSION = "0.0.3"; +const CONTRACT_VERSION = "0.0.4"; fun onInternalMessage(in: InMessage) { val msg = lazy OnRamp_InMessage.fromSlice(in.body); @@ -47,6 +48,14 @@ fun onInternalMessage(in: InMessage) { applyAllowlistUpdates(mutate st, msg.updates); st.store(); } + Upgradeable_Upgrade => { + var st = lazy OnRamp_Storage.load(); + st.ownable.requireOwner(in.senderAddress); + Upgradeable{ + migrate: migrate, + version: version, + }.upgrade(msg); + } else => { // ignore empty messages, "wrong opcode" for others assert (in.body.isEmpty()) throw 0xFFFF @@ -345,3 +354,9 @@ get fun code(): cell { get fun codeHash(): int { return contract.getCode().hash(); } + +@method_id(1000) +fun migrate(storage: cell): cell { return beginCell().endCell(); } + +@method_id(1001) +fun version(): slice { return CONTRACT_VERSION; } \ No newline at end of file diff --git a/contracts/contracts/ccip/onramp/messages.tolk b/contracts/contracts/ccip/onramp/messages.tolk index 1e696bd68..9f70a2e4a 100644 --- a/contracts/contracts/ccip/onramp/messages.tolk +++ b/contracts/contracts/ccip/onramp/messages.tolk @@ -1,6 +1,7 @@ import "types" import "../common/messages" import "../router/messages" +import "../../lib/versioning/upgradeable" type OnRamp_InMessage = | OnRamp_Send @@ -10,6 +11,7 @@ type OnRamp_InMessage = | OnRamp_UpdateDestChainConfigs | OnRamp_UpdateAllowlists | Common_JettonTransferNotification + | Upgradeable_Upgrade ; // TODO | UpdateExecutorCode diff --git a/contracts/contracts/ccip/router/contract.tolk b/contracts/contracts/ccip/router/contract.tolk index 48fd3116d..57937457f 100644 --- a/contracts/contracts/ccip/router/contract.tolk +++ b/contracts/contracts/ccip/router/contract.tolk @@ -1,18 +1,19 @@ import "messages" import "storage" import "errors" - import "../onramp/messages" import "../onramp/types" import "../common/types" import "../common/messages" -import "../../lib/access/ownable_2step" import "../../deployable/types" +import "../../lib/access/ownable_2step" import "../../lib/utils" import "../../lib/jetton/messages" import "../../lib/jetton/messages_extended" +import "../../lib/versioning/upgradeable" -const CONTRACT_VERSION = "0.0.2"; + +const CONTRACT_VERSION = "0.0.4"; fun onInternalMessage(in: InMessage) { val msg = lazy Msg.fromSlice(in.body); @@ -20,6 +21,7 @@ fun onInternalMessage(in: InMessage) { Router_SetRamps => {onSetRamps(msg, in.senderAddress)} Router_CCIPSend => { onSend(msg, in.senderAddress) } Common_JettonTransferNotification => { onTransferNotification(msg, in.senderAddress) } + Upgradeable_Upgrade => { onUpgrade(msg, in.senderAddress) } else => { // ignore empty messages, "wrong opcode" for others assert (in.body.isEmpty()) throw 0xFFFF @@ -108,6 +110,21 @@ fun ccipSend(msg: Router_CCIPSend, sender: address) { sendMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); } +fun onUpgrade(msg: Upgradeable_Upgrade, sender: address) { + var st = lazy Storage.load(); + st.ownable.requireOwner(sender); + Upgradeable{ + migrate: migrate, + version: version, + }.upgrade(msg); +} + +@method_id(1000) +fun migrate(storage: cell): cell { return beginCell().endCell(); } + +@method_id(1001) +fun version(): slice { return CONTRACT_VERSION; } + get fun typeAndVersion(): (slice, slice) { return ("com.chainlink.ton.ccip.Router", CONTRACT_VERSION); } diff --git a/contracts/contracts/ccip/router/messages.tolk b/contracts/contracts/ccip/router/messages.tolk index b0c92f31d..1ca254a2f 100644 --- a/contracts/contracts/ccip/router/messages.tolk +++ b/contracts/contracts/ccip/router/messages.tolk @@ -1,7 +1,8 @@ import "../common/types.tolk" import "../common/messages" +import "../../lib/versioning/upgradeable" -type Msg = Router_CCIPSend | Router_SetRamps | Common_JettonTransferNotification; +type Msg = Router_CCIPSend | Router_SetRamps | Common_JettonTransferNotification | Upgradeable_Upgrade; // update multiple chain selectors in batch to the same onramp address struct (0x10000001) Router_SetRamps { diff --git a/contracts/contracts/examples/counter.tolk b/contracts/contracts/examples/counter.tolk index 8115be71b..415cc18fa 100644 --- a/contracts/contracts/examples/counter.tolk +++ b/contracts/contracts/examples/counter.tolk @@ -1,7 +1,6 @@ tolk 1.1 import "@stdlib/common.tolk" -import "../lib/upgrades/type_and_version.tolk" import "../lib/utils.tolk" import "../lib/access/ownable_2step.tolk" @@ -157,6 +156,15 @@ get fun value(): int { /// Gets the current type and version of the contract. get fun typeAndVersion(): (slice, slice) { - return TypeAndVersion { typeStr: "com.chainlink.ton.examples.Counter", versionStr: CONTRACT_VERSION } - .typeAndVersion(); + return ("com.chainlink.ton.examples.Counter", CONTRACT_VERSION) +} + +// Returns the current code of the contract. +get fun code(): cell { + return contract.getCode(); +} + +// Returns the sha256 hash of the current code of the contract. +get fun codeHash(): int { + return contract.getCode().hash(); } diff --git a/contracts/contracts/lib/upgrades/type_and_version.tolk b/contracts/contracts/lib/upgrades/type_and_version.tolk deleted file mode 100644 index 84905a5d4..000000000 --- a/contracts/contracts/lib/upgrades/type_and_version.tolk +++ /dev/null @@ -1,41 +0,0 @@ -/// Implements getters for contract type and version, current code and code hash. - -/// The implementor is expected to implement a `typeAndVersion(): (slice, slice)` getter, wrapping a call to `TypeAndVersion.typeAndVersion;` -/// -/// Example: -/// -/// ```tolk -/// get typeAndVersion(): (slice, slice) { -/// return TypeAndVersion { -/// typeStr: "com.chainlink.ton.examples.Counter", -/// versionStr: "1.0.0", -/// }.typeAndVersion(); -/// } -/// ``` -/// -/// `typeStr` must be a Reverse Domain Name Notation string that is unique to the contract and should not change between versions. -/// Example: "com.chainlink.project.package.ContractName" -/// Read more about Reverse DNS Notation at https://en.wikipedia.org/wiki/Reverse_domain_name_notation -/// -/// `versionStr` must be a semantic version string (e.g. "1.0.0"). -struct TypeAndVersion { - typeStr: slice, - versionStr: slice, -} - -/// A getter returning the current type and version of the contract. -fun TypeAndVersion.typeAndVersion(self): (slice, slice) { - return (self.typeStr, self.versionStr); -} - -// Returns the current code of the contract. -get fun code(): cell { - return contract.getCode(); -} - -// Returns the sha256 hash of the current code of the contract. -get fun codeHash(): int { - return contract.getCode().hash(); -} - - diff --git a/contracts/contracts/lib/upgrades/upgradeable.tolk b/contracts/contracts/lib/versioning/upgradeable.tolk similarity index 66% rename from contracts/contracts/lib/upgrades/upgradeable.tolk rename to contracts/contracts/lib/versioning/upgradeable.tolk index a3393973a..1f857332d 100644 --- a/contracts/contracts/lib/upgrades/upgradeable.tolk +++ b/contracts/contracts/lib/versioning/upgradeable.tolk @@ -77,19 +77,41 @@ import "../utils.tolk"; /// } /// ``` struct Upgradeable { - /// Abstract methods that must be implemented by the contract. + /// Points to a function that migrates the storage before the upgrade. + /// It receives the storage cell from the old version and returns the + /// migrated storage cell for the new version. + /// + /// At TVM level, this only stores the method_id of the function to call, + /// so it automatically points to the new implementation after the code + /// is swapped. + /// + /// This function must specify a method_id explicitly in the contract, + /// and it must be consistent across versions. migrate: (cell) -> cell; + /// Points to a function that returns the version of the contract. + /// + /// At TVM level, this only stores the method_id of the function to call, + /// so it automatically points to the new implementation after the code + /// is swapped. + /// + /// This function must specify a method_id explicitly in the contract, + /// and it must be consistent across versions. version: () -> slice; } +enum Upgradeable_Error { + VersionMismatch = 28700; // Facility ID * 100 +} /// Message for upgrading a contract. -struct (0x0aa811ed) Upgradeable_Upgrade { +/// crc32("Upgradeable_Upgrade") == 0x6cf83c03 +struct (0x6cf83c03) Upgradeable_Upgrade { queryId: uint64; code: cell; + fromVersion: RemainingBitsAndRefs; } -struct UpgradedEvent { +struct Upgradeable_UpgradedEvent { /// The new code of the contract. code: cell; /// The SHA256 hash of the new code. @@ -98,21 +120,24 @@ struct UpgradedEvent { version: UnsafeBodyNoRef; } -const UPGRADED_EVENT = 0xa33b498e; // Event topic for the upgrade event +const UPGRADEABLE_UPGRADED_EVENT = 0xa33b498e; // crc32("Upgradeable_UpgradedEvent") -fun Upgradeable.upgrade(self, code: cell) { +fun Upgradeable.upgrade(self, msg: Upgradeable_Upgrade) { + // Ensure the upgrade requirements are met + val currentVersion = self.version(); + assert (currentVersion.bitsEqual(msg.fromVersion)) throw Upgradeable_Error.VersionMismatch; // Schedule the code to be changed after the compute phase - contract.setCodePostponed(code); - // Load the code dinamically - var cont = transformSliceToContinuation(code.beginParse()); + contract.setCodePostponed(msg.code); + // Load the code dynamically + var cont = transformSliceToContinuation(msg.code.beginParse()); setTvmRegisterC3(cont); // After this line, we are running the new code // Call the migrate function implemented by the new version var c = contract.getData(); var newStorage = self.migrate(c); contract.setData(newStorage); - emit(UPGRADED_EVENT, UpgradedEvent{ - code: code, - hash: code.hash(), + emit(UPGRADEABLE_UPGRADED_EVENT, Upgradeable_UpgradedEvent{ + code: msg.code, + hash: msg.code.hash(), version: UnsafeBodyNoRef { forceInline: self.version(), // This is expected to be the version of the new code }, diff --git a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v1/contract.tolk b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v1/contract.tolk similarity index 62% rename from contracts/contracts/test/examples/upgrades/upgradeable_counter/v1/contract.tolk rename to contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v1/contract.tolk index 0d255b47f..554b3e46a 100644 --- a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v1/contract.tolk +++ b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v1/contract.tolk @@ -1,7 +1,6 @@ -import "../../../../../lib/utils.tolk"; -import "../../../../../lib/upgrades/type_and_version.tolk"; -import "../../../../../lib/upgrades/upgradeable.tolk"; -import "../../../../../lib/access/ownable_2step.tolk"; +import "../../../../../../lib/utils.tolk"; +import "../../../../../../lib/versioning/upgradeable.tolk"; +import "../../../../../../lib/access/ownable_2step.tolk"; import "./storage.tolk"; type IncomingMessage = Upgradeable_Upgrade | Step; @@ -14,8 +13,8 @@ fun saveData(data: StorageV1) { contract.setData(data.toCell()); } -const typeStr= "com.chainlink.ton.examples.upgrades.UpgradeableCounter"; -const versionStr = "1.0.0"; +const FACILITY_NAME= "com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter"; +const CONTRACT_VERSION = "1.0.0"; struct (0x00000001) Step { queryId: uint64; @@ -43,7 +42,7 @@ fun onInternalMessage(in: InMessage) { Upgradeable{ migrate: migrate, version: version, - }.upgrade(msg.code); + }.upgrade(msg); } Step => { /// Instructs the contract to step the counter. @@ -74,8 +73,26 @@ get fun pendingOwner(): address? { @method_id(1000) fun migrate(storage: cell): cell { return beginCell().endCell(); } @method_id(1001) -fun version(): slice { return versionStr; } +fun version(): slice { return CONTRACT_VERSION; } get fun typeAndVersion(): (slice, slice) { - return TypeAndVersion{typeStr, versionStr}.typeAndVersion(); + return ("com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter", CONTRACT_VERSION); } + +// Returns the current code of the contract. +get fun code(): cell { + return contract.getCode(); +} + +// Returns the sha256 hash of the current code of the contract. +get fun codeHash(): int { + return contract.getCode().hash(); +} + +get fun facilityId(): uint16 { + return getFacilityId(stringCrc32("com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter")); +} + +get fun errorCode(local: uint16): uint16 { + return getErrorCode(stringCrc32("com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter"), local); +} \ No newline at end of file diff --git a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v1/storage.tolk b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v1/storage.tolk similarity index 60% rename from contracts/contracts/test/examples/upgrades/upgradeable_counter/v1/storage.tolk rename to contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v1/storage.tolk index c76ae5809..3e87390ee 100644 --- a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v1/storage.tolk +++ b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v1/storage.tolk @@ -1,4 +1,4 @@ -import "../../../../../lib/access/ownable_2step.tolk"; +import "../../../../../../lib/access/ownable_2step.tolk"; struct StorageV1 { id: uint32; diff --git a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v2/contract.tolk b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v2/contract.tolk similarity index 66% rename from contracts/contracts/test/examples/upgrades/upgradeable_counter/v2/contract.tolk rename to contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v2/contract.tolk index 9bca455db..536df241e 100644 --- a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v2/contract.tolk +++ b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v2/contract.tolk @@ -1,7 +1,6 @@ -import "../../../../../lib/utils.tolk"; -import "../../../../../lib/upgrades/type_and_version.tolk"; -import "../../../../../lib/upgrades/upgradeable.tolk"; -import "../../../../../lib/access/ownable_2step.tolk"; +import "../../../../../../lib/utils.tolk"; +import "../../../../../../lib/versioning/upgradeable.tolk"; +import "../../../../../../lib/access/ownable_2step.tolk"; import "./storage.tolk"; import "../v1/storage.tolk"; @@ -15,8 +14,8 @@ fun saveData(data: StorageV2) { contract.setData(data.toCell()); } -const typeStr= "com.chainlink.ton.examples.upgrades.UpgradeableCounter"; -const versionStr = "2.0.0"; +const FACILITY_NAME= "com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter"; +const CONTRACT_VERSION = "2.0.0"; struct (0x00000001) Step { queryId: uint64; @@ -44,7 +43,7 @@ fun onInternalMessage(in: InMessage) { Upgradeable{ migrate: migrate, version: version, - }.upgrade(msg.code); + }.upgrade(msg); } Step => { /// Instructs the contract to step the counter. @@ -85,8 +84,27 @@ fun migrate(storage: cell): cell { return newStorage.toCell(); } @method_id(1001) -fun version(): slice { return versionStr; } +fun version(): slice { return CONTRACT_VERSION; } + get fun typeAndVersion(): (slice, slice) { - return TypeAndVersion{typeStr, versionStr}.typeAndVersion(); + return ("com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter", CONTRACT_VERSION); +} + +// Returns the current code of the contract. +get fun code(): cell { + return contract.getCode(); +} + +// Returns the sha256 hash of the current code of the contract. +get fun codeHash(): int { + return contract.getCode().hash(); +} + +get fun facilityId(): uint16 { + return getFacilityId(stringCrc32("com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter")); +} + +get fun errorCode(local: uint16): uint16 { + return getErrorCode(stringCrc32("com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter"), local); } diff --git a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v2/storage.tolk b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v2/storage.tolk similarity index 60% rename from contracts/contracts/test/examples/upgrades/upgradeable_counter/v2/storage.tolk rename to contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v2/storage.tolk index 848749fe2..767322fc4 100644 --- a/contracts/contracts/test/examples/upgrades/upgradeable_counter/v2/storage.tolk +++ b/contracts/contracts/test/examples/versioning/upgrades/upgradeable_counter/v2/storage.tolk @@ -1,4 +1,4 @@ -import "../../../../../lib/access/ownable_2step.tolk"; +import "../../../../../../lib/access/ownable_2step.tolk"; struct StorageV2 { value: uint64; diff --git a/contracts/tests/ccip/CCIPRouter.spec.ts b/contracts/tests/ccip/CCIPRouter.spec.ts index 3852558bb..435f29e09 100644 --- a/contracts/tests/ccip/CCIPRouter.spec.ts +++ b/contracts/tests/ccip/CCIPRouter.spec.ts @@ -19,6 +19,8 @@ import { dump } from '../utils/prettyPrint' import { CellCodec, facilityId } from '../../wrappers/utils' import { crc32 } from 'zlib' import { CCIP_SEND_EXECUTOR_FACILITY_ID } from '../../wrappers/ccip/OnRamp' +import { OnRamp, OnRampStorage } from '../../wrappers/ccip/OnRamp' +import * as UpgradeableSpec from '../lib/versioning/UpgradeableSpec' const CHAINSEL_EVM_TEST_90000001 = 909606746561742123n const CHAINSEL_EVM_TEST_90000002 = 5548718428018410741n @@ -37,6 +39,63 @@ const EVM_ADDRESS = Buffer.from( 'hex', ) // 32 bytes +// TODO when we have a new version +// describe('Router - Upgrade Tests', () => { +// const upgradeSpec = UpgradeableSpec.newUpgradeSpec( +// { +// contractType: RouterPrev.type(), +// prevVersion: RouterPrev.version(), +// currentVersion: Router.version(), +// getPrevCode: () => RouterPrev.code(), +// getCurrentCode: () => Router.code(), +// CurrentVersionConstructor: Router, +// }, +// async (blockchain, owner) => { +// const codeV1 = await RouterPrev.code() +// const data = {} as any // TODO fill with valid data +// const contract = blockchain.openContract( +// RouterPrev.createFromConfig( +// data, +// codeV1, +// ), +// ) +// const deployer = await blockchain.treasury('deployer') +// await contract.sendDeploy(deployer.getSender(), toNano('0.05')) +// return contract +// }, +// ) +// upgradeSpec.run() +// }) + +describe('Router - Current Version Tests', () => { + const currentVersionSpec = UpgradeableSpec.newCurrentVersionSpec( + { + contractType: rt.Router.type(), + currentVersion: rt.Router.version(), + getCurrentCode: () => rt.Router.code(), + CurrentVersionConstructor: rt.Router, + }, + async (blockchain, owner) => { + const code = await rt.Router.code() + let data: rt.Storage = { + id: 0, + ownable: { + owner: owner.address, + pendingOwner: null, + }, + onRamps: Dictionary.empty(Dictionary.Keys.BigUint(64), Dictionary.Values.Address()), + } + + // TODO: use deployable to make deterministic? + const contract = blockchain.openContract(rt.Router.createFromConfig(data, code)) + const deployer = await blockchain.treasury('deployer') + await contract.sendInternal(deployer.getSender(), toNano('1'), Cell.EMPTY) + return contract + }, + ) + currentVersionSpec.run() +}) + describe('Router', () => { let blockchain: Blockchain let deployer: SandboxContract diff --git a/contracts/tests/ccip/OffRamp.spec.ts b/contracts/tests/ccip/OffRamp.spec.ts index 8c5f3ad73..e3e5cd6ab 100644 --- a/contracts/tests/ccip/OffRamp.spec.ts +++ b/contracts/tests/ccip/OffRamp.spec.ts @@ -27,6 +27,7 @@ import { generateEd25519KeyPair, generateMockTonAddress, uint8ArrayToBigInt, + ZERO_ADDRESS, } from '../../src/utils' import { KeyPair, sha256_sync } from '@ton/crypto' @@ -45,6 +46,7 @@ import { ReportContext, SignatureEd25519 } from '../../wrappers/libraries/ocr/Mu import { Receiver } from '../../wrappers/ccip/Receiver' import { crc32 } from 'zlib' import { facilityId } from '../../wrappers/utils' +import * as UpgradeableSpec from '../lib/versioning/UpgradeableSpec' const CHAINSEL_EVM_TEST_90000001 = 909606746561742123n const CHAINSEL_TON = 13879075125137744094n @@ -124,7 +126,68 @@ export function generateMessageId(message: Any2TVMRampMessage, metadataHash: big ) } -describe('OffRamp', () => { +// TODO when we have a new version +// describe('UpgradeableCounter - Upgrade Tests', () => { +// const upgradeSpec = UpgradeableSpec.newUpgradeSpec( +// { +// contractType: OffRampPrev.type(), +// prevVersion: OffRampPrev.version(), +// currentVersion: OffRamp.version(), +// getPrevCode: () => OffRampPrev.code(), +// getCurrentCode: () => OffRamp.code(), +// CurrentVersionConstructor: OffRamp, +// }, +// async (blockchain, owner) => { +// const codeV1 = await OffRampPrev.code() +// const data = {} as any // TODO fill with valid data +// const contract = blockchain.openContract( +// OffRampPrev.createFromConfig( +// data, +// codeV1, +// ), +// ) +// const deployer = await blockchain.treasury('deployer') +// await contract.sendDeploy(deployer.getSender(), toNano('0.05')) +// return contract +// }, +// ) +// upgradeSpec.run() +// }) + +describe('UpgradeableCounter - Current Version Tests', () => { + const currentVersionSpec = UpgradeableSpec.newCurrentVersionSpec( + { + contractType: OffRamp.type(), + currentVersion: OffRamp.version(), + getCurrentCode: () => OffRamp.code(), + CurrentVersionConstructor: OffRamp, + }, + async (blockchain, owner) => { + const code = await OffRamp.code() + let data: OffRampStorage = { + id: generateSecureRandomId(), + ownable: { + owner: owner.address, + pendingOwner: null, + }, + deployerCode: beginCell().endCell(), + merkleRootCode: beginCell().endCell(), + feeQuoter: ZERO_ADDRESS, + chainSelector: CHAINSEL_TON, + permissionlessExecutionThresholdSeconds: 60, + latestPriceSequenceNumber: 0n, + } + + const contract = blockchain.openContract(OffRamp.createFromConfig(data, code)) + const deployer = await blockchain.treasury('deployer') + await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + return contract + }, + ) + currentVersionSpec.run() +}) + +describe('OffRamp - Unit Tests', () => { let blockchain: Blockchain let deployer: SandboxContract let offRamp: SandboxContract diff --git a/contracts/tests/ccip/feequoter/FeeQuoter.spec.ts b/contracts/tests/ccip/feequoter/FeeQuoter.spec.ts new file mode 100644 index 000000000..8617aba5f --- /dev/null +++ b/contracts/tests/ccip/feequoter/FeeQuoter.spec.ts @@ -0,0 +1,46 @@ +import { FeeQuoter } from '../../../wrappers/ccip/FeeQuoter' +import * as UpgradeableSpec from '../../lib/versioning/UpgradeableSpec' +import { setupTestFeeQuoter } from '../helpers/SetUp' + +const CHAINSEL_TON = 13879075125137744094n // TODO this is copy/pasted from CCIPRouter.spec.ts. Isn't there a chainlink package that exports this constant? + +// TODO when we have a new version +// describe('FeeQuoter - Upgrade Tests', () => { +// const upgradeSpec = UpgradeableSpec.newUpgradeSpec( +// { +// contractType: FeeQuoterPrev.type(), +// prevVersion: FeeQuoterPrev.version(), +// currentVersion: FeeQuoter.version(), +// getPrevCode: () => FeeQuoterPrev.code(), +// getCurrentCode: () => FeeQuoter.code(), +// CurrentVersionConstructor: FeeQuoter, +// }, +// async (blockchain, owner) => { +// const codeV1 = await FeeQuoterPrev.code() +// const data = {} as any // TODO fill with valid data +// const contract = blockchain.openContract( +// FeeQuoterPrev.createFromConfig( +// data, +// codeV1, +// ), +// ) +// const deployer = await blockchain.treasury('deployer') +// await contract.sendDeploy(deployer.getSender(), toNano('0.05')) +// return contract +// }, +// ) +// upgradeSpec.run() +// }) + +describe('FeeQuoter - Current Version Tests', () => { + const currentVersionSpec = UpgradeableSpec.newCurrentVersionSpec( + { + contractType: FeeQuoter.type(), + currentVersion: FeeQuoter.version(), + getCurrentCode: () => FeeQuoter.code(), + CurrentVersionConstructor: FeeQuoter, + }, + async (blockchain, owner) => setupTestFeeQuoter(owner, blockchain), + ) + currentVersionSpec.run() +}) diff --git a/contracts/tests/ccip/onramp/OnRamp.spec.ts b/contracts/tests/ccip/onramp/OnRamp.spec.ts new file mode 100644 index 000000000..5339fae8a --- /dev/null +++ b/contracts/tests/ccip/onramp/OnRamp.spec.ts @@ -0,0 +1,70 @@ +import { beginCell, Dictionary, toNano } from '@ton/core' +import { ZERO_ADDRESS } from '../../../src/utils' +import { OnRamp, OnRampStorage } from '../../../wrappers/ccip/OnRamp' +import * as UpgradeableSpec from '../../lib/versioning/UpgradeableSpec' + +const CHAINSEL_TON = 13879075125137744094n // TODO this is copy/pasted from CCIPRouter.spec.ts. Isn't there a chainlink package that exports this constant? + +// TODO when we have a new version +// describe('OnRamp - Upgrade Tests', () => { +// const upgradeSpec = UpgradeableSpec.newUpgradeSpec( +// { +// contractType: OnRampPrev.type(), +// prevVersion: OnRampPrev.version(), +// currentVersion: OnRamp.version(), +// getPrevCode: () => OnRampPrev.code(), +// getCurrentCode: () => OnRamp.code(), +// CurrentVersionConstructor: OnRamp, +// }, +// async (blockchain, owner) => { +// const codeV1 = await OnRampPrev.code() +// const data = {} as any // TODO fill with valid data +// const contract = blockchain.openContract( +// OnRampPrev.createFromConfig( +// data, +// codeV1, +// ), +// ) +// const deployer = await blockchain.treasury('deployer') +// await contract.sendDeploy(deployer.getSender(), toNano('0.05')) +// return contract +// }, +// ) +// upgradeSpec.run() +// }) + +describe('OnRamp - Current Version Tests', () => { + const currentVersionSpec = UpgradeableSpec.newCurrentVersionSpec( + { + contractType: OnRamp.type(), + currentVersion: OnRamp.version(), + getCurrentCode: () => OnRamp.code(), + CurrentVersionConstructor: OnRamp, + }, + async (blockchain, owner) => { + const code = await OnRamp.code() + let data: OnRampStorage = { + id: 0, + ownable: { + owner: owner.address, + pendingOwner: null, + }, + chainSelector: CHAINSEL_TON, + config: { + feeQuoter: ZERO_ADDRESS, + feeAggregator: ZERO_ADDRESS, + allowlistAdmin: ZERO_ADDRESS, + }, + destChainConfigs: Dictionary.empty(Dictionary.Keys.BigUint(64), Dictionary.Values.Cell()), + currentMessageId: 0n, + executor_code: beginCell().endCell(), + } + // TODO: use deployable to make deterministic? + const contract = blockchain.openContract(OnRamp.createFromConfig(data, code)) + const deployer = await blockchain.treasury('deployer') + await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + return contract + }, + ) + currentVersionSpec.run() +}) diff --git a/contracts/tests/examples/upgrades/UpgradeableCounter.spec.ts b/contracts/tests/examples/versioning/UpgradeableCounter.spec.ts similarity index 58% rename from contracts/tests/examples/upgrades/UpgradeableCounter.spec.ts rename to contracts/tests/examples/versioning/UpgradeableCounter.spec.ts index e90abb0fb..632c029d0 100644 --- a/contracts/tests/examples/upgrades/UpgradeableCounter.spec.ts +++ b/contracts/tests/examples/versioning/UpgradeableCounter.spec.ts @@ -1,12 +1,10 @@ import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox' -import { Cell, Message, toNano } from '@ton/core' +import { Cell, toNano } from '@ton/core' import '@ton/test-utils' -import { UpgradeableCounterV1 } from '../../../wrappers/examples/upgrades/UpgradeableCounterV1' -import { UpgradeableCounterV2 } from '../../../wrappers/examples/upgrades/UpgradeableCounterV2' -import { - loadUpgradedEvent, - sendUpgradeAndReturnNewVersion, -} from '../../../wrappers/libraries/upgrades/Upgradeable' +import { UpgradeableCounterV1 } from '../../../wrappers/examples/versioning/UpgradeableCounterV1' +import { UpgradeableCounterV2 } from '../../../wrappers/examples/versioning/UpgradeableCounterV2' +import { sendUpgradeAndReturnNewVersion } from '../../../wrappers/libraries/versioning/Upgradeable' +import { newUpgradeSpec, newCurrentVersionSpec } from '../../lib/versioning/UpgradeableSpec' async function setUpTest(i: number): Promise<{ blockchain: Blockchain @@ -63,27 +61,73 @@ async function setUpTest(i: number): Promise<{ } } -describe('UpgradeableCounter', () => { +describe('UpgradeableCounter - Upgrade Tests', () => { + const upgradeSpec = newUpgradeSpec( + { + contractType: UpgradeableCounterV1.type(), + prevVersion: UpgradeableCounterV1.version(), + currentVersion: UpgradeableCounterV2.version(), + getPrevCode: () => UpgradeableCounterV1.code(), + getCurrentCode: () => UpgradeableCounterV2.code(), + CurrentVersionConstructor: UpgradeableCounterV2, + }, + async (blockchain, owner) => { + const codeV1 = await UpgradeableCounterV1.code() + const contract = blockchain.openContract( + UpgradeableCounterV1.createFromConfig( + { + id: 0, + value: 0, + ownable: { owner: owner.address, pendingOwner: null }, + }, + codeV1, + ), + ) + const deployer = await blockchain.treasury('deployer') + await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + return contract + }, + ) + upgradeSpec.run() +}) + +describe('UpgradeableCounter - Current Version Tests', () => { + const currentVersionSpec = newCurrentVersionSpec( + { + contractType: UpgradeableCounterV2.type(), + currentVersion: UpgradeableCounterV2.version(), + getCurrentCode: () => UpgradeableCounterV2.code(), + CurrentVersionConstructor: UpgradeableCounterV2, + }, + async (blockchain, owner) => { + const code = await UpgradeableCounterV2.code() + const contract = blockchain.openContract( + UpgradeableCounterV2.createFromConfig( + { + id: 0, + value: 0, + ownable: { owner: owner.address, pendingOwner: null }, + }, + code, + ), + ) + const deployer = await blockchain.treasury('deployer') + await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + return contract + }, + ) + currentVersionSpec.run() +}) + +describe('UpgradeableCounter - Unit Tests', () => { it('should deploy', async () => { await setUpTest(0) }) - it('should deploy on version 1', async () => { - let { upgradeableCounter, codeV1 } = await setUpTest(0) - const typeAndVersion = await upgradeableCounter.getTypeAndVersion() - expect(typeAndVersion.type).toBe('com.chainlink.ton.examples.upgrades.UpgradeableCounter') - expect(typeAndVersion.version).toBe('1.0.0') - const currentCode = await upgradeableCounter.getCode() - const expectedHash = codeV1.hash() - expect(currentCode.toString('hex')).toBe(codeV1.toString('hex')) - const expectedHashBigInt = BigInt('0x' + expectedHash.toString('hex')) - const hash = await upgradeableCounter.getCodeHash() - expect(hash).toBe(expectedHashBigInt) - }) + // Contract-specific tests below it('should have initial value', async () => { - let { blockchain, upgradeableCounter } = await setUpTest(0) - const user = await blockchain.treasury('user') + let { upgradeableCounter } = await setUpTest(0) const getterResult = await upgradeableCounter.getValue() expect(getterResult).toBe(0) }) @@ -95,10 +139,13 @@ describe('UpgradeableCounter', () => { const increaser = await blockchain.treasury('increaser' + i) const counterBefore = await upgradeableCounter.getValue() - let increaseResult = await upgradeableCounter.sendStep(increaser.getSender(), { - value: toNano('0.05'), - queryId: Math.floor(Math.random() * 10000), - }) + let increaseResult = await upgradeableCounter.sendStep( + increaser.getSender(), + toNano('0.05'), + { + queryId: BigInt(Math.floor(Math.random() * 10000)), + }, + ) expect(increaseResult.transactions).toHaveTransaction({ from: increaser.address, @@ -111,54 +158,6 @@ describe('UpgradeableCounter', () => { } }) - it('should be upgraded to version 2', async () => { - let { blockchain, owner, upgradeableCounter: upgradeableCounterV1, codeV2 } = await setUpTest(0) - - const typeAndVersion1 = await upgradeableCounterV1.getTypeAndVersion() - expect(typeAndVersion1.type).toBe('com.chainlink.ton.examples.upgrades.UpgradeableCounter') - expect(typeAndVersion1.version).toBe('1.0.0') - - let { upgradeResult, newVersionInstance } = await sendUpgradeAndReturnNewVersion( - upgradeableCounterV1, - owner.getSender(), - toNano('0.05'), - UpgradeableCounterV2, - ) - expect(upgradeResult.transactions).toHaveTransaction({ - from: owner.address, - to: upgradeableCounterV1.address, - success: true, - }) - - let upgradeableCounterV2 = blockchain.openContract(newVersionInstance) - - const code = await upgradeableCounterV2.getCode() - const expectedHash = codeV2.hash() - expect(code.toString('hex')).toBe(codeV2.toString('hex')) - const expectedHashBigInt = BigInt('0x' + expectedHash.toString('hex')) - const hash = await upgradeableCounterV2.getCodeHash() - expect(hash).toBe(expectedHashBigInt) - - const typeAndVersion2 = await upgradeableCounterV2.getTypeAndVersion() - expect(typeAndVersion2.type).toBe('com.chainlink.ton.examples.upgrades.UpgradeableCounter') - expect(typeAndVersion2.version).toBe('2.0.0') - - const upgradeTransaction = upgradeResult.transactions.find( - (tx) => - tx.inMessage?.info.type === 'internal' && - tx.inMessage.info.src.equals(owner.address) && - tx.inMessage.info.dest.equals(upgradeableCounterV1.address), - ) - const event = upgradeTransaction?.outMessages.values().find((msg: Message) => { - return msg.info.type === 'external-out' - }) - expect(event).toBeDefined() - const upgradedEvent = loadUpgradedEvent(event!.body.beginParse()) - expect(upgradedEvent.version).toBe('2.0.0') - expect(upgradedEvent.code.toString('hex')).toBe(codeV2.toString('hex')) - expect(upgradedEvent.codeHash).toBe(expectedHashBigInt) - }) - it('version 2 should decrease the counter', async () => { let { blockchain, owner, upgradeableCounter: upgradeableCounterV1 } = await setUpTest(3) @@ -167,6 +166,8 @@ describe('UpgradeableCounter', () => { owner.getSender(), toNano('0.05'), UpgradeableCounterV2, + '1.0.0', + await UpgradeableCounterV2.code(), ) expect(upgradeResult.transactions).toHaveTransaction({ @@ -183,10 +184,13 @@ describe('UpgradeableCounter', () => { const counterBefore = await upgradeableCounterV2.getValue() - let decreaseResult = await upgradeableCounterV2.sendStep(decreaser.getSender(), { - value: toNano('0.05'), - queryId: Math.floor(Math.random() * 10000), - }) + let decreaseResult = await upgradeableCounterV2.sendStep( + decreaser.getSender(), + toNano('0.05'), + { + queryId: BigInt(Math.floor(Math.random() * 10000)), + }, + ) expect(decreaseResult.transactions).toHaveTransaction({ from: decreaser.address, @@ -199,28 +203,6 @@ describe('UpgradeableCounter', () => { } }) - it('should fail when non-owner tries to upgrade', async () => { - let { blockchain, upgradeableCounter, codeV2 } = await setUpTest(0) - const nonOwner = await blockchain.treasury('nonOwner') - - // Try to upgrade from non-owner address - should fail - const upgradeResult = await upgradeableCounter.sendUpgrade(nonOwner.getSender(), { - value: toNano('0.05'), - queryId: Math.floor(Math.random() * 10000), - code: codeV2, - }) - - expect(upgradeResult.transactions).toHaveTransaction({ - from: nonOwner.address, - to: upgradeableCounter.address, - success: false, - }) - - // Verify the contract is still on version 1 - const typeAndVersion = await upgradeableCounter.getTypeAndVersion() - expect(typeAndVersion.version).toBe('1.0.0') - }) - it('should transfer ownership and allow new owner to upgrade', async () => { let { blockchain, owner, upgradeableCounter, codeV2 } = await setUpTest(0) const newOwner = await blockchain.treasury('newOwner') @@ -269,11 +251,15 @@ describe('UpgradeableCounter', () => { expect(currentOwner.equals(newOwner.address)).toBe(true) // Old owner should no longer be able to upgrade - const oldOwnerUpgradeResult = await upgradeableCounter.sendUpgrade(owner.getSender(), { - value: toNano('0.05'), - queryId: Math.floor(Math.random() * 10000), - code: codeV2, - }) + const oldOwnerUpgradeResult = await upgradeableCounter.sendUpgrade( + owner.getSender(), + toNano('0.05'), + { + queryId: BigInt(Math.floor(Math.random() * 10000)), + fromVersion: '1.0.0', + code: codeV2, + }, + ) expect(oldOwnerUpgradeResult.transactions).toHaveTransaction({ from: owner.address, @@ -287,6 +273,8 @@ describe('UpgradeableCounter', () => { newOwner.getSender(), toNano('0.05'), UpgradeableCounterV2, + '1.0.0', + await UpgradeableCounterV2.code(), ) expect(upgradeResult.transactions).toHaveTransaction({ @@ -299,7 +287,9 @@ describe('UpgradeableCounter', () => { // Verify the contract is now on version 2 const typeAndVersion = await upgradeableCounterV2.getTypeAndVersion() - expect(typeAndVersion.type).toBe('com.chainlink.ton.examples.upgrades.UpgradeableCounter') + expect(typeAndVersion.type).toBe( + 'com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter', + ) expect(typeAndVersion.version).toBe('2.0.0') // Verify new owner is still the owner after upgrade diff --git a/contracts/tests/lib/versioning/Upgradeable.spec.ts b/contracts/tests/lib/versioning/Upgradeable.spec.ts new file mode 100644 index 000000000..135b950bb --- /dev/null +++ b/contracts/tests/lib/versioning/Upgradeable.spec.ts @@ -0,0 +1,19 @@ +import * as upgradeable from '../../../wrappers/libraries/versioning/Upgradeable' +import { crc32 } from 'zlib' +import { errorCode } from '../../../wrappers/utils' + +describe('Upgradeable', () => { + it('should compute error code', async () => { + expect(upgradeable.Error.VersionMismatch).toBe( + errorCode(crc32('com.chainlink.ton.lib.versioning.Upgradeable'), 0), + ) + }) + + it('should have correct opcode', async () => { + expect(upgradeable.opcodes.Upgrade).toBe(crc32('Upgradeable_Upgrade')) + }) + + it('should have correct event topic', async () => { + expect(upgradeable.eventTopics.Upgraded).toBe(crc32('Upgradeable_UpgradedEvent')) + }) +}) diff --git a/contracts/tests/lib/versioning/UpgradeableSpec.ts b/contracts/tests/lib/versioning/UpgradeableSpec.ts new file mode 100644 index 000000000..c00d8afad --- /dev/null +++ b/contracts/tests/lib/versioning/UpgradeableSpec.ts @@ -0,0 +1,416 @@ +import { + Address, + beginCell, + Cell, + Contract, + ContractProvider, + Message, + Sender, + toNano, +} from '@ton/core' +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox' +import '@ton/test-utils' +import * as upgradeable from '../../../wrappers/libraries/versioning/Upgradeable' +import { TypeAndVersion } from '../../../wrappers/libraries/TypeAndVersion' + +/** + * Configuration for testing upgrades between two versions of an upgradeable contract. + */ +export type UpgradeTestConfig = { + /** The expected contract type name (e.g., 'com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter') */ + contractType: string + /** Version string for previous version contract */ + prevVersion: string + /** Version string for current version contract */ + currentVersion: string + /** Function to get the code for previous version contract */ + getPrevCode: () => Promise + /** Function to get the code for current version contract */ + getCurrentCode: () => Promise + /** Constructor for current version contract */ + CurrentVersionConstructor: new ( + address: Address, + init?: { code: Cell; data: Cell }, + ) => TCurrentVersionContract + /** Amount of TON to use on sendUpgrade */ + upgradeValue?: bigint +} + +/** + * Configuration for testing the current version of an upgradeable contract. + */ +export type CurrentVersionTestConfig = { + /** The expected contract type name (e.g., 'com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter') */ + contractType: string + /** Version string for current version contract */ + currentVersion: string + /** Function to get the code for current version contract */ + getCurrentCode: () => Promise + /** Constructor for current version contract */ + CurrentVersionConstructor: new ( + address: Address, + init?: { code: Cell; data: Cell }, + ) => TCurrentVersionContract + /** Amount of TON to use on sendUpgrade */ + upgradeValue?: bigint +} + +/** + * Contract interface that must be implemented by upgradeable contracts for testing. + */ +export interface UpgradeableContract extends upgradeable.Upgradeable, TypeAndVersion, Contract {} + +interface TestSetup { + blockchain: Blockchain + owner: SandboxContract + nonOwner: SandboxContract + prevContract: SandboxContract + prevCode: Cell + currentCode: Cell +} + +/** + * Creates a reusable test suite for testing upgrades between two versions of an upgradeable contract. + * + * @param config Configuration for the upgrade tests + * @param setupPrevContract Function to deploy and setup the previous version contract + * @returns An object with test functions + * + * @example + * ```typescript + * const upgradeSpec = newUpgradeSpec( + * { + * contractType: 'com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter', + * prevVersion: '1.0.0', + * currentVersion: '2.0.0', + * getPrevCode: () => UpgradeableCounterV1.code(), + * getCurrentCode: () => UpgradeableCounterV2.code(), + * CurrentVersionConstructor: UpgradeableCounterV2, + * }, + * async (blockchain, owner) => { + * const codeV1 = await UpgradeableCounterV1.code() + * const contract = blockchain.openContract( + * UpgradeableCounterV1.createFromConfig( + * { + * id: 0, + * value: 0, + * ownable: { owner: owner.address, pendingOwner: null }, + * }, + * codeV1, + * ), + * ) + * const deployer = await blockchain.treasury('deployer') + * await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + * return contract + * } + * ) + * + * describe('UpgradeableCounter - Upgrade Tests', () => { + * upgradeSpec.run() + * }) + * ``` + */ +export function newUpgradeSpec< + TContractV1 extends UpgradeableContract, + TContractV2 extends UpgradeableContract, +>( + config: UpgradeTestConfig, + setupPrevContract: ( + blockchain: Blockchain, + owner: SandboxContract, + ) => Promise>, +) { + async function setup(): Promise { + const blockchain = await Blockchain.create() + blockchain.verbosity = { + print: false, + blockchainLogs: false, + vmLogs: 'none', + debugLogs: false, + } + + const owner = await blockchain.treasury('owner') + const nonOwner = await blockchain.treasury('nonOwner') + const prevCode = await config.getPrevCode() + const currentCode = await config.getCurrentCode() + const prevContract: SandboxContract = await setupPrevContract( + blockchain, + owner, + ) + + return { + blockchain, + owner, + nonOwner, + prevContract, + prevCode, + currentCode, + } + } + + const amount = config.upgradeValue ?? toNano('0.05') + + return { + run: () => { + /** + * Test that the contract deploys on the correct version (previous version) + */ + it('should deploy on correct version', async () => { + const { prevContract, prevCode } = await setup() + + const typeAndVersion = await prevContract.getTypeAndVersion() + expect(typeAndVersion.type).toBe(config.contractType) + expect(typeAndVersion.version).toBe(config.prevVersion) + + const currentCode = await prevContract.getCode() + expect(currentCode.toString('hex')).toBe(prevCode.toString('hex')) + + const expectedHash = BigInt('0x' + prevCode.hash().toString('hex')) + const hash = await prevContract.getCodeHash() + expect(hash).toBe(expectedHash) + }) + + /** + * Test that the contract can be upgraded from previous to current version + */ + it('should upgrade from previous to current version', async () => { + const testSetup = await setup() + + await upgradePrevToCurrent(testSetup) + }) + + async function upgradePrevToCurrent(testSetup: TestSetup): Promise< + { + currentVersionContract: SandboxContract + } & TestSetup + > { + // Verify initial version + const typeAndVersionPrev = await testSetup.prevContract.getTypeAndVersion() + expect(typeAndVersionPrev.type).toBe(config.contractType) + expect(typeAndVersionPrev.version).toBe(config.prevVersion) + + // Perform upgrade + const { upgradeResult, newVersionInstance } = + await upgradeable.sendUpgradeAndReturnNewVersion( + testSetup.prevContract, + testSetup.owner.getSender(), + amount, + config.CurrentVersionConstructor, + config.prevVersion, + testSetup.currentCode, + ) + + expect(upgradeResult.transactions).toHaveTransaction({ + from: testSetup.owner.address, + to: testSetup.prevContract.address, + success: true, + }) + + const currentVersionContract: SandboxContract = + testSetup.blockchain.openContract(newVersionInstance) + + // Verify code changed + const code = await currentVersionContract.getCode() + expect(code.toString('hex')).toBe(testSetup.currentCode.toString('hex')) + + const expectedHash = BigInt('0x' + testSetup.currentCode.hash().toString('hex')) + const hash = await currentVersionContract.getCodeHash() + expect(hash).toBe(expectedHash) + + // Verify version changed + const typeAndVersionCurrent = await currentVersionContract.getTypeAndVersion() + expect(typeAndVersionCurrent.type).toBe(config.contractType) + expect(typeAndVersionCurrent.version).toBe(config.currentVersion) + + // Verify upgrade event was emitted + const upgradeTransaction = upgradeResult.transactions.find( + (tx) => + tx.inMessage?.info.type === 'internal' && + tx.inMessage.info.src.equals(testSetup.owner.address) && + tx.inMessage.info.dest.equals(testSetup.prevContract.address), + ) + const event = upgradeTransaction?.outMessages.values().find((msg: Message) => { + return msg.info.type === 'external-out' + }) + expect(event).toBeDefined() + + const upgradedEvent = upgradeable.builder.event.upgraded.load(event!.body.beginParse()) + expect(upgradedEvent.version).toBe(config.currentVersion) + expect(upgradedEvent.code.toString('hex')).toBe(testSetup.currentCode.toString('hex')) + expect(upgradedEvent.codeHash).toBe(expectedHash) + return { currentVersionContract, ...testSetup } + } + }, + } +} + +interface CurrentVersionTestSetup { + blockchain: Blockchain + owner: SandboxContract + nonOwner: SandboxContract + currentContract: SandboxContract + currentCode: Cell +} + +/** + * Creates a reusable test suite for testing the current version of an upgradeable contract. + * + * @param config Configuration for the current version tests + * @param setupCurrentContract Function to deploy and setup the current version contract + * @returns An object with test functions + * + * @example + * ```typescript + * const currentVersionSpec = newCurrentVersionSpec( + * { + * contractType: 'com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter', + * currentVersion: '2.0.0', + * getCurrentCode: () => UpgradeableCounterV2.code(), + * CurrentVersionConstructor: UpgradeableCounterV2, + * }, + * async (blockchain, owner) => { + * const code = await UpgradeableCounterV2.code() + * const contract = blockchain.openContract( + * UpgradeableCounterV2.createFromConfig( + * { + * id: 0, + * value: 0, + * ownable: { owner: owner.address, pendingOwner: null }, + * }, + * code, + * ), + * ) + * const deployer = await blockchain.treasury('deployer') + * await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + * return contract + * } + * ) + * + * describe('UpgradeableCounter - Current Version Tests', () => { + * currentVersionSpec.run() + * }) + * ``` + */ +export function newCurrentVersionSpec( + config: CurrentVersionTestConfig, + setupCurrentContract: ( + blockchain: Blockchain, + owner: SandboxContract, + ) => Promise>, +) { + async function setup(): Promise { + const blockchain = await Blockchain.create() + blockchain.verbosity = { + print: false, + blockchainLogs: false, + vmLogs: 'none', + debugLogs: false, + } + + const owner = await blockchain.treasury('owner') + const nonOwner = await blockchain.treasury('nonOwner') + const currentCode = await config.getCurrentCode() + const currentContract: SandboxContract = await setupCurrentContract( + blockchain, + owner, + ) + + return { + blockchain, + owner, + nonOwner, + currentContract, + currentCode, + } + } + + const amount = config.upgradeValue ?? toNano('0.05') + + return { + run: () => { + /** + * Test that the contract deploys on the correct version + */ + it('should deploy on correct version', async () => { + const { currentContract, currentCode } = await setup() + + const typeAndVersion = await currentContract.getTypeAndVersion() + expect(typeAndVersion.type).toBe(config.contractType) + expect(typeAndVersion.version).toBe(config.currentVersion) + + const code = await currentContract.getCode() + expect(code.toString('hex')).toBe(currentCode.toString('hex')) + + const expectedHash = BigInt('0x' + currentCode.hash().toString('hex')) + const hash = await currentContract.getCodeHash() + expect(hash).toBe(expectedHash) + }) + + /** + * Test that upgrade fails when a non-owner tries to upgrade + */ + it('should fail when non-owner tries to upgrade', async () => { + const { currentContract, nonOwner, currentCode } = await setup() + + // Verify initial version + const typeAndVersion = await currentContract.getTypeAndVersion() + expect(typeAndVersion.version).toBe(config.currentVersion) + + // Try to upgrade from non-owner address - should fail + // Use some dummy code for the upgrade attempt + const upgradeResult = await currentContract.sendUpgrade(nonOwner.getSender(), amount, { + queryId: BigInt(Math.floor(Math.random() * 10000)), + fromVersion: config.currentVersion, + code: beginCell().endCell(), // Dummy code + }) + + expect(upgradeResult.transactions).toHaveTransaction({ + from: nonOwner.address, + to: currentContract.address, + success: false, + }) + + // Verify the contract is still on current version + const finalVersion = await currentContract.getTypeAndVersion() + expect(finalVersion.version).toBe(config.currentVersion) + + // Verify the code hasn't changed + const code = await currentContract.getCode() + expect(code.toString('hex')).toBe(currentCode.toString('hex')) + }) + + /** + * Test that upgrade fails when fromVersion doesn't match current version + */ + it('should fail when fromVersion does not match current version', async () => { + const { currentContract, owner, currentCode } = await setup() + + // Verify initial version + const typeAndVersion = await currentContract.getTypeAndVersion() + expect(typeAndVersion.version).toBe(config.currentVersion) + + // Try to upgrade with wrong fromVersion - should fail + const upgradeResult = await currentContract.sendUpgrade(owner.getSender(), amount, { + queryId: BigInt(Math.floor(Math.random() * 10000)), + fromVersion: config.currentVersion + '-different', // Wrong version! + code: beginCell().endCell(), // Dummy code + }) + + expect(upgradeResult.transactions).toHaveTransaction({ + from: owner.address, + to: currentContract.address, + success: false, + exitCode: upgradeable.Error.VersionMismatch, + }) + + // Verify the contract is still on current version + const finalVersion = await currentContract.getTypeAndVersion() + expect(finalVersion.version).toBe(config.currentVersion) + + // Verify the code hasn't changed + const code = await currentContract.getCode() + expect(code.toString('hex')).toBe(currentCode.toString('hex')) + }) + }, + } +} diff --git a/contracts/wrappers/ccip/FeeQuoter.ts b/contracts/wrappers/ccip/FeeQuoter.ts index f3fa10607..1ec3ca4c4 100644 --- a/contracts/wrappers/ccip/FeeQuoter.ts +++ b/contracts/wrappers/ccip/FeeQuoter.ts @@ -17,6 +17,11 @@ import { import * as ownable2step from '../libraries/access/Ownable2Step' import { CellCodec } from '../utils' import { asSnakeData, fromSnakeData } from '../../src/utils' +import * as upgradeable from '../libraries/versioning/Upgradeable' +import * as typeAndVersion from '../libraries/TypeAndVersion' +import { compile } from '@ton/blueprint' + +export const FEE_QUOTER_CONTRACT_VERSION = '0.0.4' export const FEE_QUOTER_FACILITY_NAME = 'com.chainlink.ton.ccip.FeeQuoter' export const FEE_QUOTER_FACILITY_ID = 248 @@ -381,7 +386,7 @@ export type UpdateDestChainConfigs = { export abstract class Errors {} -export class FeeQuoter implements Contract { +export class FeeQuoter implements upgradeable.Upgradeable, typeAndVersion.TypeAndVersion, Contract { constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell }, @@ -413,6 +418,37 @@ export class FeeQuoter implements Contract { }) } + sendUpgrade( + provider: ContractProvider, + via: Sender, + value: bigint, + body: upgradeable.Upgrade, + ): Promise { + return upgradeable.sendUpgrade(provider, via, value, body) + } + + getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { + return typeAndVersion.getTypeAndVersion(provider) + } + getCode(provider: ContractProvider): Promise { + return typeAndVersion.getCode(provider) + } + getCodeHash(provider: ContractProvider): Promise { + return typeAndVersion.getCodeHash(provider) + } + + static version() { + return FEE_QUOTER_CONTRACT_VERSION + } + + static type() { + return FEE_QUOTER_FACILITY_NAME + } + + static async code() { + return await compile('FeeQuoter') + } + async sendUpdateDestChainConfigs( provider: ContractProvider, via: Sender, diff --git a/contracts/wrappers/ccip/OffRamp.ts b/contracts/wrappers/ccip/OffRamp.ts index bdf07e6b4..72c77af90 100644 --- a/contracts/wrappers/ccip/OffRamp.ts +++ b/contracts/wrappers/ccip/OffRamp.ts @@ -10,6 +10,7 @@ import { SendMode, Slice, Builder, + ContractABI, } from '@ton/core' import { OCR3Base, ReportContext, SignatureEd25519 } from '../libraries/ocr/MultiOCR3Base' @@ -18,6 +19,10 @@ import * as ownable2step from '../libraries/access/Ownable2Step' import { crc32 } from 'zlib' import { CellCodec, facilityId } from '../utils' import { CCIPReceive, ReceiverStorage } from './Receiver' +import * as upgradeable from '../libraries/versioning/Upgradeable' +import * as typeAndVersion from '../libraries/TypeAndVersion' +import { Maybe } from '@ton/core/dist/utils/maybe' +import { compile } from '@ton/blueprint' export type OffRampStorage = { id: bigint @@ -212,6 +217,8 @@ export const MERKLE_ROOT_FACILITY_NAME = 'com.chainlink.ton.ccip.MerkleRoot' export const MERKLE_ROOT_FACILITY_ID = 479 export const MERKLE_ROOT_ERROR_CODE = 47900 //FACILITY_ID * 100 +export const OFFRAMP_CONTRACT_VERSION = '0.0.4' + export const OFFRAMP_FACILITY_NAME = 'com.chainlink.ton.ccip.OffRamp' export const OFFRAMP_FACILITY_ID = 84 export const OFFRAMP_ERROR_CODE = 8400 //FACILITY_ID * 100 @@ -232,13 +239,17 @@ export enum MerkleRootError { NotOwner, } -export class OffRamp extends OCR3Base { +export class OffRamp + extends OCR3Base + implements upgradeable.Upgradeable, typeAndVersion.TypeAndVersion, Contract +{ constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell }, ) { super() } + abi?: Maybe static createFromAddress(address: Address) { return new OffRamp(address) @@ -266,6 +277,37 @@ export class OffRamp extends OCR3Base { }) } + sendUpgrade( + provider: ContractProvider, + via: Sender, + value: bigint, + body: upgradeable.Upgrade, + ): Promise { + return upgradeable.sendUpgrade(provider, via, value, body) + } + + getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { + return typeAndVersion.getTypeAndVersion(provider) + } + getCode(provider: ContractProvider): Promise { + return typeAndVersion.getCode(provider) + } + getCodeHash(provider: ContractProvider): Promise { + return typeAndVersion.getCodeHash(provider) + } + + static version() { + return OFFRAMP_CONTRACT_VERSION + } + + static type() { + return OFFRAMP_FACILITY_NAME + } + + static async code() { + return await compile('OffRamp') + } + async sendCommit( provider: ContractProvider, via: Sender, diff --git a/contracts/wrappers/ccip/OnRamp.ts b/contracts/wrappers/ccip/OnRamp.ts index 90addc2b7..2373c18b3 100644 --- a/contracts/wrappers/ccip/OnRamp.ts +++ b/contracts/wrappers/ccip/OnRamp.ts @@ -16,11 +16,16 @@ import * as ownable2step from '../libraries/access/Ownable2Step' import { asSnakeData } from '../../src/utils' import { CellCodec } from '../utils' import * as rt from './Router' +import * as upgradeable from '../libraries/versioning/Upgradeable' +import * as typeAndVersion from '../libraries/TypeAndVersion' +import { compile } from '@ton/blueprint' export const ONRAMP_FACILITY_NAME = 'com.chainlink.ton.ccip.OnRamp' export const ONRAMP_FACILITY_ID = 181 export const ONRAMP_ERROR_CODE = 18100 //FACILITY_ID * 100 +export const ONRAMP_CONTRACT_VERSION = '0.0.4' + export const CCIP_SEND_EXECUTOR_FACILITY_NAME = 'com.chainlink.ton.ccip.CCIPSendExecutor' export const CCIP_SEND_EXECUTOR_FACILITY_ID = 436 export const CCIP_SEND_EXECUTOR_ERROR_CODE = 43600 //FACILITY_ID * 100 @@ -171,6 +176,37 @@ export class OnRamp implements Contract { }) } + sendUpgrade( + provider: ContractProvider, + via: Sender, + value: bigint, + body: upgradeable.Upgrade, + ): Promise { + return upgradeable.sendUpgrade(provider, via, value, body) + } + + getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { + return typeAndVersion.getTypeAndVersion(provider) + } + getCode(provider: ContractProvider): Promise { + return typeAndVersion.getCode(provider) + } + getCodeHash(provider: ContractProvider): Promise { + return typeAndVersion.getCodeHash(provider) + } + + static version() { + return ONRAMP_CONTRACT_VERSION + } + + static type() { + return ONRAMP_FACILITY_NAME + } + + static async code() { + return await compile('OnRamp') + } + async sendSetDynamicConfig( provider: ContractProvider, via: Sender, diff --git a/contracts/wrappers/ccip/Router.ts b/contracts/wrappers/ccip/Router.ts index b1ab7a1a0..8f0a7df5a 100644 --- a/contracts/wrappers/ccip/Router.ts +++ b/contracts/wrappers/ccip/Router.ts @@ -16,6 +16,12 @@ import * as ownable2step from '../libraries/access/Ownable2Step' import { CellCodec } from '../utils' import { asSnakeData, asSnakeDataUint, fromSnakeData } from '../../src/utils' +import * as upgradeable from '../libraries/versioning/Upgradeable' +import * as typeAndVersion from '../libraries/TypeAndVersion' +import { compile } from '@ton/blueprint' + +export const ROUTER_CONTRACT_VERSION = '0.0.4' + export const ROUTER_FACILITY_NAME = 'com.chainlink.ton.ccip.Router' export const ROUTER_FACILITY_ID = 496 export const ROUTER_ERROR_CODE = 49600 //FACILITY_ID * 100 @@ -38,7 +44,7 @@ export abstract class Opcodes { static ccipSend = 0x00000001 } -export class Router implements Contract { +export class Router implements upgradeable.Upgradeable, typeAndVersion.TypeAndVersion, Contract { constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell }, @@ -73,6 +79,37 @@ export class Router implements Contract { }) } + sendUpgrade( + provider: ContractProvider, + via: Sender, + value: bigint, + body: upgradeable.Upgrade, + ): Promise { + return upgradeable.sendUpgrade(provider, via, value, body) + } + + getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { + return typeAndVersion.getTypeAndVersion(provider) + } + getCode(provider: ContractProvider): Promise { + return typeAndVersion.getCode(provider) + } + getCodeHash(provider: ContractProvider): Promise { + return typeAndVersion.getCodeHash(provider) + } + + static version() { + return ROUTER_CONTRACT_VERSION + } + + static type() { + return ROUTER_FACILITY_NAME + } + + static async code() { + return await compile('Router') + } + async sendSetRamps( provider: ContractProvider, via: Sender, diff --git a/contracts/wrappers/examples.upgrades.UpgradeableCounterV1.compile.ts b/contracts/wrappers/examples.versioning.upgrades.UpgradeableCounterV1.compile.ts similarity index 78% rename from contracts/wrappers/examples.upgrades.UpgradeableCounterV1.compile.ts rename to contracts/wrappers/examples.versioning.upgrades.UpgradeableCounterV1.compile.ts index b91413fa0..9fe4eddda 100644 --- a/contracts/wrappers/examples.upgrades.UpgradeableCounterV1.compile.ts +++ b/contracts/wrappers/examples.versioning.upgrades.UpgradeableCounterV1.compile.ts @@ -2,7 +2,7 @@ import { CompilerConfig } from '@ton/blueprint' export const compile: CompilerConfig = { lang: 'tolk', - entrypoint: 'contracts/test/examples/upgrades/upgradeable_counter/v1/contract.tolk', + entrypoint: 'contracts/test/examples/versioning/upgrades/upgradeable_counter/v1/contract.tolk', withStackComments: true, // Fift output will contain comments, if you wish to debug its output withSrcLineComments: true, // Fift output will contain .tolk lines as comments experimentalOptions: '', // you can pass experimental compiler options here diff --git a/contracts/wrappers/examples.upgrades.UpgradeableCounterV2.compile.ts b/contracts/wrappers/examples.versioning.upgrades.UpgradeableCounterV2.compile.ts similarity index 78% rename from contracts/wrappers/examples.upgrades.UpgradeableCounterV2.compile.ts rename to contracts/wrappers/examples.versioning.upgrades.UpgradeableCounterV2.compile.ts index 0c0e38a1a..b506a4809 100644 --- a/contracts/wrappers/examples.upgrades.UpgradeableCounterV2.compile.ts +++ b/contracts/wrappers/examples.versioning.upgrades.UpgradeableCounterV2.compile.ts @@ -2,7 +2,7 @@ import { CompilerConfig } from '@ton/blueprint' export const compile: CompilerConfig = { lang: 'tolk', - entrypoint: 'contracts/test/examples/upgrades/upgradeable_counter/v2/contract.tolk', + entrypoint: 'contracts/test/examples/versioning/upgrades/upgradeable_counter/v2/contract.tolk', withStackComments: true, // Fift output will contain comments, if you wish to debug its output withSrcLineComments: true, // Fift output will contain .tolk lines as comments experimentalOptions: '', // you can pass experimental compiler options here diff --git a/contracts/wrappers/examples/Counter.ts b/contracts/wrappers/examples/Counter.ts index 1c2010991..521ee03bc 100644 --- a/contracts/wrappers/examples/Counter.ts +++ b/contracts/wrappers/examples/Counter.ts @@ -10,7 +10,7 @@ import { SendMode, Slice, } from '@ton/core' -import { TypeAndVersion } from '../libraries/TypeAndVersion' +import * as typeAndVersion from '../libraries/TypeAndVersion' import * as ownable2step from '../libraries/access/Ownable2Step' import { CellCodec } from '../utils' @@ -113,15 +113,11 @@ export const builder = { })(), } -export class ContractClient implements Contract, TypeAndVersion { - private typeAndVersion: TypeAndVersion - +export class ContractClient implements Contract, typeAndVersion.TypeAndVersion { constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell }, - ) { - this.typeAndVersion = new TypeAndVersion() - } + ) {} static newAt(address: Address): ContractClient { return new ContractClient(address) @@ -180,14 +176,14 @@ export class ContractClient implements Contract, TypeAndVersion { // Delegate TypeAndVersion methods async getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { - return this.typeAndVersion.getTypeAndVersion(provider) + return typeAndVersion.getTypeAndVersion(provider) } async getCode(provider: ContractProvider): Promise { - return this.typeAndVersion.getCode(provider) + return typeAndVersion.getCode(provider) } async getCodeHash(provider: ContractProvider): Promise { - return this.typeAndVersion.getCodeHash(provider) + return typeAndVersion.getCodeHash(provider) } } diff --git a/contracts/wrappers/examples/upgrades/UpgradeableCounterV1.ts b/contracts/wrappers/examples/versioning/UpgradeableCounterV1.ts similarity index 55% rename from contracts/wrappers/examples/upgrades/UpgradeableCounterV1.ts rename to contracts/wrappers/examples/versioning/UpgradeableCounterV1.ts index ad2b916a9..17f9523c5 100644 --- a/contracts/wrappers/examples/upgrades/UpgradeableCounterV1.ts +++ b/contracts/wrappers/examples/versioning/UpgradeableCounterV1.ts @@ -1,17 +1,23 @@ import { Address, beginCell, + Builder, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, + Slice, } from '@ton/core' -import { Upgradeable } from '../../libraries/upgrades/Upgradeable' +import * as upgradeable from '../../libraries/versioning/Upgradeable' import { compile } from '@ton/blueprint' -import { TypeAndVersion } from '../../libraries/TypeAndVersion' +import * as typeAndVersion from '../../libraries/TypeAndVersion' import * as ownable2step from '../../libraries/access/Ownable2Step' +import { CellCodec } from '../../utils' + +export const FACILITY_NAME = 'com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter' +export const CONTRACT_VERSION = '1.0.0' export type CounterConfig = { id: number @@ -19,27 +25,59 @@ export type CounterConfig = { ownable: ownable2step.Data } -export function counterConfigToCell(config: CounterConfig): Cell { - const builder = beginCell().storeUint(config.id, 32).storeUint(config.value, 32) - builder.storeBuilder(ownable2step.builder.data.traitData.encode(config.ownable)) - return builder.endCell() +export type Step = { + queryId: bigint +} + +export const opcodes = { + Step: 0x00000001, } -export const Opcodes = { - OP_STEP: 0x00000001, +export const builder = { + message: { + in: { + step: ((): CellCodec => { + return { + encode: (msg: Step): Builder => { + return beginCell().storeUint(opcodes.Step, 32).storeUint(msg.queryId, 64) + }, + load: (src: Slice): Step => { + src.skip(32) + return { + queryId: src.loadUintBig(64), + } + }, + } + })(), + upgrade: upgradeable.builder.message.in.upgrade, + }, + }, + data: { + counterConfig: ((): CellCodec => { + return { + encode: (config: CounterConfig): Builder => { + return beginCell() + .storeUint(config.id, 32) + .storeUint(config.value, 32) + .storeBuilder(ownable2step.builder.data.traitData.encode(config.ownable)) + }, + load: (src: Slice): CounterConfig => { + throw new Error('Not implemented') + }, + } + })(), + }, } -export class UpgradeableCounterV1 implements TypeAndVersion, Upgradeable { - private typeAndVersion: TypeAndVersion - private upgradeable: Upgradeable +export class UpgradeableCounterV1 + implements typeAndVersion.TypeAndVersion, upgradeable.Upgradeable +{ private ownable: ownable2step.ContractClient constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell }, ) { - this.typeAndVersion = new TypeAndVersion() - this.upgradeable = new Upgradeable() this.ownable = new ownable2step.ContractClient(address) } @@ -48,15 +86,19 @@ export class UpgradeableCounterV1 implements TypeAndVersion, Upgradeable { } static code(): Promise { - return compile('examples.upgrades.UpgradeableCounterV1') + return compile('examples.versioning.upgrades.UpgradeableCounterV1') + } + + static version() { + return CONTRACT_VERSION } - code(): Promise { - return compile('examples.upgrades.UpgradeableCounterV1') + static type() { + return FACILITY_NAME } static createFromConfig(config: CounterConfig, code: Cell, workchain = 0) { - const data = counterConfigToCell(config) + const data = builder.data.counterConfig.encode(config).endCell() const init = { code, data } return new UpgradeableCounterV1(contractAddress(workchain, init), init) } @@ -69,21 +111,11 @@ export class UpgradeableCounterV1 implements TypeAndVersion, Upgradeable { }) } - async sendStep( - provider: ContractProvider, - via: Sender, - opts: { - value: bigint - queryId?: number - }, - ) { + async sendStep(provider: ContractProvider, via: Sender, value: bigint, body: Step) { await provider.internal(via, { - value: opts.value, + value: value, sendMode: SendMode.PAY_GAS_SEPARATELY, - body: beginCell() - .storeUint(Opcodes.OP_STEP, 32) - .storeUint(opts.queryId ?? 0, 64) - .endCell(), + body: builder.message.in.step.encode(body).endCell(), }) } @@ -94,28 +126,25 @@ export class UpgradeableCounterV1 implements TypeAndVersion, Upgradeable { // Delegate TypeAndVersion methods async getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { - return this.typeAndVersion.getTypeAndVersion(provider) + return typeAndVersion.getTypeAndVersion(provider) } async getCode(provider: ContractProvider): Promise { - return this.typeAndVersion.getCode(provider) + return typeAndVersion.getCode(provider) } async getCodeHash(provider: ContractProvider): Promise { - return this.typeAndVersion.getCodeHash(provider) + return typeAndVersion.getCodeHash(provider) } // Delegate Upgradeable methods async sendUpgrade( provider: ContractProvider, via: Sender, - opts: { - value: bigint - queryId?: number - code: Cell - }, + value: bigint, + body: upgradeable.Upgrade, ) { - await this.upgradeable.sendUpgrade(provider, via, opts) + await upgradeable.sendUpgrade(provider, via, value, body) } // Ownership methods diff --git a/contracts/wrappers/examples/upgrades/UpgradeableCounterV2.ts b/contracts/wrappers/examples/versioning/UpgradeableCounterV2.ts similarity index 53% rename from contracts/wrappers/examples/upgrades/UpgradeableCounterV2.ts rename to contracts/wrappers/examples/versioning/UpgradeableCounterV2.ts index 6192a5dde..f4a82ac80 100644 --- a/contracts/wrappers/examples/upgrades/UpgradeableCounterV2.ts +++ b/contracts/wrappers/examples/versioning/UpgradeableCounterV2.ts @@ -1,45 +1,87 @@ import { Address, beginCell, + Builder, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, + Slice, } from '@ton/core' -import { Upgradeable } from '../../libraries/upgrades/Upgradeable' +import * as upgradeable from '../../libraries/versioning/Upgradeable' import { compile } from '@ton/blueprint' -import { TypeAndVersion } from '../../libraries/TypeAndVersion' +import * as typeAndVersion from '../../libraries/TypeAndVersion' import * as ownable2step from '../../libraries/access/Ownable2Step' +export const FACILITY_NAME = 'com.chainlink.ton.examples.versioning.upgrades.UpgradeableCounter' +export const CONTRACT_VERSION = '2.0.0' + export type CounterConfig = { id: number value: number ownable: ownable2step.Data } -export function counterConfigToCell(config: CounterConfig): Cell { - const builder = beginCell().storeUint(config.id, 32).storeUint(config.value, 32) - builder.storeBuilder(ownable2step.builder.data.traitData.encode(config.ownable)) - return builder.endCell() +export type Step = { + queryId: bigint +} + +interface CellCodec { + encode: (data: T) => Builder + load: (src: Slice) => T } -export const Opcodes = { - OP_STEP: 0x00000001, +export const opcodes = { + Step: 0x00000001, +} + +export const builder = { + message: { + in: { + step: ((): CellCodec => { + return { + encode: (msg: Step): Builder => { + return beginCell().storeUint(opcodes.Step, 32).storeUint(msg.queryId, 64) + }, + load: (src: Slice): Step => { + src.skip(32) + return { + queryId: src.loadUintBig(64), + } + }, + } + })(), + upgrade: upgradeable.builder.message.in.upgrade, + }, + }, + data: { + counterConfig: ((): CellCodec => { + return { + encode: (config: CounterConfig): Builder => { + return beginCell() + .storeUint(config.value, 64) + .storeUint(config.id, 32) + .storeBuilder(ownable2step.builder.data.traitData.encode(config.ownable)) + }, + load: (src: Slice): CounterConfig => { + throw new Error('Not implemented') + }, + } + })(), + }, } -export class UpgradeableCounterV2 implements Contract, TypeAndVersion, Upgradeable { - private typeAndVersion: TypeAndVersion - private upgradeable: Upgradeable +export class UpgradeableCounterV2 + implements Contract, typeAndVersion.TypeAndVersion, upgradeable.Upgradeable +{ private ownable: ownable2step.ContractClient constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell }, ) { - this.typeAndVersion = new TypeAndVersion() - this.upgradeable = new Upgradeable() this.ownable = new ownable2step.ContractClient(address) } @@ -47,16 +89,12 @@ export class UpgradeableCounterV2 implements Contract, TypeAndVersion, Upgradeab return new UpgradeableCounterV2(address) } - static code(): Promise { - return compile('examples.upgrades.UpgradeableCounterV2') - } - code(): Promise { - return compile('examples.upgrades.UpgradeableCounterV2') + return compile('examples.versioning.upgrades.UpgradeableCounterV2') } static createFromConfig(config: CounterConfig, code: Cell, workchain = 0) { - const data = counterConfigToCell(config) + const data = builder.data.counterConfig.encode(config).endCell() const init = { code, data } return new UpgradeableCounterV2(contractAddress(workchain, init), init) } @@ -69,21 +107,11 @@ export class UpgradeableCounterV2 implements Contract, TypeAndVersion, Upgradeab }) } - async sendStep( - provider: ContractProvider, - via: Sender, - opts: { - value: bigint - queryId?: number - }, - ) { + async sendStep(provider: ContractProvider, via: Sender, value: bigint, body: Step) { await provider.internal(via, { - value: opts.value, + value: value, sendMode: SendMode.PAY_GAS_SEPARATELY, - body: beginCell() - .storeUint(Opcodes.OP_STEP, 32) - .storeUint(opts.queryId ?? 0, 64) - .endCell(), + body: builder.message.in.step.encode(body).endCell(), }) } @@ -94,28 +122,37 @@ export class UpgradeableCounterV2 implements Contract, TypeAndVersion, Upgradeab // Delegate TypeAndVersion methods async getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { - return this.typeAndVersion.getTypeAndVersion(provider) + return typeAndVersion.getTypeAndVersion(provider) } async getCode(provider: ContractProvider): Promise { - return this.typeAndVersion.getCode(provider) + return typeAndVersion.getCode(provider) } async getCodeHash(provider: ContractProvider): Promise { - return this.typeAndVersion.getCodeHash(provider) + return typeAndVersion.getCodeHash(provider) } // Delegate Upgradeable methods async sendUpgrade( provider: ContractProvider, via: Sender, - opts: { - value: bigint - queryId?: number - code: Cell - }, + value: bigint, + body: upgradeable.Upgrade, ) { - await this.upgradeable.sendUpgrade(provider, via, opts) + await upgradeable.sendUpgrade(provider, via, value, body) + } + + static code(): Promise { + return compile('examples.versioning.upgrades.UpgradeableCounterV2') + } + + static version() { + return CONTRACT_VERSION + } + + static type() { + return FACILITY_NAME } // Ownership methods diff --git a/contracts/wrappers/libraries/TypeAndVersion.ts b/contracts/wrappers/libraries/TypeAndVersion.ts index 886e338c9..7e4eaa1ac 100644 --- a/contracts/wrappers/libraries/TypeAndVersion.ts +++ b/contracts/wrappers/libraries/TypeAndVersion.ts @@ -1,18 +1,24 @@ import { Address, Cell, Contract, ContractProvider } from '@ton/core' -export class TypeAndVersion { - async getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { - const result = await provider.get('typeAndVersion', []) - return { type: result.stack.readString(), version: result.stack.readString() } - } +export async function getTypeAndVersion( + provider: ContractProvider, +): Promise<{ type: string; version: string }> { + const result = await provider.get('typeAndVersion', []) + return { type: result.stack.readString(), version: result.stack.readString() } +} + +export async function getCode(provider: ContractProvider): Promise { + const result = await provider.get('code', []) + return result.stack.readCell() +} - async getCode(provider: ContractProvider): Promise { - const result = await provider.get('code', []) - return result.stack.readCell() - } +export async function getCodeHash(provider: ContractProvider): Promise { + const result = await provider.get('codeHash', []) + return result.stack.readBigNumber() +} - async getCodeHash(provider: ContractProvider): Promise { - const result = await provider.get('codeHash', []) - return result.stack.readBigNumber() - } +export interface TypeAndVersion { + getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> + getCode(provider: ContractProvider): Promise + getCodeHash(provider: ContractProvider): Promise } diff --git a/contracts/wrappers/libraries/upgrades/Upgradeable.ts b/contracts/wrappers/libraries/upgrades/Upgradeable.ts deleted file mode 100644 index cca2bf706..000000000 --- a/contracts/wrappers/libraries/upgrades/Upgradeable.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - Address, - beginCell, - Cell, - Contract, - ContractProvider, - Sender, - SendMode, - Slice, -} from '@ton/core' -import { SandboxContract, SendMessageResult } from '@ton/sandbox' -import { crc32 } from 'zlib' - -export const Opcodes = { - OP_UPGRADE: crc32('Upgradeable_Upgrade'), -} - -export class Upgradeable { - readonly address: Address - - async sendUpgrade( - provider: ContractProvider, - via: Sender, - opts: { - value: bigint - queryId?: number - code: Cell - }, - ) { - await provider.internal(via, { - value: opts.value, - sendMode: SendMode.PAY_GAS_SEPARATELY, - body: beginCell() - .storeUint(Opcodes.OP_UPGRADE, 32) - .storeUint(opts.queryId ?? 0, 64) - .storeRef(opts.code) - .endCell(), - }) - } - - code(): Promise { - throw new Error('Method not implemented.') - } -} - -export async function sendUpgradeAndReturnNewVersion( - current: SandboxContract, - via: Sender, - value: bigint, - newVersion: new (address: Address, init?: { code: Cell; data: Cell }) => T, - queryId?: number, -): Promise<{ upgradeResult: SendMessageResult; newVersionInstance: T }> { - const newVersionInstance = new newVersion(current.address) - const upgradeResult = await current.sendUpgrade(via, { - value: value, - queryId: queryId, - code: await newVersionInstance.code(), - }) - return { upgradeResult, newVersionInstance } -} - -export function loadUpgradedEvent(slice: Slice): { - version: string - code: Cell - codeHash: bigint -} { - const code = slice.loadRef() - const codeHash = slice.loadUintBig(256) - const version = slice.loadStringTail() - return { - version, - code, - codeHash, - } -} diff --git a/contracts/wrappers/libraries/versioning/Upgradeable.ts b/contracts/wrappers/libraries/versioning/Upgradeable.ts new file mode 100644 index 000000000..b23a2a3b3 --- /dev/null +++ b/contracts/wrappers/libraries/versioning/Upgradeable.ts @@ -0,0 +1,118 @@ +import { + Address, + beginCell, + Builder, + Cell, + Contract, + ContractProvider, + Sender, + SendMode, + Slice, +} from '@ton/core' +import { SandboxContract, SendMessageResult } from '@ton/sandbox' +import { CellCodec } from '../../utils' + +export const opcodes = { + Upgrade: 0x0aa811ed, +} + +export enum Error { + VersionMismatch = 28700, +} + +export const eventTopics = { + Upgraded: 0x6cf83c03, // crc32("Upgradeable_UpgradedEvent") +} + +export type Upgrade = { + queryId: bigint + code: Cell + fromVersion: string +} + +export type UpgradedEvent = { + code: Cell + codeHash: bigint + version: string +} + +export const builder = { + message: { + in: { + upgrade: ((): CellCodec => { + return { + encode: (msg: Upgrade): Builder => { + return beginCell() + .storeUint(opcodes.Upgrade, 32) + .storeUint(msg.queryId, 64) + .storeRef(msg.code) + .storeStringTail(msg.fromVersion) + }, + load: (src: Slice): Upgrade => { + src.skip(32) // opcode + return { + queryId: src.loadUintBig(64), + code: src.loadRef(), + fromVersion: src.loadStringTail(), + } + }, + } + })(), + }, + }, + event: { + upgraded: ((): CellCodec => { + return { + encode: (event: UpgradedEvent): Builder => { + return beginCell() + .storeRef(event.code) + .storeUint(event.codeHash, 256) + .storeStringTail(event.version) + }, + load: (src: Slice): UpgradedEvent => { + return { + code: src.loadRef(), + codeHash: src.loadUintBig(256), + version: src.loadStringTail(), + } + }, + } + })(), + }, +} + +export async function sendUpgrade( + provider: ContractProvider, + via: Sender, + value: bigint, + body: Upgrade, +) { + await provider.internal(via, { + value: value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: builder.message.in.upgrade.encode(body).endCell(), + }) +} +export interface Upgradeable extends Contract { + // readonly address: Address + + sendUpgrade(provider: ContractProvider, via: Sender, value: bigint, body: Upgrade): Promise +} + +export async function sendUpgradeAndReturnNewVersion( + current: SandboxContract, + via: Sender, + value: bigint, + newVersion: new (address: Address, init?: { code: Cell; data: Cell }) => T, + fromVersion: string, + newCode: Cell, + queryId?: bigint, +): Promise<{ upgradeResult: SendMessageResult; newVersionInstance: T }> { + const newVersionInstance = new newVersion(current.address) + const upgradeResult = await current.sendUpgrade(via, value, { + queryId: queryId ?? 0n, + fromVersion: fromVersion, + code: newCode, + }) + return { upgradeResult, newVersionInstance } +} diff --git a/docs/contracts/overview/libraries/upgradeable.md b/docs/contracts/overview/libraries/upgradeable.md index 6c0ea2e53..1d0d96b07 100644 --- a/docs/contracts/overview/libraries/upgradeable.md +++ b/docs/contracts/overview/libraries/upgradeable.md @@ -27,9 +27,12 @@ Handles `Upgradeable_Upgrade` message of type: struct (0x0aa811ed) Upgradeable_Upgrade { queryId: uint64; code: cell; + fromVersion: RemainingBitsAndRefs; } ``` +The `fromVersion` parameter ensures that upgrades are performed from the expected version, preventing accidental upgrades from intermediate or incorrect versions. + Emits an `UpgradedEvent` upon successful upgrade: ```tolk @@ -37,12 +40,22 @@ struct UpgradedEvent { /// The new code of the contract. code: cell; /// The SHA256 hash of the new code. - sha: uint256; + hash: uint256; /// The version of the contract after the upgrade. version: UnsafeBodyNoRef; } ``` +### Error Codes + +The module defines the following error codes: + +```tolk +enum Upgradeable_Error { + VersionMismatch = 43700; // Thrown when fromVersion doesn't match current version +} +``` + ### Requirements Required method implementations in your contract: @@ -175,6 +188,8 @@ get fun typeAndVersion(): (slice, slice) { ## Upgrade Flow +The upgrade process includes version verification to ensure upgrades are performed from the expected version: + ```mermaid --- config: @@ -187,14 +202,15 @@ sequenceDiagram Note over Counter: Initial state:
code: V1 (increment)
state: StorageV1 {
- id: uint32
- value: uint32
- ownable2Step: Ownable2Step
}
version: "1.0.0" - Owner->>Counter: send Upgradeable_Upgrade message
(with V2 code) + Owner->>Counter: send Upgradeable_Upgrade message
(with V2 code + fromVersion: "1.0.0") activate Counter Note over Counter: 1. Verify sender is owner
(requireUpgrade check) - Note over Counter: 2. Get current storage - Note over Counter: 3. Call migrateStorage(oldCell) - Note over Counter: 4. Replace contract code - Note over Counter: 5. Set new storage - Note over Counter: 6. Emit UpgradedEvent + Note over Counter: 2. Verify fromVersion matches current version
(throws VersionMismatch error if not) + Note over Counter: 3. Get current storage + Note over Counter: 4. Call migrateStorage(oldCell) + Note over Counter: 5. Replace contract code + Note over Counter: 6. Set new storage + Note over Counter: 7. Emit UpgradedEvent deactivate Counter Note over Counter: New state:
code: V2 (decrement)
state: StorageV2 {
- value: uint64
- id: uint32
- ownable2Step: Ownable2Step
}
version: "2.0.0" @@ -204,3 +220,177 @@ sequenceDiagram Note over Counter: Decrements counter
(value = n - 1) deactivate Counter ``` + +### Version Mismatch Protection + +The upgrade mechanism includes built-in protection against incorrect version upgrades: + +```typescript +// ✅ Correct: Upgrading from the current version +await contract.sendUpgrade(owner.getSender(), toNano('0.05'), { + queryId: 0n, + fromVersion: '1.0.0', // Matches current version + code: v2Code, +}) + +// ❌ Incorrect: Will fail with exit code 43700 (VersionMismatch) +await contract.sendUpgrade(owner.getSender(), toNano('0.05'), { + queryId: 0n, + fromVersion: '2.0.0', // Doesn't match current version "1.0.0" + code: v2Code, +}) +``` + +This prevents scenarios where: + +- A contract is upgraded from an unexpected intermediate version +- Multiple upgrade transactions are sent simultaneously +- An old upgrade transaction is replayed after a newer upgrade has completed + +## Testing Upgradeable Contracts + +The framework provides two reusable test specifications for upgradeable contracts: + +1. **`newUpgradeSpec`**: Tests the upgrade process from a previous version to a current version +2. **`newCurrentVersionSpec`**: Tests the current version's upgradeable interface without going through the upgrade process + +This separation allows you to test the upgrade path separately from the current version's behavior, avoiding unnecessary setup when you only need to test the current version. + +### Testing Upgrade Process + +Use `newUpgradeSpec` to test that upgrades work correctly from a previous version to the current version: + +```typescript +import { newUpgradeSpec } from '../../../wrappers/libraries/upgrades/UpgradeableSpec' +import { UpgradeableCounterV1 } from '../../../wrappers/examples/upgrades/UpgradeableCounterV1' +import { UpgradeableCounterV2 } from '../../../wrappers/examples/upgrades/UpgradeableCounterV2' + +describe('UpgradeableCounter - Upgrade Tests', () => { + const upgradeSpec = newUpgradeSpec( + { + contractType: UpgradeableCounterV1.type(), + prevVersion: UpgradeableCounterV1.version(), + currentVersion: UpgradeableCounterV2.version(), + getPrevCode: () => UpgradeableCounterV1.code(), + getCurrentCode: () => UpgradeableCounterV2.code(), + CurrentVersionConstructor: UpgradeableCounterV2, + upgradeValue: toNano('0.05'), // Optional: defaults to 0.05 TON + }, + async (blockchain, owner) => { + // Setup function: deploy your previous version contract + const code = await UpgradeableCounterV1.code() + const contract = blockchain.openContract( + UpgradeableCounterV1.createFromConfig( + { + id: 0, + value: 0, + ownable: { owner: owner.address, pendingOwner: null }, + }, + code, + ), + ) + const deployer = await blockchain.treasury('deployer') + await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + return contract + }, + ) + + upgradeSpec.run() +}) +``` + +#### Upgrade Test Coverage + +The upgrade test spec provides the following test cases: + +1. **should deploy on correct version**: Verifies that the previous version contract deploys with the correct version, type, code, and code hash +2. **should upgrade from previous to current version**: Tests the complete upgrade flow, including: + - Version verification before and after upgrade + - Code and code hash verification + - Upgrade event emission with correct version, code, and code hash +3. **should fail when fromVersion does not match current version**: Verifies that upgrades fail with exit code 43700 when `fromVersion` doesn't match the current version + +### Testing Current Version + +Use `newCurrentVersionSpec` to test the current version's upgradeable interface directly: + +```typescript +import { newCurrentVersionSpec } from '../../../wrappers/libraries/upgrades/UpgradeableSpec' +import { UpgradeableCounterV2 } from '../../../wrappers/examples/upgrades/UpgradeableCounterV2' + +describe('UpgradeableCounter - Current Version Tests', () => { + const currentVersionSpec = newCurrentVersionSpec( + { + contractType: UpgradeableCounterV2.type(), + currentVersion: UpgradeableCounterV2.version(), + getCurrentCode: () => UpgradeableCounterV2.code(), + CurrentVersionConstructor: UpgradeableCounterV2, + upgradeValue: toNano('0.05'), // Optional: defaults to 0.05 TON + }, + async (blockchain, owner) => { + // Setup function: deploy your current version contract directly + const code = await UpgradeableCounterV2.code() + const contract = blockchain.openContract( + UpgradeableCounterV2.createFromConfig( + { + id: 0, + value: 0, + ownable: { owner: owner.address, pendingOwner: null }, + }, + code, + ), + ) + const deployer = await blockchain.treasury('deployer') + await contract.sendDeploy(deployer.getSender(), toNano('0.05')) + return contract + }, + ) + + currentVersionSpec.run() + + // Add your contract-specific tests + it('should decrement counter', async () => { + // Your custom test logic + }) +}) +``` + +#### Current Version Test Coverage + +The current version test spec provides the following test cases: + +1. **should deploy on correct version**: Verifies that the contract deploys with the correct version, type, code, and code hash +2. **should fail when non-owner tries to upgrade**: Ensures that only the owner can perform upgrades + +### Configuration Options + +#### UpgradeTestConfig + +For `newUpgradeSpec`, accepts the following parameters: + +- `contractType`: The expected contract type name (e.g., from `YourContract.type()`) +- `prevVersion`: Version string for the previous version contract (e.g., from `YourContractV1.version()`) +- `currentVersion`: Version string for the current version contract (e.g., from `YourContractV2.version()`) +- `getPrevCode`: Function to get the code for the previous version contract +- `getCurrentCode`: Function to get the code for the current version contract +- `CurrentVersionConstructor`: Constructor class for the current version contract +- `upgradeValue` (optional): Amount of TON to use for upgrade transactions (defaults to 0.05 TON) + +#### CurrentVersionTestConfig + +For `newCurrentVersionSpec`, accepts the following parameters: + +- `contractType`: The expected contract type name (e.g., from `YourContract.type()`) +- `currentVersion`: Version string for the current version contract (e.g., from `YourContractV2.version()`) +- `getCurrentCode`: Function to get the code for the current version contract +- `CurrentVersionConstructor`: Constructor class for the current version contract +- `upgradeValue` (optional): Amount of TON to use for upgrade transactions (defaults to 0.05 TON) + +### Benefits + +- **Separation of Concerns**: Test upgrade process separately from current version behavior +- **Efficiency**: Skip upgrade setup when testing current version features +- **Consistency**: All upgradeable contracts are tested the same way +- **Maintainability**: Bug fixes and improvements to upgrade testing are automatically applied to all contracts +- **Focus**: Allows you to focus on testing contract-specific functionality +- **Type Safety**: Properly typed with `SandboxContract` for full TypeScript support