TransparentUpgradeableProxy.behaviour.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { impersonate } = require('../../helpers/account');
  4. const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/storage');
  5. // createProxy, initialOwner, accounts
  6. module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
  7. before(async function () {
  8. const implementationV0 = await ethers.deployContract('DummyImplementation');
  9. const implementationV1 = await ethers.deployContract('DummyImplementation');
  10. const createProxyWithImpersonatedProxyAdmin = async (logic, initData, opts = undefined) => {
  11. const [proxy, tx] = await this.createProxy(logic, initData, opts).then(instance =>
  12. Promise.all([ethers.getContractAt('ITransparentUpgradeableProxy', instance), instance.deploymentTransaction()]),
  13. );
  14. const proxyAdmin = await ethers.getContractAt(
  15. 'ProxyAdmin',
  16. ethers.getCreateAddress({ from: proxy.target, nonce: 1n }),
  17. );
  18. const proxyAdminAsSigner = await proxyAdmin.getAddress().then(impersonate);
  19. return {
  20. instance: logic.attach(proxy.target), // attaching proxy directly works well for everything except for event resolution
  21. proxy,
  22. proxyAdmin,
  23. proxyAdminAsSigner,
  24. tx,
  25. };
  26. };
  27. Object.assign(this, {
  28. implementationV0,
  29. implementationV1,
  30. createProxyWithImpersonatedProxyAdmin,
  31. });
  32. });
  33. beforeEach(async function () {
  34. Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.implementationV0, '0x'));
  35. });
  36. describe('implementation', function () {
  37. it('returns the current implementation address', async function () {
  38. expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementationV0);
  39. });
  40. it('delegates to the implementation', async function () {
  41. expect(await this.instance.get()).to.be.true;
  42. });
  43. });
  44. describe('proxy admin', function () {
  45. it('emits AdminChanged event during construction', async function () {
  46. await expect(this.tx).to.emit(this.proxy, 'AdminChanged').withArgs(ethers.ZeroAddress, this.proxyAdmin);
  47. });
  48. it('sets the proxy admin in storage with the correct initial owner', async function () {
  49. expect(await getAddressInSlot(this.proxy, AdminSlot)).to.equal(this.proxyAdmin);
  50. expect(await this.proxyAdmin.owner()).to.equal(this.owner);
  51. });
  52. it('can overwrite the admin by the implementation', async function () {
  53. await this.instance.unsafeOverrideAdmin(this.other);
  54. const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot);
  55. expect(ERC1967AdminSlotValue).to.equal(this.other);
  56. expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdmin);
  57. // Still allows previous admin to execute admin operations
  58. await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.implementationV1, '0x'))
  59. .to.emit(this.proxy, 'Upgraded')
  60. .withArgs(this.implementationV1);
  61. });
  62. });
  63. describe('upgradeToAndCall', function () {
  64. describe('without migrations', function () {
  65. beforeEach(async function () {
  66. this.behavior = await ethers.deployContract('InitializableMock');
  67. });
  68. describe('when the call does not fail', function () {
  69. beforeEach(function () {
  70. this.initializeData = this.behavior.interface.encodeFunctionData('initializeWithX', [42n]);
  71. });
  72. describe('when the sender is the admin', function () {
  73. const value = 10n ** 5n;
  74. beforeEach(async function () {
  75. this.tx = await this.proxy
  76. .connect(this.proxyAdminAsSigner)
  77. .upgradeToAndCall(this.behavior, this.initializeData, {
  78. value,
  79. });
  80. });
  81. it('upgrades to the requested implementation', async function () {
  82. expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behavior);
  83. });
  84. it('emits an event', async function () {
  85. await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behavior);
  86. });
  87. it('calls the initializer function', async function () {
  88. expect(await this.behavior.attach(this.proxy).x()).to.equal(42n);
  89. });
  90. it('sends given value to the proxy', async function () {
  91. expect(await ethers.provider.getBalance(this.proxy)).to.equal(value);
  92. });
  93. it('uses the storage of the proxy', async function () {
  94. // storage layout should look as follows:
  95. // - 0: Initializable storage ++ initializerRan ++ onlyInitializingRan
  96. // - 1: x
  97. expect(await ethers.provider.getStorage(this.proxy, 1n)).to.equal(42n);
  98. });
  99. });
  100. describe('when the sender is not the admin', function () {
  101. it('reverts', async function () {
  102. await expect(this.proxy.connect(this.other).upgradeToAndCall(this.behavior, this.initializeData)).to.be
  103. .reverted;
  104. });
  105. });
  106. });
  107. describe('when the call does fail', function () {
  108. beforeEach(function () {
  109. this.initializeData = this.behavior.interface.encodeFunctionData('fail');
  110. });
  111. it('reverts', async function () {
  112. await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.behavior, this.initializeData))
  113. .to.be.reverted;
  114. });
  115. });
  116. });
  117. describe('with migrations', function () {
  118. describe('when the sender is the admin', function () {
  119. const value = 10n ** 5n;
  120. describe('when upgrading to V1', function () {
  121. beforeEach(async function () {
  122. this.behaviorV1 = await ethers.deployContract('MigratableMockV1');
  123. const v1MigrationData = this.behaviorV1.interface.encodeFunctionData('initialize', [42n]);
  124. this.balancePreviousV1 = await ethers.provider.getBalance(this.proxy);
  125. this.tx = await this.proxy
  126. .connect(this.proxyAdminAsSigner)
  127. .upgradeToAndCall(this.behaviorV1, v1MigrationData, {
  128. value,
  129. });
  130. });
  131. it('upgrades to the requested version and emits an event', async function () {
  132. expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV1);
  133. await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV1);
  134. });
  135. it("calls the 'initialize' function and sends given value to the proxy", async function () {
  136. expect(await this.behaviorV1.attach(this.proxy).x()).to.equal(42n);
  137. expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV1 + value);
  138. });
  139. describe('when upgrading to V2', function () {
  140. beforeEach(async function () {
  141. this.behaviorV2 = await ethers.deployContract('MigratableMockV2');
  142. const v2MigrationData = this.behaviorV2.interface.encodeFunctionData('migrate', [10n, 42n]);
  143. this.balancePreviousV2 = await ethers.provider.getBalance(this.proxy);
  144. this.tx = await this.proxy
  145. .connect(this.proxyAdminAsSigner)
  146. .upgradeToAndCall(this.behaviorV2, v2MigrationData, {
  147. value,
  148. });
  149. });
  150. it('upgrades to the requested version and emits an event', async function () {
  151. expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV2);
  152. await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV2);
  153. });
  154. it("calls the 'migrate' function and sends given value to the proxy", async function () {
  155. expect(await this.behaviorV2.attach(this.proxy).x()).to.equal(10n);
  156. expect(await this.behaviorV2.attach(this.proxy).y()).to.equal(42n);
  157. expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV2 + value);
  158. });
  159. describe('when upgrading to V3', function () {
  160. beforeEach(async function () {
  161. this.behaviorV3 = await ethers.deployContract('MigratableMockV3');
  162. const v3MigrationData = this.behaviorV3.interface.encodeFunctionData('migrate()');
  163. this.balancePreviousV3 = await ethers.provider.getBalance(this.proxy);
  164. this.tx = await this.proxy
  165. .connect(this.proxyAdminAsSigner)
  166. .upgradeToAndCall(this.behaviorV3, v3MigrationData, {
  167. value,
  168. });
  169. });
  170. it('upgrades to the requested version and emits an event', async function () {
  171. expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV3);
  172. await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV3);
  173. });
  174. it("calls the 'migrate' function and sends given value to the proxy", async function () {
  175. expect(await this.behaviorV3.attach(this.proxy).x()).to.equal(42n);
  176. expect(await this.behaviorV3.attach(this.proxy).y()).to.equal(10n);
  177. expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV3 + value);
  178. });
  179. });
  180. });
  181. });
  182. });
  183. describe('when the sender is not the admin', function () {
  184. it('reverts', async function () {
  185. const behaviorV1 = await ethers.deployContract('MigratableMockV1');
  186. const v1MigrationData = behaviorV1.interface.encodeFunctionData('initialize', [42n]);
  187. await expect(this.proxy.connect(this.other).upgradeToAndCall(behaviorV1, v1MigrationData)).to.be.reverted;
  188. });
  189. });
  190. });
  191. });
  192. describe('transparent proxy', function () {
  193. beforeEach('creating proxy', async function () {
  194. this.clashingImplV0 = await ethers.deployContract('ClashingImplementation');
  195. this.clashingImplV1 = await ethers.deployContract('ClashingImplementation');
  196. Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.clashingImplV0, '0x'));
  197. });
  198. it('proxy admin cannot call delegated functions', async function () {
  199. const factory = await ethers.getContractFactory('TransparentUpgradeableProxy');
  200. await expect(this.instance.connect(this.proxyAdminAsSigner).delegatedFunction()).to.be.revertedWithCustomError(
  201. factory,
  202. 'ProxyDeniedAdminAccess',
  203. );
  204. });
  205. describe('when function names clash', function () {
  206. it('executes the proxy function if the sender is the admin', async function () {
  207. await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.clashingImplV1, '0x'))
  208. .to.emit(this.proxy, 'Upgraded')
  209. .withArgs(this.clashingImplV1);
  210. });
  211. it('delegates the call to implementation when sender is not the admin', async function () {
  212. await expect(this.proxy.connect(this.other).upgradeToAndCall(this.clashingImplV1, '0x'))
  213. .to.emit(this.instance, 'ClashingImplementationCall')
  214. .to.not.emit(this.proxy, 'Upgraded');
  215. });
  216. });
  217. });
  218. describe('regression', function () {
  219. const initializeData = '0x';
  220. it('should add new function', async function () {
  221. const impl1 = await ethers.deployContract('Implementation1');
  222. const impl2 = await ethers.deployContract('Implementation2');
  223. const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
  224. impl1,
  225. initializeData,
  226. );
  227. await instance.setValue(42n);
  228. // `getValue` is not available in impl1
  229. await expect(impl2.attach(instance).getValue()).to.be.reverted;
  230. // do upgrade
  231. await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');
  232. // `getValue` is available in impl2
  233. expect(await impl2.attach(instance).getValue()).to.equal(42n);
  234. });
  235. it('should remove function', async function () {
  236. const impl1 = await ethers.deployContract('Implementation1');
  237. const impl2 = await ethers.deployContract('Implementation2');
  238. const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
  239. impl2,
  240. initializeData,
  241. );
  242. await instance.setValue(42n);
  243. // `getValue` is available in impl2
  244. expect(await impl2.attach(instance).getValue()).to.equal(42n);
  245. // do downgrade
  246. await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl1, '0x');
  247. // `getValue` is not available in impl1
  248. await expect(impl2.attach(instance).getValue()).to.be.reverted;
  249. });
  250. it('should change function signature', async function () {
  251. const impl1 = await ethers.deployContract('Implementation1');
  252. const impl3 = await ethers.deployContract('Implementation3');
  253. const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
  254. impl1,
  255. initializeData,
  256. );
  257. await instance.setValue(42n);
  258. await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl3, '0x');
  259. expect(await impl3.attach(instance).getValue(8n)).to.equal(50n);
  260. });
  261. it('should add fallback function', async function () {
  262. const impl1 = await ethers.deployContract('Implementation1');
  263. const impl4 = await ethers.deployContract('Implementation4');
  264. const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
  265. impl1,
  266. initializeData,
  267. );
  268. await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl4, '0x');
  269. await this.other.sendTransaction({ to: proxy });
  270. expect(await impl4.attach(instance).getValue()).to.equal(1n);
  271. });
  272. it('should remove fallback function', async function () {
  273. const impl2 = await ethers.deployContract('Implementation2');
  274. const impl4 = await ethers.deployContract('Implementation4');
  275. const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
  276. impl4,
  277. initializeData,
  278. );
  279. await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');
  280. await expect(this.other.sendTransaction({ to: proxy })).to.be.reverted;
  281. expect(await impl2.attach(instance).getValue()).to.equal(0n);
  282. });
  283. });
  284. };