Skip to content

Commit

Permalink
Safe yul compatibility with test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
dodger213 committed Apr 12, 2024
1 parent 6fbf203 commit cec0add
Show file tree
Hide file tree
Showing 17 changed files with 255 additions and 119 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ yarn-error.log
# Certora Formal Verification related files
.certora_internal
.certora_recent_jobs.json
.zip-output-url.txt
.zip-output-url.txt

# Safe YUL code
contracts/SafeBytecode.sol
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
JQ ?= jq

SAFEYULROOT ?=
SAFEYULBASE := build/yul/Safe.json
SAFEYUL := $(SAFEYULROOT)/$(SAFEYULBASE)

contracts/SafeBytecode.sol: $(SAFEYUL)
@echo '// SPDX-License-Identifier: LGPL-3.0-only' >$@
@echo 'pragma solidity >=0.7.0 <0.9.0;' >>$@
@echo '' >>$@
@echo 'contract SafeBytecode {' >>$@
@echo ' bytes public constant DEPLOYED_BYTECODE =' >>$@
@echo ' hex$(shell $(JQ) '.evm.deployedBytecode.object' $<);' >>$@
@echo '}' >>$@

.PHONY: $(SAFEYUL)
$(SAFEYUL):
@test -n "$(SAFEYULROOT)" || ( echo 'SAFEYULROOT not specified'; exit 1 )
@$(MAKE) -C $(SAFEYULROOT) $(SAFEYULBASE)
15 changes: 14 additions & 1 deletion contracts/Safe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import "./common/StorageAccessible.sol";
import "./interfaces/ISignatureValidator.sol";
import "./external/SafeMath.sol";

import "./SafeBytecode.sol";

/**
* @title Safe - A multisignature wallet with support for confirmations using signed messages based on EIP-712.
* @dev Most important concepts:
Expand Down Expand Up @@ -70,13 +72,24 @@ contract Safe is
mapping(address => mapping(bytes32 => uint256)) public approvedHashes;

// This constructor ensures that this contract can only be used as a singleton for Proxy contracts
constructor() {
constructor(address byteCode) {
/**
* By setting the threshold it is not possible to call setup anymore,
* so we create a Safe with 0 owners and threshold 1.
* This is an unusable Safe, perfect for the singleton
*/
threshold = 1;

if (byteCode != address(0)) {
bytes memory code = SafeBytecode(byteCode).DEPLOYED_BYTECODE();

/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
return(add(code, 32), mload(code))
}
/* solhint-enable no-inline-assembly */
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions contracts/SafeL2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ contract SafeL2 is Safe {

event SafeModuleTransaction(address module, address to, uint256 value, bytes data, Enum.Operation operation);

constructor() Safe(address(0)) {}

// @inheritdoc Safe
function execTransaction(
address to,
Expand Down
81 changes: 81 additions & 0 deletions contracts/accessors/SafeFallbackAccessor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

contract SafeFallbackAccessor {
address private constant _SENTINEL = address(1);

address private _singleton;
mapping(address => address) private _modules;
mapping(address => address) private _owners;
uint256 private _ownerCount;
uint256 private _threshold;
uint256 private _nonce;
bytes32 private _deprecatedDomainSeparator;
mapping(bytes32 => uint256) private _signedMessages;
mapping(address => mapping(bytes32 => uint256)) private _approvedHashes;

function signedMessages(bytes32 hash) external view returns (bool signed) {
return _signedMessages[hash] != 0;
}

function getModulesPaginated(address start, uint256 pageSize)
public
view
returns (address[] memory modules, address next)
{
require(start == _SENTINEL || _modules[start] != address(0), "GS105");
require(pageSize > 0, "GS106");
modules = new address[](pageSize);

uint256 moduleCount = 0;
next = _modules[start];
while (next != address(0) && next != _SENTINEL && moduleCount < pageSize) {
modules[moduleCount] = next;
next = _modules[next];
moduleCount++;
}

if (next != _SENTINEL) {
next = modules[moduleCount - 1];
}

/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
mstore(modules, moduleCount)
}
/* solhint-enable no-inline-assembly */
}

function getModules() external view returns (address[] memory modules) {
address next;
(modules, next) = getModulesPaginated(_SENTINEL, 10);
require(next == address(0) || next == _SENTINEL, "GS107");
return modules;
}

function getOwners() external view returns (address[] memory array) {
array = new address[](_ownerCount);

uint256 index = 0;
address currentOwner = _owners[_SENTINEL];
while (currentOwner != _SENTINEL) {
array[index] = currentOwner;
currentOwner = _owners[currentOwner];
index++;
}
}

function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory result) {
result = new bytes(length * 32);
for (uint256 index = 0; index < length; index++) {
/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
let word := sload(add(offset, index))
mstore(add(add(result, 0x20), mul(index, 0x20)), word)
}
/* solhint-enable no-inline-assembly */
}
}
}
92 changes: 3 additions & 89 deletions contracts/handler/CompatibilityFallbackHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import "./TokenCallbackHandler.sol";
import "../interfaces/ISignatureValidator.sol";
import "../Safe.sol";

import "./SafeFallbackHandler.sol";

/**
* @title Compatibility Fallback Handler - Provides compatibility between pre 1.3.0 and 1.3.0+ Safe contracts.
* @author Richard Meissner - @rmeissner
*/
contract CompatibilityFallbackHandler is TokenCallbackHandler, ISignatureValidator {
contract CompatibilityFallbackHandler is SafeFallbackHandler, TokenCallbackHandler, ISignatureValidator {
// keccak256("SafeMessage(bytes message)");
bytes32 private constant SAFE_MSG_TYPEHASH = 0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1146b5a6150fbca;

Expand Down Expand Up @@ -79,92 +81,4 @@ contract CompatibilityFallbackHandler is TokenCallbackHandler, ISignatureValidat
bytes4 value = validator.isValidSignature(abi.encode(_dataHash), _signature);
return (value == EIP1271_MAGIC_VALUE) ? UPDATED_MAGIC_VALUE : bytes4(0);
}

/**
* @dev Returns array of first 10 modules.
* @return Array of modules.
*/
function getModules() external view returns (address[] memory) {
// Caller should be a Safe
Safe safe = Safe(payable(msg.sender));
(address[] memory array, ) = safe.getModulesPaginated(SENTINEL_MODULES, 10);
return array;
}

/**
* @dev Performs a delegatecall on a targetContract in the context of self.
* Internally reverts execution to avoid side effects (making it static). Catches revert and returns encoded result as bytes.
* @dev Inspired by https://github.com/gnosis/util-contracts/blob/bb5fe5fb5df6d8400998094fb1b32a178a47c3a1/contracts/StorageAccessible.sol
* @param targetContract Address of the contract containing the code to execute.
* @param calldataPayload Calldata that should be sent to the target contract (encoded method name and arguments).
*/
function simulate(address targetContract, bytes calldata calldataPayload) external returns (bytes memory response) {
/**
* Suppress compiler warnings about not using parameters, while allowing
* parameters to keep names for documentation purposes. This does not
* generate code.
*/
targetContract;
calldataPayload;

// solhint-disable-next-line no-inline-assembly
assembly {
let internalCalldata := mload(0x40)
/**
* Store `simulateAndRevert.selector`.
* String representation is used to force right padding
*/
mstore(internalCalldata, "\xb4\xfa\xba\x09")
/**
* Abuse the fact that both this and the internal methods have the
* same signature, and differ only in symbol name (and therefore,
* selector) and copy calldata directly. This saves us approximately
* 250 bytes of code and 300 gas at runtime over the
* `abi.encodeWithSelector` builtin.
*/
calldatacopy(add(internalCalldata, 0x04), 0x04, sub(calldatasize(), 0x04))

/**
* `pop` is required here by the compiler, as top level expressions
* can't have return values in inline assembly. `call` typically
* returns a 0 or 1 value indicated whether or not it reverted, but
* since we know it will always revert, we can safely ignore it.
*/
pop(
call(
gas(),
// address() has been changed to caller() to use the implementation of the Safe
caller(),
0,
internalCalldata,
calldatasize(),
/**
* The `simulateAndRevert` call always reverts, and
* instead encodes whether or not it was successful in the return
* data. The first 32-byte word of the return data contains the
* `success` value, so write it to memory address 0x00 (which is
* reserved Solidity scratch space and OK to use).
*/
0x00,
0x20
)
)

/**
* Allocate and copy the response bytes, making sure to increment
* the free memory pointer accordingly (in case this method is
* called as an internal function). The remaining `returndata[0x20:]`
* contains the ABI encoded response bytes, so we can just write it
* as is to memory.
*/
let responseSize := sub(returndatasize(), 0x20)
response := mload(0x40)
mstore(0x40, add(response, responseSize))
returndatacopy(response, 0x20, responseSize)

if iszero(mload(0x00)) {
revert(add(response, 0x20), mload(response))
}
}
}
}
84 changes: 84 additions & 0 deletions contracts/handler/SafeFallbackHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

import {SafeFallbackAccessor} from "../accessors/SafeFallbackAccessor.sol";

contract SafeFallbackHandler {
SafeFallbackAccessor private immutable _ACCESSOR;

constructor() {
_ACCESSOR = new SafeFallbackAccessor();
}

function signedMessages(bytes32) external view returns (bool) {
_fallbackToAccessor();
}

function getChainId() external view returns (uint256 chainId) {
/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
chainId := chainid()
}
/* solhint-enable no-inline-assembly */
}

function getModulesPaginated(address, uint256) external view returns (address[] memory, address) {
_fallbackToAccessor();
}

function getModules() external view returns (address[] memory) {
_fallbackToAccessor();
}

function getOwners() external view returns (address[] memory) {
_fallbackToAccessor();
}

function getStorageAt(uint256, uint256) external view returns (bytes memory) {
_fallbackToAccessor();
}

function simulate(address target, bytes calldata data) public returns (bytes memory result) {
bytes memory simulationCallData = abi.encodeWithSelector(0xb4faba09, target, data);

/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
pop(call(gas(), caller(), 0, add(simulationCallData, 0x20), mload(simulationCallData), 0x00, 0x20))

let responseSize := sub(returndatasize(), 0x20)
result := mload(0x40)
mstore(0x40, add(result, responseSize))
returndatacopy(result, 0x20, responseSize)

if iszero(mload(0x00)) { revert(add(result, 0x20), mload(result)) }
}
/* solhint-enable no-inline-assembly */
}

function _simulateAccessor(bytes calldata data) internal view returns (bytes memory result) {
function(address, bytes calldata) internal returns (bytes memory) _simulate = simulate;
function(address, bytes calldata) internal view returns (bytes memory) _simulateView;

/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
_simulateView := _simulate
}
/* solhint-enable no-inline-assembly */

return _simulateView(address(_ACCESSOR), data);
}

function _fallbackToAccessor() internal view {
bytes memory result = _simulateAccessor(msg.data);

/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
return(add(result, 0x20), mload(result))
}
/* solhint-enable no-inline-assembly */
}
}
7 changes: 7 additions & 0 deletions src/deploy/deploy_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
deterministicDeployment: true,
});

await deploy("SafeFallbackHandler", {
from: deployer,
args: [],
log: true,
deterministicDeployment: true,
});

await deploy("CompatibilityFallbackHandler", {
from: deployer,
args: [],
Expand Down
9 changes: 8 additions & 1 deletion src/deploy/deploy_safe_singleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await getNamedAccounts();
const { deploy } = deployments;

await deploy("Safe", {
const safeBytecode = await deploy("SafeBytecode", {
from: deployer,
args: [],
log: true,
deterministicDeployment: true,
})

await deploy("Safe", {
from: deployer,
args: [safeBytecode.address],
log: true,
deterministicDeployment: true,
});
};

Expand Down
2 changes: 1 addition & 1 deletion test/accessors/SimulateTxAccessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("SimulateTxAccessor", async () => {
const simulation = accessor.interface.decodeFunctionResult("simulate", acccessibleData);
expect(safe.interface.decodeFunctionResult("getOwners", simulation.returnData)[0]).to.be.deep.eq([user1.address]);
expect(simulation.success).to.be.true;
expect(simulation.estimate.toNumber()).to.be.lte(10000);
expect(simulation.estimate.toNumber()).to.be.lte(15000);
});

it("simulate delegatecall", async () => {
Expand Down
Loading

0 comments on commit cec0add

Please sign in to comment.