Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions solidity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,9 @@ npx hardhat upgrade --network sapphire-testnet --address 0xad3C76e4E621C0cfF7540
npx hardhat upgrade --network sapphire --address <accounting-proxy-address>
```

*Option 2:* With `SECRET_KEY` used to deploy an implementation only; the
owner is Safe account that will be signed and submitted afterwards:
*Option 2:* With `SECRET_KEY` used to deploy an implementation only. The
owner is Safe account that will sign and execute upgrade transactions
separately:

```shell
# Sapphire Testnet
Expand All @@ -179,6 +180,15 @@ npx hardhat upgrade --network sapphire-testnet --address 0xad3C76e4E621C0cfF7540
npx hardhat upgrade --network sapphire --address <accounting-proxy-address> --output-safe accounting-upgrade-safe.json
```

This will generate two Safe Transaction Builder JSON files:

1. the `proposeUpgrade()` transaction with suffix -1
2. the `upgradeToAndCall()` transaction with suffix -2

Both transactions need to be separately signed and submitted in order to upgrade
the Accounting contracts. Combining them inside a single batch is not possible
due to the simulation attack gated by `UPUPSUpgradeable`.

### Check status

You can view all public info of the contract by running:
Expand Down
12 changes: 9 additions & 3 deletions solidity/contracts/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "./Types.sol";
import {IAccountingSiweAuth} from "./interfaces/IAccountingSiweAuth.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {UPUPSUpgradeable} from "./lib/UPUPSUpgradeable.sol";

/**
* @title Accounting
Expand All @@ -25,7 +25,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U
* addresses derived on-chain from contract's secretKey. Fund locking, P2P transfers,
* and automated withdrawals via EIP-712 signatures.
*/
contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, OwnableUpgradeable, UUPSUpgradeable {
contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, OwnableUpgradeable, UPUPSUpgradeable {
/// @notice Contract version, bumped on each upgrade for tracking/verification.
uint64 public constant VERSION = 1;

Expand Down Expand Up @@ -142,12 +142,18 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, OwnableUpg
__Accounting_init(_roflAppID, _owner);
}

/**
* @notice Authorizes the upgrade proposal.
* @dev Required by UPUPSUpgradeable. Only the contract owner can propose the upgrade.
*/
function _authorizeProposeUpgrade() internal override onlyOwner {}

/**
* @notice Authorizes an upgrade to a new implementation.
* @dev Required by UUPSUpgradeable. Only the contract owner can upgrade.
* @param newImplementation Address of the new implementation contract
*/
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner acceptProposedUpgrade(newImplementation) {}
Comment thread
matevz marked this conversation as resolved.

/// @dev Ownership renunciation is disabled to prevent bricking the proxy.
function renounceOwnership() public pure override {
Expand Down
176 changes: 176 additions & 0 deletions solidity/contracts/lib/UPUPSUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

/**
* @title Universal Proposeable Upgradeable Proxy Standard (UPUPS)
* @notice Extends UUPSUpgradeable with a two-step upgrade process that prevents
* silent simulated upgrades that could extract confidential contract state.
* The upgrader must first call `proposeUpgrade` to announce the new
* implementation, and then call regular `upgradeToAndCall` in a later block.
*
* #### Example
*
* ```solidity
* contract MyContract is UPUPSUpgradeable, OwnableUpgradeable {
* function initialize(address _owner) public initializer {
* __Ownable_init(_owner);
* }
*
* // Gate proposeUpgrade with appropriate modifier.
* function _authorizeProposeUpgrade() internal onlyOwner { }
*
* // Gate UUPSUpgradeable.authorizeUpgrade with additional acceptProposedUpgrade() modifier.
* function _authorizeUpgrade(address newImpl) internal onlyOwner acceptProposedUpgrade(newImpl) { }
* }
* ```
*/
abstract contract UPUPSUpgradeable is UUPSUpgradeable {
/// @custom:storage-location erc7201:oasisprotocol.storage.UPUPSUpgradeable
struct UPUPSUpgradeableStorage {
address _newImplementation;
bytes32 _newImplementationHash;
uint256 _minBlockNumber;
}

// keccak256(abi.encode(uint256(keccak256("oasisprotocol.storage.UPUPSUpgradeable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant UPUPSUpgradeableStorageLocation =
0x2ba9668233a4827367587178d890a758d4583dfd9e5c9ef8b58defb278a6ad00;

function _getUPUPSUpgradeableStorage()
private
pure
returns (UPUPSUpgradeableStorage storage $)
{
assembly {
$.slot := UPUPSUpgradeableStorageLocation
}
}

error ImplementationDoesNotMatch();
error ImplementationHashDoesNotMatch();
error MinBlockNumberNotReached();
error MinBlockNumberInPast();
error NewImplementationNotAContract();

event UpgradeProposed(
address indexed newImplementation,
bytes32 indexed newImplementationHash,
uint256 indexed minBlockNumber
);

event UpgradeAccepted(
address indexed newImplementation,
bytes32 indexed newImplementationHash,
uint256 indexed minBlockNumber
);

/**
* @notice Initializes the contract.
*/
function __UPUPSUpgradeable_init()
internal
onlyInitializing
{
}

/**
* @notice Checks if the proposed upgrade is valid and clears the proposed
* upgrade bits.
*/
modifier acceptProposedUpgrade(address newImplementation) {
_acceptProposeUpgrade(newImplementation);

_;
}

/**
* @notice Function that should revert when `msg.sender` is not authorized
* to propose the upgrade. Use {proposedUpgradeImplementation} and
* {proposedUpgradeMinBlockNumber} to fetch current proposal details. Called
* by {proposeUpgrade}.
*
* Normally, this function will use an xref:access.adoc[access control]
* modifier such as {Ownable-onlyOwner}.
*
* ```solidity
* function _authorizeProposeUpgrade() internal onlyOwner {}
* ```
*/
function _authorizeProposeUpgrade() internal virtual;

/**
* @notice Returns the new implementation address of the proposed upgrade.
*/
function proposedUpgradeImplementation() public view virtual returns (address) {
UPUPSUpgradeableStorage storage $ = _getUPUPSUpgradeableStorage();
return $._newImplementation;
}

/**
* @notice Returns the hash of the implementation runtime code of the proposed upgrade.
*/
function proposedUpgradeImplementationHash() public view virtual returns (bytes32) {
UPUPSUpgradeableStorage storage $ = _getUPUPSUpgradeableStorage();
return $._newImplementationHash;
}

/**
* @notice Returns the proposed upgrade minimum block number.
*/
function proposedUpgradeMinBlockNumber() public view virtual returns (uint256) {
UPUPSUpgradeableStorage storage $ = _getUPUPSUpgradeableStorage();
return $._minBlockNumber;
}

/**
* @notice Reverts unless the implementations matches and the current block
* number is at least the minBlockNumber.
*/
function _acceptProposeUpgrade(address newImplementation) internal virtual {
if (newImplementation != proposedUpgradeImplementation()) {
revert ImplementationDoesNotMatch();
}
if (newImplementation.codehash != proposedUpgradeImplementationHash()) {
revert ImplementationHashDoesNotMatch();
}
if (block.number < proposedUpgradeMinBlockNumber()) {
revert MinBlockNumberNotReached();
}
emit UpgradeAccepted(newImplementation, newImplementation.codehash, proposedUpgradeMinBlockNumber());

UPUPSUpgradeableStorage storage $ = _getUPUPSUpgradeableStorage();
$._newImplementation = address(0);
$._minBlockNumber = 0;
}

/**
* @notice Propose the upgrade to the new implementation address after the
* given block number. If minBlockNumber is zero, take the number of the
* next block.
* @dev minBlockNumber is intentionally left to the caller's discretion:
* the function guards against the simulated upgrade attack, but a longer
* window may be provided for the new contract implementation review.
*/
function proposeUpgrade(address newImplementation, uint256 minBlockNumber) public virtual {
Comment thread
matevz marked this conversation as resolved.
if (newImplementation.code.length == 0) {
revert NewImplementationNotAContract();
}
if (minBlockNumber == 0) {
minBlockNumber = block.number+1;
}
if (minBlockNumber <= block.number) {
revert MinBlockNumberInPast();
}

UPUPSUpgradeableStorage storage $ = _getUPUPSUpgradeableStorage();
$._newImplementation = newImplementation;
$._newImplementationHash = newImplementation.codehash;
$._minBlockNumber = minBlockNumber;

_authorizeProposeUpgrade();

emit UpgradeProposed(newImplementation, newImplementation.codehash, minBlockNumber);
}
}
75 changes: 39 additions & 36 deletions solidity/tasks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from "path";
import '@nomicfoundation/hardhat-ethers';
import '@oasisprotocol/sapphire-hardhat';
import '@typechain/hardhat';
import {JsonRpcProvider} from "ethers";
import {JsonRpcProvider, TransactionResponse} from "ethers";
import { task } from "hardhat/config";
import {HardhatEthersSigner} from "@nomicfoundation/hardhat-ethers/signers";
import {HardhatRuntimeEnvironment} from "hardhat/types";
Expand Down Expand Up @@ -207,7 +207,7 @@ task("upgrade")
await hre.run("compile");

const Accounting = await hre.ethers.getContractFactory("Accounting", await getUwDeployer(hre));
const current = await hre.ethers.getContractAt("Accounting", args.address);
const current = await hre.ethers.getContractAt("Accounting", args.address, await getUwDeployer(hre));
let siweAuthAddress: string = args.siweauth;

if (!siweAuthAddress) {
Expand Down Expand Up @@ -248,35 +248,16 @@ task("upgrade")
constructorArgs: [siweAuthAddress],
});

let newImplAddress: string
if (!args.outputSafe) {
const upgraded = await hre.upgrades.upgradeProxy(args.address, Accounting, {
kind: 'uups',
constructorArgs: [siweAuthAddress],
redeployImplementation: 'always',
txOverrides: { gasLimit: 15000000 }
});
// await upgraded.waitForDeployment(); doesn't work for unwrapped providers.
// Extract the upgrade tx and wait for it directly.
const upgradeTx = (upgraded as unknown as { deployTransaction?: { wait: () => Promise<unknown> } }).deployTransaction;
await upgradeTx!.wait();

newImplAddress = await hre.upgrades.erc1967.getImplementationAddress(args.address);
console.log(`Upgraded! New implementation: ${newImplAddress}`);

if (currentImpl === newImplAddress) {
console.log(`Warning: Implementation address unchanged. Upgrade may have been a no-op.`);
}
} else {
newImplAddress = await hre.upgrades.prepareUpgrade(args.address, Accounting, {
kind: 'uups',
constructorArgs: [siweAuthAddress],
redeployImplementation: 'always',
txOverrides: { gasLimit: 15000000 }
}) as string;

console.log(`Deployed new proposed implementation: ${newImplAddress}`);
}
const deployTx = await hre.upgrades.prepareUpgrade(args.address, Accounting, {
Comment thread
matevz marked this conversation as resolved.
kind: 'uups',
constructorArgs: [siweAuthAddress],
redeployImplementation: 'always',
txOverrides: { gasLimit: 15000000 },
getTxResponse: true,
}) as TransactionResponse;
const deployReceipt = await deployTx.wait();
const newImplAddress = deployReceipt!.contractAddress!;
console.log(`Deployed new proposed implementation: ${newImplAddress} (tx: ${deployTx.hash})`);

try {
await hre.run("verify:sourcify", { address: newImplAddress, contract: "Accounting" });
Expand All @@ -293,17 +274,39 @@ task("upgrade")
);
}

if (args.outputSafe) {
const data = Accounting.interface.encodeFunctionData("upgradeToAndCall", [newImplAddress, "0x"]);
if (!args.outputSafe) {
const txProposeUpgrade = await (await current.proposeUpgrade(newImplAddress, 0)).wait();
Comment thread
matevz marked this conversation as resolved.
console.log(`Proposed upgrade to ${newImplAddress}. (tx: ${txProposeUpgrade?.hash})`);

const txUpgradeAndCall = await (await current.upgradeToAndCall(newImplAddress, "0x")).wait();
console.log(`Upgraded! New implementation: ${newImplAddress}. (tx: ${txUpgradeAndCall?.hash})`);

const checkImplAddress = await hre.upgrades.erc1967.getImplementationAddress(args.address);
if (checkImplAddress === currentImpl) {
console.log(`Warning: Implementation address unchanged. Upgrade may have been a no-op.`);
}
} else {
const dataProposeUpgrade = Accounting.interface.encodeFunctionData("proposeUpgrade", [newImplAddress, 0]);
const jsonProposeUpgrade = await createSafeJson(
args.address,
dataProposeUpgrade,
"Propose Upgrade of Accounting",
`Propose Upgrade of Accounting contract ${args.address} to implementation ${newImplAddress}`,
(await hre.ethers.provider.getNetwork()).chainId.toString()
);
writeFileSync(args.outputSafe+"-1", jsonProposeUpgrade);

const dataUpgradeToAndCall = Accounting.interface.encodeFunctionData("upgradeToAndCall", [newImplAddress, "0x"]);
const json = await createSafeJson(
args.address,
data,
dataUpgradeToAndCall,
"Upgrade Accounting",
`Upgrade Accounting contract ${args.address} to implementation ${newImplAddress}`,
(await hre.ethers.provider.getNetwork()).chainId.toString()
);
writeFileSync(args.outputSafe, json);
console.log(`Safe Transaction Builder JSON written to ${args.outputSafe}`);
writeFileSync(args.outputSafe+"-2", json);
Comment thread
matevz marked this conversation as resolved.

console.log(`Two Safe Transaction Builder JSON batches written to ${args.outputSafe}-1 and ${args.outputSafe}-2. Execute them separately.`);
}
});

Expand Down
Loading
Loading