Skip to content

Swan upgrade documentation and implementation deployment script #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 15, 2025
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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ The chain ID is 8453 for Base Mainnet, and 84532 for Base Sepolia.

### Upgrade Contract

#### Using single-sig wallet
Upgrading an existing contract is done as per the instructions in [openzeppelin-foundry-upgrades](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades) repository.

First, we create a new contract with its name as `ContractNameV2`, and then we execute the following command:
Expand All @@ -186,6 +187,46 @@ forge script ./script/Deploy.s.sol:Upgrade<CONTRACT_NAME> \
> cast wallet address --account <WALLET_NAME>
> ```

#### Using multisig wallet (Our currrent approach with Gnosis Safe multisig)

To upgrade your Swan UUPS contract via a Gnosis multisig, follow these steps:

1. **Deploy the new implementation contract**
Execute the deployment script to get the new implementation address:

```sh
forge script ./script/Deploy.s.sol:DeploySwanImpl \
--rpc-url <RPC_URL> \
--account <WALLET_NAME> --broadcast \
--sender <WALLET_ADDRESS> \
--verify --verifier blockscout \
--verifier-url <VERIFIER_URL>
```

2. **Generate upgrade calldata**

Once you have the new implementation address, generate the calldata for the Gnosis multisig:

```sh
cast calldata "upgradeToAndCall(address,bytes)" 0xNewImplementationAddress 0x
```

This will output something like:
```
0x4f1ef28600000000000000000000000017b6d1eddcd5f9ca19bb2ffed2f3deb6bd74bd2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000
```

3. **Submit transaction to Gnosis Safe**

Create a new transaction in the Gnosis Safe interface with:
- **To**: Your Swan proxy address
- **Value**: 0 ETH
- **Data**: The calldata generated in step 2

4. **Execute the transaction**

Have the required signers approve the transaction, then execute it to complete the upgrade.

## Testing & Diagnostics

Run tests on local network:
Expand Down
9 changes: 9 additions & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ contract DeploySwan is Script {
}
}

contract DeploySwanImpl is Script {
HelperConfig public config;

function run() external returns (address addr) {
config = new HelperConfig();
addr = config.deploySwanImpl();
}
}

contract DeploySwanLottery is Script {
HelperConfig public config;

Expand Down
9 changes: 9 additions & 0 deletions script/HelperConfig.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ contract HelperConfig is Script {
return (swanProxy, swanImplementation);
}

function deploySwanImpl() external returns (address impl) {
vm.startBroadcast();
Swan newImplementation = new Swan();
vm.stopBroadcast();

// console.log("New implementation address:", address(newImplementation));
return address(newImplementation);
}

function deploySwanLottery() external returns (address) {
// read Swan proxy address from deployments file
string memory dir = "deployments/";
Expand Down
123 changes: 123 additions & 0 deletions test/SwanUpgradeTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test, console} from "forge-std/Test.sol";
import {Swan} from "../src/Swan.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {SwanManager, SwanMarketParameters} from "../src/SwanManager.sol";
import {LLMOracleTaskParameters} from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol";
import {MockERC20} from "./mock/MockERC20.sol";
import {MockLLMOracleCoordinator} from "./mock/MockLLMOracleCoordinator.sol";
import {MockSwanAgentFactory} from "./mock/MockSwanAgentFactory.sol";
import {MockSwanArtifactFactory} from "./mock/MockSwanArtifactFactory.sol";

contract SwanUpgradeTest is Test {
Swan swanImplementationV1;
Swan proxy;

MockERC20 token;
MockLLMOracleCoordinator coordinator;
MockSwanAgentFactory agentFactory;
MockSwanArtifactFactory artifactFactory;

address owner = address(0x1);
address user = address(0x2);

function setUp() public {
// Deploy mock dependencies
token = new MockERC20("Test Token", "TEST");
coordinator = new MockLLMOracleCoordinator();
agentFactory = new MockSwanAgentFactory();
artifactFactory = new MockSwanArtifactFactory();

vm.startPrank(owner);

// Deploy the original implementation
swanImplementationV1 = new Swan();

SwanMarketParameters memory marketParams = SwanMarketParameters({
withdrawInterval: 1 days,
listingInterval: 2 days,
buyInterval: 3 days,
platformFee: 10,
maxArtifactCount: 10,
minArtifactPrice: 1 ether,
timestamp: block.timestamp,
maxAgentFee: 20
});

LLMOracleTaskParameters memory oracleParams =
LLMOracleTaskParameters({difficulty: 1, numGenerations: 1, numValidations: 1});

bytes memory initData = abi.encodeCall(
Swan.initialize,
(
marketParams,
oracleParams,
address(coordinator),
address(token),
address(agentFactory),
address(artifactFactory)
)
);

// Deploy the proxy with the implementation and initialization data
ERC1967Proxy proxyContract = new ERC1967Proxy(address(swanImplementationV1), initData);

// Cast the proxy to Swan for easier interaction
proxy = Swan(address(proxyContract));

vm.stopPrank();

// Verify initial setup
assertEq(proxy.owner(), owner);
}

function testUpgrade() public {
// Pre-upgrade checks
assertEq(address(proxy.agentFactory()), address(agentFactory));
assertEq(address(proxy.artifactFactory()), address(artifactFactory));

// Deploy the new implementation
vm.startPrank(owner);
Swan swanImplementationV2 = new Swan();

proxy.upgradeToAndCall(address(swanImplementationV2), "");
vm.stopPrank();

// Get implementation address
bytes32 implementationSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 implementationValue = vm.load(address(proxy), implementationSlot);
address currentImplementation = address(uint160(uint256(implementationValue)));

assertEq(currentImplementation, address(swanImplementationV2));

assertEq(address(proxy.agentFactory()), address(agentFactory));
assertEq(address(proxy.artifactFactory()), address(artifactFactory));

vm.startPrank(owner);
address newAgentFactory = address(new MockSwanAgentFactory());
address newArtifactFactory = address(new MockSwanArtifactFactory());
proxy.setFactories(newAgentFactory, newArtifactFactory);
vm.stopPrank();

// Verify the new factories were set correctly
assertEq(address(proxy.agentFactory()), newAgentFactory);
assertEq(address(proxy.artifactFactory()), newArtifactFactory);
}

// Test that the upgrade fails if called by a non-owner
function testUpgradeFailsWhenCalledByNonOwner() public {
vm.startPrank(owner);
Swan swanImplementationV2 = new Swan();
vm.stopPrank();

// Try to upgrade from a non-owner account
vm.startPrank(user);
vm.expectRevert();
proxy.upgradeToAndCall(address(swanImplementationV2), "");
vm.stopPrank();
}
}
14 changes: 14 additions & 0 deletions test/mock/MockERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1000000 * 10 ** 18);
}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
6 changes: 6 additions & 0 deletions test/mock/MockLLMOracleCoordinator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {LLMOracleCoordinator} from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol";

contract MockLLMOracleCoordinator {}
17 changes: 17 additions & 0 deletions test/mock/MockSwanAgentFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {SwanAgentFactory, SwanAgent} from "../../src/SwanAgent.sol";

contract MockSwanAgentFactory {
function deploy(
string calldata _name,
string calldata _description,
uint96 _listingFee,
uint256 _amountPerRound,
address _owner
) external returns (SwanAgent) {
// Return a mock agent
return SwanAgent(address(0));
}
}
14 changes: 14 additions & 0 deletions test/mock/MockSwanArtifactFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {SwanArtifactFactory, SwanArtifact} from "../../src/SwanArtifact.sol";

contract MockSwanArtifactFactory {
function deploy(string calldata _name, string calldata _symbol, bytes calldata _desc, address _owner)
external
returns (SwanArtifact)
{
// Return a mock artifact
return SwanArtifact(address(0));
}
}
Loading