Skip to content

Commit 746d9da

Browse files
authored
Swan upgrade documentation and implementation deployment script (#27)
* Update README.md * Update README.md * Add scripts for deploy swan impl + test * Clean up comments
1 parent a685a04 commit 746d9da

8 files changed

+233
-0
lines changed

README.md

+41
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ The chain ID is 8453 for Base Mainnet, and 84532 for Base Sepolia.
165165

166166
### Upgrade Contract
167167

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

170171
First, we create a new contract with its name as `ContractNameV2`, and then we execute the following command:
@@ -186,6 +187,46 @@ forge script ./script/Deploy.s.sol:Upgrade<CONTRACT_NAME> \
186187
> cast wallet address --account <WALLET_NAME>
187188
> ```
188189
190+
#### Using multisig wallet (Our currrent approach with Gnosis Safe multisig)
191+
192+
To upgrade your Swan UUPS contract via a Gnosis multisig, follow these steps:
193+
194+
1. **Deploy the new implementation contract**
195+
Execute the deployment script to get the new implementation address:
196+
197+
```sh
198+
forge script ./script/Deploy.s.sol:DeploySwanImpl \
199+
--rpc-url <RPC_URL> \
200+
--account <WALLET_NAME> --broadcast \
201+
--sender <WALLET_ADDRESS> \
202+
--verify --verifier blockscout \
203+
--verifier-url <VERIFIER_URL>
204+
```
205+
206+
2. **Generate upgrade calldata**
207+
208+
Once you have the new implementation address, generate the calldata for the Gnosis multisig:
209+
210+
```sh
211+
cast calldata "upgradeToAndCall(address,bytes)" 0xNewImplementationAddress 0x
212+
```
213+
214+
This will output something like:
215+
```
216+
0x4f1ef28600000000000000000000000017b6d1eddcd5f9ca19bb2ffed2f3deb6bd74bd2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000
217+
```
218+
219+
3. **Submit transaction to Gnosis Safe**
220+
221+
Create a new transaction in the Gnosis Safe interface with:
222+
- **To**: Your Swan proxy address
223+
- **Value**: 0 ETH
224+
- **Data**: The calldata generated in step 2
225+
226+
4. **Execute the transaction**
227+
228+
Have the required signers approve the transaction, then execute it to complete the upgrade.
229+
189230
## Testing & Diagnostics
190231

191232
Run tests on local network:

script/Deploy.s.sol

+9
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ contract DeploySwan is Script {
6262
}
6363
}
6464

65+
contract DeploySwanImpl is Script {
66+
HelperConfig public config;
67+
68+
function run() external returns (address addr) {
69+
config = new HelperConfig();
70+
addr = config.deploySwanImpl();
71+
}
72+
}
73+
6574
contract DeploySwanLottery is Script {
6675
HelperConfig public config;
6776

script/HelperConfig.s.sol

+9
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ contract HelperConfig is Script {
199199
return (swanProxy, swanImplementation);
200200
}
201201

202+
function deploySwanImpl() external returns (address impl) {
203+
vm.startBroadcast();
204+
Swan newImplementation = new Swan();
205+
vm.stopBroadcast();
206+
207+
// console.log("New implementation address:", address(newImplementation));
208+
return address(newImplementation);
209+
}
210+
202211
function deploySwanLottery() external returns (address) {
203212
// read Swan proxy address from deployments file
204213
string memory dir = "deployments/";

test/SwanUpgradeTest.t.sol

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Test, console} from "forge-std/Test.sol";
5+
import {Swan} from "../src/Swan.sol";
6+
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
7+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
8+
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
9+
import {SwanManager, SwanMarketParameters} from "../src/SwanManager.sol";
10+
import {LLMOracleTaskParameters} from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol";
11+
import {MockERC20} from "./mock/MockERC20.sol";
12+
import {MockLLMOracleCoordinator} from "./mock/MockLLMOracleCoordinator.sol";
13+
import {MockSwanAgentFactory} from "./mock/MockSwanAgentFactory.sol";
14+
import {MockSwanArtifactFactory} from "./mock/MockSwanArtifactFactory.sol";
15+
16+
contract SwanUpgradeTest is Test {
17+
Swan swanImplementationV1;
18+
Swan proxy;
19+
20+
MockERC20 token;
21+
MockLLMOracleCoordinator coordinator;
22+
MockSwanAgentFactory agentFactory;
23+
MockSwanArtifactFactory artifactFactory;
24+
25+
address owner = address(0x1);
26+
address user = address(0x2);
27+
28+
function setUp() public {
29+
// Deploy mock dependencies
30+
token = new MockERC20("Test Token", "TEST");
31+
coordinator = new MockLLMOracleCoordinator();
32+
agentFactory = new MockSwanAgentFactory();
33+
artifactFactory = new MockSwanArtifactFactory();
34+
35+
vm.startPrank(owner);
36+
37+
// Deploy the original implementation
38+
swanImplementationV1 = new Swan();
39+
40+
SwanMarketParameters memory marketParams = SwanMarketParameters({
41+
withdrawInterval: 1 days,
42+
listingInterval: 2 days,
43+
buyInterval: 3 days,
44+
platformFee: 10,
45+
maxArtifactCount: 10,
46+
minArtifactPrice: 1 ether,
47+
timestamp: block.timestamp,
48+
maxAgentFee: 20
49+
});
50+
51+
LLMOracleTaskParameters memory oracleParams =
52+
LLMOracleTaskParameters({difficulty: 1, numGenerations: 1, numValidations: 1});
53+
54+
bytes memory initData = abi.encodeCall(
55+
Swan.initialize,
56+
(
57+
marketParams,
58+
oracleParams,
59+
address(coordinator),
60+
address(token),
61+
address(agentFactory),
62+
address(artifactFactory)
63+
)
64+
);
65+
66+
// Deploy the proxy with the implementation and initialization data
67+
ERC1967Proxy proxyContract = new ERC1967Proxy(address(swanImplementationV1), initData);
68+
69+
// Cast the proxy to Swan for easier interaction
70+
proxy = Swan(address(proxyContract));
71+
72+
vm.stopPrank();
73+
74+
// Verify initial setup
75+
assertEq(proxy.owner(), owner);
76+
}
77+
78+
function testUpgrade() public {
79+
// Pre-upgrade checks
80+
assertEq(address(proxy.agentFactory()), address(agentFactory));
81+
assertEq(address(proxy.artifactFactory()), address(artifactFactory));
82+
83+
// Deploy the new implementation
84+
vm.startPrank(owner);
85+
Swan swanImplementationV2 = new Swan();
86+
87+
proxy.upgradeToAndCall(address(swanImplementationV2), "");
88+
vm.stopPrank();
89+
90+
// Get implementation address
91+
bytes32 implementationSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
92+
bytes32 implementationValue = vm.load(address(proxy), implementationSlot);
93+
address currentImplementation = address(uint160(uint256(implementationValue)));
94+
95+
assertEq(currentImplementation, address(swanImplementationV2));
96+
97+
assertEq(address(proxy.agentFactory()), address(agentFactory));
98+
assertEq(address(proxy.artifactFactory()), address(artifactFactory));
99+
100+
vm.startPrank(owner);
101+
address newAgentFactory = address(new MockSwanAgentFactory());
102+
address newArtifactFactory = address(new MockSwanArtifactFactory());
103+
proxy.setFactories(newAgentFactory, newArtifactFactory);
104+
vm.stopPrank();
105+
106+
// Verify the new factories were set correctly
107+
assertEq(address(proxy.agentFactory()), newAgentFactory);
108+
assertEq(address(proxy.artifactFactory()), newArtifactFactory);
109+
}
110+
111+
// Test that the upgrade fails if called by a non-owner
112+
function testUpgradeFailsWhenCalledByNonOwner() public {
113+
vm.startPrank(owner);
114+
Swan swanImplementationV2 = new Swan();
115+
vm.stopPrank();
116+
117+
// Try to upgrade from a non-owner account
118+
vm.startPrank(user);
119+
vm.expectRevert();
120+
proxy.upgradeToAndCall(address(swanImplementationV2), "");
121+
vm.stopPrank();
122+
}
123+
}

test/mock/MockERC20.sol

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract MockERC20 is ERC20 {
7+
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
8+
_mint(msg.sender, 1000000 * 10 ** 18);
9+
}
10+
11+
function mint(address to, uint256 amount) public {
12+
_mint(to, amount);
13+
}
14+
}
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {LLMOracleCoordinator} from "@firstbatch/dria-oracle-contracts/LLMOracleCoordinator.sol";
5+
6+
contract MockLLMOracleCoordinator {}

test/mock/MockSwanAgentFactory.sol

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {SwanAgentFactory, SwanAgent} from "../../src/SwanAgent.sol";
5+
6+
contract MockSwanAgentFactory {
7+
function deploy(
8+
string calldata _name,
9+
string calldata _description,
10+
uint96 _listingFee,
11+
uint256 _amountPerRound,
12+
address _owner
13+
) external returns (SwanAgent) {
14+
// Return a mock agent
15+
return SwanAgent(address(0));
16+
}
17+
}

test/mock/MockSwanArtifactFactory.sol

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {SwanArtifactFactory, SwanArtifact} from "../../src/SwanArtifact.sol";
5+
6+
contract MockSwanArtifactFactory {
7+
function deploy(string calldata _name, string calldata _symbol, bytes calldata _desc, address _owner)
8+
external
9+
returns (SwanArtifact)
10+
{
11+
// Return a mock artifact
12+
return SwanArtifact(address(0));
13+
}
14+
}

0 commit comments

Comments
 (0)