Skip to content

Commit 796ef98

Browse files
committed
Swap out Safe requirements for generic multisig
1 parent 2223ef7 commit 796ef98

5 files changed

Lines changed: 84 additions & 41 deletions

File tree

src/StreamWeightActor.sol

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {FVMRewards} from "./lib/FVMRewards.sol";
99
import {PendingOp, Share, WeightRecord, WeightRecordUpdate} from "./lib/FVMRewardTypes.sol";
1010
import {OwnersLibrary} from "./lib/Owners.sol";
1111
import {UnanimousGovernance} from "./lib/UnanimousGovernance.sol";
12-
import {IsASafe} from "./lib/IsASafe.sol";
12+
import {IsAMultisig} from "./lib/IsAMultisig.sol";
1313

1414
uint64 constant SERVICE_ID = 2;
1515

@@ -20,20 +20,20 @@ int256 constant STEP = 5e16; // 5%
2020
/// @dev Writes require unanimous owner approval, except `cancelPending`/`cancelPendingWeight`
2121
/// (any single owner, immediate) and `quarterlyGateCheck` (fully permissionless).
2222
contract StreamWeightActor is UnanimousGovernance {
23-
using IsASafe for address;
23+
using IsAMultisig for address;
2424
using OwnersLibrary for address;
2525

2626
IServiceRewardsActor immutable SRA;
2727
Epoch immutable QUARTER;
2828
Epoch immutable HOLD;
2929

3030
/// @notice Deploys the actor with its two initial owners, bound to a Service Rewards Actor.
31-
/// @param owner1 First owner; must be a Safe.
32-
/// @param owner2 Second owner; must be a Safe.
31+
/// @param owner1 First owner; must be a multisig with more than one signer.
32+
/// @param owner2 Second owner; must be a multisig with more than one signer.
3333
/// @param sra Service Rewards Actor supplying QUARTER/HOLD and gating `quarterlyGateCheck`.
3434
constructor(address owner1, address owner2, IServiceRewardsActor sra) {
35-
owner1.isProbablyASafe();
36-
owner2.isProbablyASafe();
35+
owner1.requireMultisig();
36+
owner2.requireMultisig();
3737

3838
owner1.addOwner();
3939
owner2.addOwner();
@@ -118,9 +118,9 @@ contract StreamWeightActor is UnanimousGovernance {
118118

119119
/// @notice Replaces one of the two owners.
120120
/// @param prevOwner Owner being removed.
121-
/// @param newOwner Owner being added; must be a Safe.
121+
/// @param newOwner Owner being added; must be a multisig with more than one signer.
122122
function replaceOwner(address prevOwner, address newOwner) external unanimousNoHold(keccak256(msg.data)) {
123-
newOwner.isProbablyASafe();
123+
newOwner.requireMultisig();
124124
prevOwner.removeOwner();
125125
newOwner.addOwner();
126126
}

src/lib/IsAMultisig.sol

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: Apache-2.0 OR MIT
2+
pragma solidity ^0.8.36;
3+
4+
/// @notice The only thing we require of a multisig: it can list its signers.
5+
/// @dev Safe{Wallet} and the other common multisig implementations all expose this.
6+
interface IMultisig {
7+
function getOwners() external view returns (address[] memory);
8+
}
9+
10+
library IsAMultisig {
11+
error NotAMultisig(address account);
12+
error TooFewSigners(address account, uint256 signers);
13+
14+
/// @notice Requires that `account` is a contract behaving like a multisig with >1 signer.
15+
/// @dev FIP-0118 mandates no specific multisig implementation, so this deliberately checks
16+
/// behaviour, not identity: any contract answering `getOwners()` with two or more signers
17+
/// qualifies, and EOAs (no code, so no return data) do not.
18+
function requireMultisig(address account) internal view {
19+
(bool ok, bytes memory ret) = account.staticcall(abi.encodeCall(IMultisig.getOwners, ()));
20+
// 64 bytes is the shortest well-formed `address[]` encoding: head offset + length
21+
require(ok && ret.length >= 64, NotAMultisig(account));
22+
address[] memory signers = abi.decode(ret, (address[]));
23+
require(signers.length > 1, TooFewSigners(account, signers.length));
24+
}
25+
}

src/lib/IsASafe.sol

Lines changed: 0 additions & 18 deletions
This file was deleted.

test/StreamWeightActor.t.sol

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
// SPDX-License-Identifier: Apache-2.0 OR MIT
22
pragma solidity ^0.8.36;
33

4-
import {SafeProxy} from "@safe/proxies/SafeProxy.sol";
5-
64
import {USR_FORBIDDEN, USR_ILLEGAL_ARGUMENT, USR_NOT_FOUND} from "fvm-solidity/FVMErrors.sol";
75

6+
import {MockMultisig} from "./mocks/MockMultisig.sol";
87
import {MockRewardTest} from "./mocks/MockRewardTest.sol";
98
import {WAD} from "./mocks/FVMRewardActor.sol";
109
import {StreamWeightActor} from "../src/StreamWeightActor.sol";
1110
import {IServiceRewardsActor} from "../src/interfaces/IServiceRewardsActor.sol";
1211
import {PendingOp, Share, WeightRecord, WeightRecordUpdate} from "../src/lib/FVMRewardTypes.sol";
1312
import {Epoch} from "../src/lib/Epoch.sol";
1413
import {FVMRewards} from "../src/lib/FVMRewards.sol";
14+
import {IsAMultisig} from "../src/lib/IsAMultisig.sol";
1515
import {SWA_TIMELOCK} from "../src/lib/FVMRewardMethod.sol";
1616

1717
contract StreamWeightActorTest is MockRewardTest {
@@ -27,8 +27,8 @@ contract StreamWeightActorTest is MockRewardTest {
2727

2828
function setUp() public override {
2929
super.setUp();
30-
owner1 = _makeSafeOwner("owner1");
31-
owner2 = _makeSafeOwner("owner2");
30+
owner1 = _makeMultisigOwner("owner1");
31+
owner2 = _makeMultisigOwner("owner2");
3232

3333
address sra = makeAddr("sra");
3434
vm.mockCall(
@@ -40,16 +40,9 @@ contract StreamWeightActorTest is MockRewardTest {
4040
rewardActor().mockSwa(address(actor));
4141
}
4242

43-
function _makeSafeOwner(string memory label) internal returns (address proxyAddr) {
44-
address masterCopy = makeAddr(string.concat(label, "-mastercopy"));
45-
vm.etch(masterCopy, new bytes(8001));
46-
47-
SafeProxy real = new SafeProxy(masterCopy);
48-
bytes memory code = address(real).code;
49-
50-
proxyAddr = makeAddr(label);
51-
vm.etch(proxyAddr, code);
52-
vm.store(proxyAddr, bytes32(0), bytes32(uint256(uint160(masterCopy))));
43+
function _makeMultisigOwner(string memory label) internal returns (address multisig) {
44+
multisig = address(new MockMultisig(2));
45+
vm.label(multisig, label);
5346
}
5447

5548
// -------------------------------------------------------------------------
@@ -285,7 +278,7 @@ contract StreamWeightActorTest is MockRewardTest {
285278
// -------------------------------------------------------------------------
286279

287280
function test_ReplaceOwner_Success_SwapsApprovalRights() public {
288-
address newOwner = _makeSafeOwner("newOwner");
281+
address newOwner = _makeMultisigOwner("newOwner");
289282

290283
vm.prank(owner1);
291284
actor.replaceOwner(owner2, newOwner);
@@ -301,4 +294,30 @@ contract StreamWeightActorTest is MockRewardTest {
301294
vm.prank(newOwner);
302295
actor.cancelPendingWeight(PendingOp.SET_WEIGHT);
303296
}
297+
298+
// -------------------------------------------------------------------------
299+
// Owner must be a multisig
300+
// -------------------------------------------------------------------------
301+
302+
function test_Constructor_EoaOwner_RevertsNotAMultisig() public {
303+
address eoa = makeAddr("eoa");
304+
vm.expectRevert(abi.encodeWithSelector(IsAMultisig.NotAMultisig.selector, eoa));
305+
new StreamWeightActor(owner1, eoa, IServiceRewardsActor(makeAddr("sra")));
306+
}
307+
308+
function test_Constructor_SingleSignerOwner_RevertsTooFewSigners() public {
309+
address solo = address(new MockMultisig(1));
310+
vm.expectRevert(abi.encodeWithSelector(IsAMultisig.TooFewSigners.selector, solo, uint256(1)));
311+
new StreamWeightActor(owner1, solo, IServiceRewardsActor(makeAddr("sra")));
312+
}
313+
314+
function test_ReplaceOwner_EoaOwner_RevertsNotAMultisig() public {
315+
address eoa = makeAddr("eoa");
316+
317+
vm.prank(owner1);
318+
actor.replaceOwner(owner2, eoa);
319+
vm.prank(owner2);
320+
vm.expectRevert(abi.encodeWithSelector(IsAMultisig.NotAMultisig.selector, eoa));
321+
actor.replaceOwner(owner2, eoa);
322+
}
304323
}

test/mocks/MockMultisig.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: Apache-2.0 OR MIT
2+
pragma solidity ^0.8.36;
3+
4+
/// @notice Minimal stand-in for any multisig: it just lists its signers.
5+
contract MockMultisig {
6+
address[] internal signers;
7+
8+
constructor(uint256 count) {
9+
for (uint256 i = 0; i < count; i++) {
10+
signers.push(address(uint160(i + 1)));
11+
}
12+
}
13+
14+
function getOwners() external view returns (address[] memory) {
15+
return signers;
16+
}
17+
}

0 commit comments

Comments
 (0)