Skip to content

Commit

Permalink
feat(ct): add Balance contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
sam-goldman committed Jul 24, 2023
1 parent 98e4de6 commit 1ce34a9
Show file tree
Hide file tree
Showing 24 changed files with 781 additions and 170 deletions.
7 changes: 7 additions & 0 deletions .changeset/thin-deers-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@sphinx/contracts': minor
'@sphinx/plugins': patch
'@sphinx/core': patch
---

Add Balance contracts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SphinxAuth } from "./SphinxAuth.sol";
import { SphinxAuthProxy } from "./SphinxAuthProxy.sol";
import { Version, Semver } from "./Semver.sol";

contract SphinxFactory is Ownable {
contract SphinxAuthFactory is Ownable {
/**
* @notice Emitted whenever a SphinxAuthProxy is deployed.
*
Expand Down Expand Up @@ -83,10 +83,13 @@ contract SphinxFactory is Ownable {
bytes memory _registryData,
string memory _projectName
) external {
require(currentAuthImplementation != address(0), "SphinxFactory: no auth implementation");
require(
currentAuthImplementation != address(0),
"SphinxAuthFactory: no auth implementation"
);

bytes32 salt = keccak256(abi.encode(_authData, _projectName));
require(address(auths[salt]) == address(0), "SphinxFactory: already deployed");
require(address(auths[salt]) == address(0), "SphinxAuthFactory: already deployed");

address authProxyAddress = getAuthProxyAddress(salt);

Expand All @@ -96,7 +99,7 @@ contract SphinxFactory is Ownable {

require(
address(authProxy) == authProxyAddress,
"SphinxFactory: failed to deploy auth proxy"
"SphinxAuthFactory: failed to deploy auth proxy"
);

auths[salt] = payable(authProxyAddress);
Expand Down Expand Up @@ -134,7 +137,10 @@ contract SphinxFactory is Ownable {
uint256 minor = version.minor;
uint256 patch = version.patch;

require(versions[major][minor][patch] == address(0), "SphinxFactory: version already set");
require(
versions[major][minor][patch] == address(0),
"SphinxAuthFactory: version already set"
);

authImplementations[_auth] = true;
versions[major][minor][patch] = _auth;
Expand All @@ -143,7 +149,7 @@ contract SphinxFactory is Ownable {
}

function setCurrentAuthImplementation(address _impl) external onlyOwner {
require(authImplementations[_impl], "SphinxFactory: invalid auth implementation");
require(authImplementations[_impl], "SphinxAuthFactory: invalid auth implementation");
currentAuthImplementation = _impl;
emit CurrentAuthImplementationSet(_impl);
}
Expand Down
18 changes: 9 additions & 9 deletions packages/contracts/contracts/SphinxAuthProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@
pragma solidity ^0.8.15;

import { Proxy } from "@eth-optimism/contracts-bedrock/contracts/universal/Proxy.sol";
import { SphinxFactory } from "./SphinxFactory.sol";
import { SphinxAuthFactory } from "./SphinxAuthFactory.sol";

/**
* @title SphinxAuthProxy
* @notice Proxy contract owned by the user. This proxy is designed to be upgradable by the user in
an opt-in manner. New implementations of SphinxAuth must be approved by the owner of the
SphinxFactory contract to prevent malicious SphinxAuth implementations from being
SphinxAuthFactory contract to prevent malicious SphinxAuth implementations from being
used.
*/
contract SphinxAuthProxy is Proxy {
/**
* @notice Address of the SphinxFactory.
* @notice Address of the SphinxAuthFactory.
*/
SphinxFactory public immutable factory;
SphinxAuthFactory public immutable factory;

/**
* @notice Modifier that throws an error if the new implementation is not approved by the
SphinxFactory.
SphinxAuthFactory.
@param _implementation The address of the new implementation.
*/
Expand All @@ -32,16 +32,16 @@ contract SphinxAuthProxy is Proxy {
}

/**
* @param _factory The SphinxFactory's address.
* @param _factory The SphinxAuthFactory's address.
* @param _admin Owner of this contract. Usually the end-user.
*/
constructor(SphinxFactory _factory, address _admin) payable Proxy(_admin) {
constructor(SphinxAuthFactory _factory, address _admin) payable Proxy(_admin) {
factory = _factory;
}

/**
* @notice Sets a new implementation for this proxy. Only the owner can call this function. The
new implementation must be approved by the SphinxFactory to prevent malicious
new implementation must be approved by the SphinxAuthFactory to prevent malicious
SphinxAuth implementations.
*/
function upgradeTo(
Expand All @@ -53,7 +53,7 @@ contract SphinxAuthProxy is Proxy {
/**
* @notice Sets a new implementation for this proxy and delegatecalls an arbitrary function.
Only the owner can call this function. The new implementation must be approved by the
SphinxFactory to prevent malicious SphinxAuth implementations.
SphinxAuthFactory to prevent malicious SphinxAuth implementations.
*/
function upgradeToAndCall(
address _implementation,
Expand Down
54 changes: 54 additions & 0 deletions packages/contracts/contracts/SphinxBalance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

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

/**
* @title SphinxBalance
* @notice The SphinxBalance contract is where an organization stores its USDC, which pays for
* deployments in the Sphinx DevOps platform. This contract is only meant to exist on
* Optimism Mainnet and Optimism Goerli.
*
* This contract is owned by a single address, which belongs to the organization. Anyone can
* transfer USDC to this contract, but only the owner can transfer these funds elsewhere. To
* fund a deployment, the owner of this contract sends a transaction that transfers USDC to
* the corresponding SphinxEscrow contract. There is a one-to-one mapping between
* SphinxBalance and SphinxEscrow contracts. Both are deployed by the SphinxBalanceFactory
* contract.
*
* The owner of this contract can also increase or decrease the USDC allowance of an
* arbitrary spender address using the standard ERC20 allowance mechanism. By setting the
* corresponding SphinxEscrow contract as the spender, the owner can fund deployments via
* this allowance mechanism.
*
* Note that we don't need to check the boolean values that are returned from function calls
* to the USDC contract, such as `usdc.transfer`. The is because these functions in the USDC
* contract always return true.
*/
contract SphinxBalance is Ownable {
ERC20 public immutable usdc;

address public immutable escrow;

string public orgId;

constructor(string memory _orgId, address _owner, address _usdc, address _escrow) {
orgId = _orgId;
usdc = ERC20(_usdc);
escrow = _escrow;
_transferOwnership(_owner);
}

function transfer(address _to, uint256 _amount) external onlyOwner {
usdc.transfer(_to, _amount);
}

function increaseAllowance(address _spender, uint256 _addedAmount) external onlyOwner {
usdc.increaseAllowance(_spender, _addedAmount);
}

function decreaseAllowance(address _spender, uint256 _subtractedAmount) external onlyOwner {
usdc.decreaseAllowance(_spender, _subtractedAmount);
}
}
64 changes: 64 additions & 0 deletions packages/contracts/contracts/SphinxBalanceFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { SphinxBalance } from "./SphinxBalance.sol";
import { SphinxEscrow } from "./SphinxEscrow.sol";

/**
* @title SphinxBalanceFactory
* @notice Allows anyone to deploy a SphinxBalance contract and SphinxEscrow contract for a given
* org ID. These two contracts handle payments in the Sphinx DevOps platform. The addresses
* of both contracts are calculated via Create2 using the org ID as the salt. This contract
* is only meant to exist on Optimism Mainnet and Optimism Goerli.
*/
contract SphinxBalanceFactory {
event BalanceFactoryDeployment(
string indexed orgIdHash,
address owner,
string orgId,
address caller
);

address public immutable usdc;

address public immutable managedService;

mapping(bytes32 => bool) public isDeployed;

constructor(address _usdc, address _managedService) {
usdc = _usdc;
managedService = _managedService;
}

function deploy(string memory _orgId, address _owner) external {
require(_owner != address(0), "SphinxBalanceFactory: owner cannot be address(0)");

bytes32 salt = keccak256(abi.encode(_orgId));
require(!isDeployed[salt], "SphinxBalanceFactory: org id already deployed");

isDeployed[salt] = true;

// Next, we'll deploy the SphinxBalance and SphinxEscrow contracts. We don't need to check
// that they've been deployed at the correct Create2 address because their constructors
// can't revert and because it's not possible for a contract to already exist at the Create2
// address.

SphinxEscrow escrow = new SphinxEscrow{ salt: salt }(_orgId, usdc, managedService);

// Deploy a SphinxBalance contract with this contract as the initial owner. This makes it
// easy to calculate the Create2 address of the SphinxBalance contract off-chain, since we
// don't need to know the owner's address to calculate it.
SphinxBalance balance = new SphinxBalance{ salt: salt }(
_orgId,
address(this),
usdc,
address(escrow)
);

// Transfer ownership of the SphinxBalance contract to the specified owner.
Ownable(balance).transferOwnership(_owner);

emit BalanceFactoryDeployment(_orgId, _owner, _orgId, msg.sender);
}
}
56 changes: 56 additions & 0 deletions packages/contracts/contracts/SphinxEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

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

/**
* @title SphinxEscrow
* @notice The SphinxEscrow contract receives USDC from its corresponding SphinxBalance contract to
* pay for deployments in the Sphinx DevOps platform. Each organization is meant to have one
* SphinxBalance contract and SphinxEscrow contract.
*
* USDC can only be transferred from this contract by addresses that belong to the Sphinx
* DevOps platform. These addresses can also transfer funds away from any contract that has
* given an allowance to this contract.
*
* This contract is only meant to exist on Optimism Mainnet and Optimism Goerli.
*
* Note that we don't need to check the boolean values that are returned from function calls
* to the USDC contract, such as `usdc.transfer`. The is because these functions in the USDC
* contract always return true.
*/
contract SphinxEscrow {
bytes32 private constant FUNDER_ROLE = keccak256("FUNDER_ROLE");

IERC20 public immutable usdc;

IAccessControl public immutable managedService;

string public orgId;

modifier onlyFunder() {
require(
managedService.hasRole(FUNDER_ROLE, msg.sender),
"SphinxEscrow: caller is not a funder"
);
_;
}

constructor(string memory _orgId, address _usdc, address _managedService) {
orgId = _orgId;
usdc = IERC20(_usdc);
managedService = IAccessControl(_managedService);
}

function batchTransfer(address[] memory _to, uint256[] memory _amounts) external onlyFunder {
require(_to.length == _amounts.length, "SphinxEscrow: array length mismatch");
for (uint256 i = 0; i < _to.length; i++) {
usdc.transfer(_to[i], _amounts[i]);
}
}

function transferFrom(address _from, address _to, uint256 _amount) external onlyFunder {
usdc.transferFrom(_from, _to, _amount);
}
}
10 changes: 8 additions & 2 deletions packages/contracts/src/ifaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ export const OZTransparentAdapterArtifact = require('../artifacts/contracts/adap
export const DefaultGasPriceCalculatorArtifact = require('../artifacts/contracts/DefaultGasPriceCalculator.sol/DefaultGasPriceCalculator.json')
export const DefaultCreate3Artifact = require('../artifacts/contracts/DefaultCreate3.sol/DefaultCreate3.json')
export const AuthArtifact = require('../artifacts/contracts/SphinxAuth.sol/SphinxAuth.json')
export const FactoryArtifact = require('../artifacts/contracts/SphinxFactory.sol/SphinxFactory.json')
export const AuthFactoryArtifact = require('../artifacts/contracts/SphinxAuthFactory.sol/SphinxAuthFactory.json')
export const AuthProxyArtifact = require('../artifacts/contracts/SphinxAuthProxy.sol/SphinxAuthProxy.json')
export const BalanceFactoryArtifact = require('../artifacts/contracts/SphinxBalanceFactory.sol/SphinxBalanceFactory.json')
export const BalanceArtifact = require('../artifacts/contracts/SphinxBalance.sol/SphinxBalance.json')
export const EscrowArtifact = require('../artifacts/contracts/SphinxEscrow.sol/SphinxEscrow.json')

const directoryPath = path.join(__dirname, '../artifacts/build-info')
const fileNames = fs.readdirSync(directoryPath)
Expand All @@ -43,5 +46,8 @@ export const DefaultGasPriceCalculatorABI =
DefaultGasPriceCalculatorArtifact.abi
export const DefaultCreate3ABI = DefaultCreate3Artifact.abi
export const AuthABI = AuthArtifact.abi
export const FactoryABI = FactoryArtifact.abi
export const AuthFactoryABI = AuthFactoryArtifact.abi
export const AuthProxyABI = AuthProxyArtifact.abi
export const BalanceFactoryABI = BalanceFactoryArtifact.abi
export const BalanceABI = BalanceArtifact.abi
export const EscrowABI = EscrowArtifact.abi
Loading

0 comments on commit 1ce34a9

Please sign in to comment.