mirror of
https://github.com/getwax/wax.git
synced 2026-01-09 15:18:02 -05:00
360 lines
11 KiB
Solidity
360 lines
11 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.12;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "forge-std/console2.sol";
|
|
import {TestHelper} from "../utils/TestHelper.sol";
|
|
import {SafeECDSARecoveryPlugin, ECDSARecoveryStorage} from "../../../src/safe/SafeECDSARecoveryPlugin.sol";
|
|
import {SafeECDSAPlugin} from "../../../src/safe/SafeECDSAPlugin.sol";
|
|
import {Safe} from "safe-contracts/contracts/Safe.sol";
|
|
import {SafeProxy} from "safe-contracts/contracts/proxies/SafeProxy.sol";
|
|
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
|
|
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
|
|
|
|
/* solhint-disable func-name-mixedcase */
|
|
/* solhint-disable private-vars-leading-underscore */
|
|
/* solhint-disable var-name-mixedcase */
|
|
|
|
contract SafeECDSARecoveryPluginTest is TestHelper {
|
|
using ECDSA for bytes32;
|
|
|
|
constructor() TestHelper() {}
|
|
|
|
SafeECDSARecoveryPlugin public safeECDSARecoveryPlugin;
|
|
SafeECDSAPlugin public safeECDSAPlugin;
|
|
Safe public safeSingleton;
|
|
Safe public safe;
|
|
address public safeAddress;
|
|
|
|
address public owner;
|
|
|
|
bytes32 RECOVERY_HASH_DOMAIN;
|
|
|
|
function setUp() public {
|
|
safeECDSARecoveryPlugin = new SafeECDSARecoveryPlugin();
|
|
safeECDSAPlugin = new SafeECDSAPlugin(entryPointAddress);
|
|
|
|
safeSingleton = new Safe();
|
|
SafeProxy safeProxy = new SafeProxy(address(safeSingleton));
|
|
|
|
address[] memory owners = new address[](1);
|
|
owner = Alice.addr;
|
|
owners[0] = owner;
|
|
|
|
safe = Safe(payable(address(safeProxy)));
|
|
safeAddress = address(safe);
|
|
|
|
safe.setup(
|
|
owners,
|
|
1,
|
|
address(safeECDSAPlugin),
|
|
abi.encodeCall(SafeECDSAPlugin.enableMyself, (owner)),
|
|
address(safeECDSAPlugin),
|
|
address(0),
|
|
0,
|
|
payable(address(0))
|
|
);
|
|
|
|
vm.startPrank(safeAddress);
|
|
safe.enableModule(address(safeECDSARecoveryPlugin));
|
|
vm.stopPrank();
|
|
|
|
RECOVERY_HASH_DOMAIN = keccak256(
|
|
abi.encodePacked(
|
|
"RECOVERY_PLUGIN",
|
|
uint256(1),
|
|
block.chainid,
|
|
address(safeECDSARecoveryPlugin)
|
|
)
|
|
);
|
|
}
|
|
|
|
function test_addRecoveryAccount_ModuleNotEnabled() public {
|
|
// Arrange
|
|
address recoveryAccount = Bob.addr;
|
|
string memory salt = "test salt";
|
|
|
|
bytes32 recoveryHash = keccak256(
|
|
abi.encodePacked(RECOVERY_HASH_DOMAIN, recoveryAccount, owner, salt)
|
|
);
|
|
|
|
address prevModuleInLinkedList = address(0x1);
|
|
address moduleToDisable = address(safeECDSARecoveryPlugin);
|
|
|
|
// Act
|
|
vm.startPrank(safeAddress);
|
|
safe.disableModule(prevModuleInLinkedList, moduleToDisable);
|
|
|
|
// Assert
|
|
vm.expectRevert(SafeECDSARecoveryPlugin.MODULE_NOT_ENABLED.selector);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
recoveryHash,
|
|
safeAddress,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
}
|
|
|
|
function test_addRecoveryAccount_invalidOwner() public {
|
|
// Arrange
|
|
address recoveryAccount = Bob.addr;
|
|
address invalidOwner = Dave.addr;
|
|
string memory salt = "test salt";
|
|
|
|
bytes32 recoveryHash = keccak256(
|
|
abi.encodePacked(RECOVERY_HASH_DOMAIN, recoveryAccount, owner, salt)
|
|
);
|
|
|
|
// Act & Assert
|
|
vm.startPrank(safeAddress);
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
SafeECDSARecoveryPlugin.INVALID_OWNER.selector,
|
|
owner,
|
|
invalidOwner
|
|
)
|
|
);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
recoveryHash,
|
|
invalidOwner,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
}
|
|
|
|
function test_addRecoveryAccount_recoveryAccountAdded() public {
|
|
// Arrange
|
|
address recoveryAccount = Bob.addr;
|
|
string memory salt = "test salt";
|
|
|
|
bytes32 recoveryHash = keccak256(
|
|
abi.encodePacked(RECOVERY_HASH_DOMAIN, recoveryAccount, owner, salt)
|
|
);
|
|
|
|
// Act
|
|
vm.startPrank(safeAddress);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
recoveryHash,
|
|
owner,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
|
|
ECDSARecoveryStorage
|
|
memory ecdsaRecoveryStorage = safeECDSARecoveryPlugin
|
|
.getEcdsaRecoveryStorage(safeAddress);
|
|
|
|
// Assert
|
|
assertEq(ecdsaRecoveryStorage.recoveryHash, recoveryHash);
|
|
}
|
|
|
|
function test_addRecoveryAccount_addMultipleRecoveryAccountsToSamePlugin()
|
|
public
|
|
{
|
|
// Arrange
|
|
|
|
// Create and setup second safe to use with plugin
|
|
SafeProxy safeProxy2 = new SafeProxy(address(safeSingleton));
|
|
Safe safe2 = Safe(payable(address(safeProxy2)));
|
|
address safe2Address = address(safe2);
|
|
|
|
address[] memory owners = new address[](1);
|
|
owners[0] = owner;
|
|
|
|
vm.startPrank(safe2Address);
|
|
safe2.setup(
|
|
owners,
|
|
1,
|
|
address(safeECDSAPlugin),
|
|
abi.encodeCall(SafeECDSAPlugin.enableMyself, (owner)),
|
|
address(safeECDSAPlugin),
|
|
address(0),
|
|
0,
|
|
payable(address(0))
|
|
);
|
|
|
|
safe2.enableModule(address(safeECDSARecoveryPlugin));
|
|
vm.stopPrank();
|
|
|
|
address recoveryAccount1 = Bob.addr;
|
|
address recoveryAccount2 = Carol.addr;
|
|
|
|
string memory salt = "test salt";
|
|
|
|
bytes32 guardianHash1 = keccak256(
|
|
abi.encodePacked(
|
|
RECOVERY_HASH_DOMAIN,
|
|
recoveryAccount1,
|
|
owner,
|
|
salt
|
|
)
|
|
);
|
|
|
|
bytes32 guardianHash2 = keccak256(
|
|
abi.encodePacked(
|
|
RECOVERY_HASH_DOMAIN,
|
|
recoveryAccount2,
|
|
owner,
|
|
salt
|
|
)
|
|
);
|
|
|
|
// Act
|
|
vm.startPrank(safeAddress);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
guardianHash1,
|
|
owner,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
|
|
vm.startPrank(safe2Address);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
guardianHash2,
|
|
owner,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
|
|
// Assert
|
|
ECDSARecoveryStorage
|
|
memory ecdsaRecoveryStorage = safeECDSARecoveryPlugin
|
|
.getEcdsaRecoveryStorage(safeAddress);
|
|
ECDSARecoveryStorage
|
|
memory ecdsaRecoveryStorage2 = safeECDSARecoveryPlugin
|
|
.getEcdsaRecoveryStorage(safe2Address);
|
|
|
|
assertEq(ecdsaRecoveryStorage.recoveryHash, guardianHash1);
|
|
assertEq(ecdsaRecoveryStorage2.recoveryHash, guardianHash2);
|
|
}
|
|
|
|
function test_resetEcdsaAddress_invalidRecoveryHash() public {
|
|
// Arrange
|
|
address recoveryAccount = Bob.addr;
|
|
string memory salt = "test salt";
|
|
|
|
bytes32 recoveryHash = keccak256(
|
|
abi.encodePacked(
|
|
"INVALID_RECOVERY_HASH_DOMAIN",
|
|
recoveryAccount,
|
|
owner,
|
|
salt
|
|
)
|
|
);
|
|
|
|
bytes32 expectedGuardianHash = keccak256(
|
|
abi.encodePacked(RECOVERY_HASH_DOMAIN, recoveryAccount, owner, salt)
|
|
);
|
|
|
|
vm.startPrank(safeAddress);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
recoveryHash,
|
|
owner,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
vm.stopPrank();
|
|
|
|
Vm.Wallet memory newOwner = Carol;
|
|
bytes memory newOwnerSignature = abi.encodePacked("dummy signature");
|
|
|
|
// Act & Assert
|
|
vm.startPrank(recoveryAccount);
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
SafeECDSARecoveryPlugin.INVALID_GUARDIAN_HASH.selector,
|
|
recoveryHash,
|
|
expectedGuardianHash
|
|
)
|
|
);
|
|
safeECDSARecoveryPlugin.resetEcdsaAddress(
|
|
newOwnerSignature,
|
|
safeAddress,
|
|
salt,
|
|
address(safeECDSAPlugin),
|
|
owner,
|
|
newOwner.addr
|
|
);
|
|
}
|
|
|
|
function test_resetEcdsaAddress_invalidNewOwnerSignature() public {
|
|
// Arrange
|
|
address recoveryAccount = Bob.addr;
|
|
string memory salt = "test salt";
|
|
|
|
bytes32 recoveryHash = keccak256(
|
|
abi.encodePacked(RECOVERY_HASH_DOMAIN, recoveryAccount, owner, salt)
|
|
);
|
|
|
|
vm.startPrank(safeAddress);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
recoveryHash,
|
|
owner,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
vm.stopPrank();
|
|
|
|
Vm.Wallet memory newOwner = Carol;
|
|
|
|
bytes32 invalidOwnerHash = keccak256(
|
|
abi.encodePacked("invalid address hash")
|
|
);
|
|
bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(
|
|
invalidOwnerHash
|
|
);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(newOwner, ethSignedHash);
|
|
bytes memory newOwnerSignature = abi.encodePacked(r, s, v); // note the order here is different from the tuple above.
|
|
|
|
// Act & Assert
|
|
vm.startPrank(recoveryAccount);
|
|
vm.expectRevert(
|
|
SafeECDSARecoveryPlugin.INVALID_NEW_OWNER_SIGNATURE.selector
|
|
);
|
|
safeECDSARecoveryPlugin.resetEcdsaAddress(
|
|
newOwnerSignature,
|
|
safeAddress,
|
|
salt,
|
|
address(safeECDSAPlugin),
|
|
owner,
|
|
newOwner.addr
|
|
);
|
|
}
|
|
|
|
function test_resetEcdsaAddress_resetsEcdsaAddress() public {
|
|
// Arrange
|
|
address recoveryAccount = Bob.addr;
|
|
string memory salt = "test salt";
|
|
|
|
bytes32 recoveryHash = keccak256(
|
|
abi.encodePacked(RECOVERY_HASH_DOMAIN, recoveryAccount, owner, salt)
|
|
);
|
|
|
|
vm.startPrank(safeAddress);
|
|
safeECDSARecoveryPlugin.addRecoveryAccount(
|
|
recoveryHash,
|
|
owner,
|
|
address(safeECDSAPlugin)
|
|
);
|
|
vm.stopPrank();
|
|
|
|
Vm.Wallet memory newOwner = Carol;
|
|
|
|
bytes32 currentOwnerHash = keccak256(abi.encodePacked(owner));
|
|
bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(
|
|
currentOwnerHash
|
|
);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(newOwner, ethSignedHash);
|
|
bytes memory newOwnerSignature = abi.encodePacked(r, s, v); // note the order here is different from the tuple above.
|
|
|
|
// Act
|
|
vm.startPrank(recoveryAccount);
|
|
safeECDSARecoveryPlugin.resetEcdsaAddress(
|
|
newOwnerSignature,
|
|
safeAddress,
|
|
salt,
|
|
address(safeECDSAPlugin),
|
|
owner,
|
|
newOwner.addr
|
|
);
|
|
|
|
// Assert
|
|
address updatedOwner = safeECDSAPlugin.getOwner(safeAddress);
|
|
assertEq(updatedOwner, newOwner.addr);
|
|
}
|
|
}
|