Skip to content

Commit

Permalink
merge with previous PR
Browse files Browse the repository at this point in the history
  • Loading branch information
cryptoAtwill committed Oct 1, 2024
2 parents cfd8e57 + 78a509f commit 00e14bd
Show file tree
Hide file tree
Showing 41 changed files with 649 additions and 514 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

All notable changes to this project will be documented in this file.

## [axon-r05] - 2024-09-24

_Full changelog below._

### ⭐ HIGHLIGHT | Token-bound validator collateral 🪙🔐

Token-bound validator collateral enables subnets to designate an `ERC20` token as the staking collateral for consensus validators, rather than being limited to the parent chain's native token (e.g., `FIL`).

This feature provides subnet operators with the flexibility to use their own tokens for staking, aligning network security with the native token economy. As subnets grow and evolve, this capability becomes increasingly important for subnet autonomy. Validators can stake the subnet’s native token, simplifying participation and reinforcing the subnet's economic model.

By decoupling consensus security from the parent token, subnet operators gain greater control over their own cryptoeconomics, making it easier to integrate staking, rewards, and penalties with their native token. This is essential for future subnet development, ensuring that staking mechanisms can evolve alongside the growth of specialized token economies.

### 🚀 Features

- _(contracts)_ Token-bound validator collateral (#1130)

### 🐛 Bug Fixes

- _(topdown)_ Pull effects up until committed finality. (#887)

### ⚙️ Miscellaneous Tasks

- Remove Python requirement for contracts development (#1144)

## [axon-r04] - 2024-09-18

_Full changelog below._
Expand Down
20 changes: 6 additions & 14 deletions contracts/Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Targets that are commands.
COMMANDS := deploy-stack gen compile-abi rust-binding slither lint fmt deps build \
test install-dev install-npm-package install-eth-abi storage clean coverage \
prepare build-selector-library forge
prepare forge

# Targets that are prerequisite commands.
PREREQ_COMMANDS := pnpm

# Targets that are not commands.
NON_COMMANDS := node_modules
NON_COMMANDS := node_modules test/helpers/SelectorLibrary.sol

# Declare commands and prerequisite commands as phony targets.
.PHONY: $(COMMANDS) $(PREREQ_COMMANDS)
Expand Down Expand Up @@ -77,17 +77,9 @@ build: node_modules | forge
forge build
FOUNDRY_SRC=sdk forge build

test: node_modules | forge
test: node_modules test/helpers/SelectorLibrary.sol | forge
forge test -vvv --ffi

install-dev: install-npm-package install-eth-abi

install-npm-package: node_modules
pnpm install --save-dev

install-eth-abi:
curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py && python3 get-pip.py && rm get-pip.py && python3 -m pip install eth_abi

storage:
rm -rf ./cache
rm -rf ./cache_hardhat
Expand All @@ -108,10 +100,10 @@ coverage: node_modules | forge
fi
./tools/check_coverage.sh

prepare: build-selector-library fmt lint test slither
prepare: test/helpers/SelectorLibrary.sol fmt lint test slither

build-selector-library: | forge
python3 scripts/python/build_selector_library.py
test/helpers/SelectorLibrary.sol: | forge
pnpm exec hardhat gen-selector-library
pnpm exec prettier -w test/helpers/SelectorLibrary.sol

# Forge is used by the ipc-solidity-actors compilation steps.
Expand Down
1 change: 0 additions & 1 deletion contracts/contracts/errors/IPCErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ error InvalidFederationPayload();
error DuplicatedGenesisValidator();
error NotEnoughGenesisValidators();
error ValidatorPowerChangeDenied();
error IncreaseAllowanceFailed();

enum InvalidXnetMessageReason {
Sender,
Expand Down
7 changes: 7 additions & 0 deletions contracts/contracts/gateway/GatewayManagerFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ contract GatewayManagerFacet is GatewayActorModifiers, ReentrancyGuard {
revert NotEnoughFunds();
}

// The fund flow for stake is from Validator -> SubnetActor -> Gateway.
// Because msg.sender is actually the subnet actor, this method sends the fund from
// the subnet actor caller the gateway.
SubnetActorGetterFacet(msg.sender).collateralSource().lock(amount);

(bool registered, Subnet storage subnet) = LibGateway.getSubnet(msg.sender);
Expand Down Expand Up @@ -95,6 +98,10 @@ contract GatewayManagerFacet is GatewayActorModifiers, ReentrancyGuard {
}

subnet.stake -= amount;

// Release fund flows from Gateway -> SubnetActor -> ReleaseQueue (Locking) -> Validator.
// Because msg.sender is actually the subnet actor, this method sends the fund back to
// the subnet actor caller.
SubnetActorGetterFacet(msg.sender).collateralSource().transferFunds(payable(msg.sender), amount);
}

Expand Down
74 changes: 33 additions & 41 deletions contracts/contracts/lib/AssetHelper.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {NotEnoughBalance, IncreaseAllowanceFailed} from "../errors/IPCErrors.sol";
import {NotEnoughBalance} from "../errors/IPCErrors.sol";
import {Asset, AssetKind} from "../structs/Subnet.sol";
import {EMPTY_BYTES} from "../constants/Constants.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Expand All @@ -12,11 +12,6 @@ import {SubnetActorGetterFacet} from "../subnet/SubnetActorGetterFacet.sol";
library AssetHelper {
using SafeERC20 for IERC20;

error InvalidERC20Address();
error NoBalanceIncrease();
error UnexpectedAsset();
error UnknownAsset();

/// @notice Assumes that the address provided belongs to a subnet rooted on this network,
/// and checks if its supply kind matches the provided one.
/// It reverts if the address does not correspond to a subnet actor.
Expand All @@ -28,9 +23,7 @@ library AssetHelper {
/// It reverts if conditions are not met.
function validate(Asset memory asset) internal view {
if (asset.kind == AssetKind.ERC20) {
if (asset.tokenAddress == address(0)) {
revert InvalidERC20Address();
}
require(asset.tokenAddress != address(0), "Invalid ERC20 address");
// We require that the ERC20 token contract exists beforehand.
// The call to balanceOf will revert if the supplied address does not exist, or if it's not an ERC20 contract.
// Ideally we'd use ERC165 to check if the contract implements the ERC20 standard, but the latter does not support supportsInterface().
Expand All @@ -41,9 +34,7 @@ library AssetHelper {

/// @notice Asserts that the supply strategy is of the given kind. If not, it reverts.
function expect(Asset memory asset, AssetKind kind) internal pure {
if (asset.kind != kind) {
revert UnexpectedAsset();
}
require(asset.kind == kind, "Unexpected asset");
}

/// @notice Locks the specified amount from msg.sender into custody.
Expand All @@ -52,19 +43,19 @@ library AssetHelper {
function lock(Asset memory asset, uint256 value) internal returns (uint256) {
if (asset.kind == AssetKind.ERC20) {
IERC20 token = IERC20(asset.tokenAddress);

// Dealing with deflationary tokens.
uint256 initialBalance = token.balanceOf(address(this));
token.safeTransferFrom({from: msg.sender, to: address(this), value: value});
uint256 finalBalance = token.balanceOf(address(this));
if (finalBalance <= initialBalance) {
revert NoBalanceIncrease();
}
// Safe arithmetic is not necessary because underflow is not possible due to the check above
require(finalBalance > initialBalance, "No balance increase");
// Safe arithmetic is not necessary because underflow is not possible due to the assert above
return finalBalance - initialBalance;
} else {
// now we are handling native token
if (msg.value < value) {
revert NoBalanceIncrease();
}
// Ensure we have received enough funds to cover the value.
// msg.value might have coins in excess of the amount that we need to lock (e.g. when contributing both native collateral and supply at the same time).
// That's why we can't do a strict equality check.
require(msg.value >= value, "Insufficient funds");
}
// Do nothing for native.
return value;
Expand Down Expand Up @@ -94,11 +85,11 @@ library AssetHelper {
) internal returns (bool success, bytes memory ret) {
return
asset.tokenAddress.call(
// using IERC20 transfer instead of safe transfer so we can
// bubble-up the failure instead of reverting on failure so we
// can send the receipt.
abi.encodePacked(IERC20.transfer.selector, abi.encode(recipient, value))
);
// using IERC20 transfer instead of safe transfer so we can
// bubble-up the failure instead of reverting on failure so we
// can send the receipt.
abi.encodePacked(IERC20.transfer.selector, abi.encode(recipient, value))
);
}

/// @notice Calls the target with the specified data, ensuring it receives the specified value.
Expand Down Expand Up @@ -143,7 +134,7 @@ library AssetHelper {
if (ret.length > 0) {
assembly {
let returndata_size := mload(ret)
// see https://ethereum.stackexchange.com/questions/133748/trying-to-understand-solidity-assemblys-revert-function
// see https://ethereum.stackexchange.com/questions/133748/trying-to-understand-solidity-assemblys-revert-function
revert(add(32, ret), returndata_size)
}
}
Expand Down Expand Up @@ -192,7 +183,7 @@ library AssetHelper {
if (address(this).balance < value) {
revert NotEnoughBalance();
}
(bool success, ) = recipient.call{value: value}("");
(bool success,) = recipient.call{value: value}("");
return success;
}

Expand All @@ -205,21 +196,22 @@ library AssetHelper {
}
}

function native() internal pure returns (Asset memory) {
return Asset({kind: AssetKind.Native, tokenAddress: address(0)});
// @notice Makes the asset available for spending by the given spender, without actually sending it.
// @return msgValue The amount of msg.value that needs to be sent along with the subsequent call that will _actually_ spend that asset.
// Will be 0 if the asset is a token, since no native coins are to be sent.
function makeAvailable(Asset memory self, address spender, uint256 amount) internal returns (uint256 msgValue) {
if (self.kind == AssetKind.Native) {
msgValue = amount;
} else if (self.kind == AssetKind.ERC20) {
IERC20 token = IERC20(self.tokenAddress);
token.safeIncreaseAllowance(spender, amount);
msgValue = 0;
}
return msgValue;
}

function isNative(Asset memory self) internal pure returns(bool) {
return self.kind == AssetKind.Native;
function native() internal pure returns (Asset memory) {
return Asset({kind: AssetKind.Native, tokenAddress: address(0)});
}

function increaseAllowance(Asset memory self, address spender, uint256 amount) internal {
if (self.kind == AssetKind.ERC20) {
IERC20 token = IERC20(self.tokenAddress);
uint256 allowance = token.allowance(address(this), spender);
if (!token.approve(spender, allowance + amount)) {
revert IncreaseAllowanceFailed();
}
}
}
}
}
13 changes: 5 additions & 8 deletions contracts/contracts/lib/LibStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -581,15 +581,12 @@ library LibStaking {
s.validatorSet.confirmWithdraw(validator, amount);
s.releaseQueue.addNewRelease(validator, amount);
IGateway(gateway).releaseStake(amount);
} else {
} else if (change.op == StakingOperation.Deposit) {
s.validatorSet.confirmDeposit(validator, amount);

if (s.collateralSource.isNative()) {
IGateway(gateway).addStake{value: amount}(amount);
} else {
s.collateralSource.increaseAllowance(gateway, amount);
IGateway(gateway).addStake(amount);
}
uint256 msgValue = s.collateralSource.makeAvailable(gateway, amount);
IGateway(gateway).addStake{value: msgValue}(amount);
} else {
revert("Unknown staking operation");
}
}

Expand Down
29 changes: 7 additions & 22 deletions contracts/contracts/lib/LibSubnetActor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,28 +159,13 @@ library LibSubnetActor {
function registerInGateway(uint256 collateral) internal {
SubnetActorStorage storage s = LibSubnetActorStorage.appStorage();

uint256 g = s.genesisCircSupply;

bool supplySourceNative = s.supplySource.isNative();
bool collateralSourceNative = s.collateralSource.isNative();

// this method is unnecessarily handling different cases because subnet actor needs
// to "register" in gateway and different token types needs to be attached or approved.
// TODO: it's known that having gateway holding all subnets' funds is insecure, this
// TODO: can be removed once contract redesign is in place.
if (supplySourceNative && collateralSourceNative) {
IGateway(s.ipcGatewayAddr).register{value: g + collateral}(g, collateral);
} else if (!supplySourceNative && collateralSourceNative) {
s.supplySource.increaseAllowance(s.ipcGatewayAddr, g);
IGateway(s.ipcGatewayAddr).register{value: collateral}(g, collateral);
} else if (supplySourceNative && !collateralSourceNative) {
s.collateralSource.increaseAllowance(s.ipcGatewayAddr, collateral);
IGateway(s.ipcGatewayAddr).register{value: g}(g, collateral);
} else {
s.supplySource.increaseAllowance(s.ipcGatewayAddr, g);
s.collateralSource.increaseAllowance(s.ipcGatewayAddr, collateral);
IGateway(s.ipcGatewayAddr).register(g, collateral);
}
uint256 genesisCircSupply = s.genesisCircSupply;

uint256 msgValue = 0;
msgValue += s.supplySource.makeAvailable(s.ipcGatewayAddr, genesisCircSupply);
msgValue += s.collateralSource.makeAvailable(s.ipcGatewayAddr, collateral);

IGateway(s.ipcGatewayAddr).register{value: msgValue}(genesisCircSupply, collateral);
}

/// @notice method that allows the contract owner to set the validators' federated power after
Expand Down
30 changes: 19 additions & 11 deletions contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import './extensions'

// Load environment variables from .env file.
import { config as dotenvConfig } from 'dotenv'

dotenvConfig({ path: './.env' })

// Import our tasks.
Expand All @@ -29,17 +30,7 @@ const networkDefinition = (chainId: number, url: string) => ({
saveDeployments: true,
})

const config: HardhatUserConfig = {
defaultNetwork: 'calibrationnet',
networks: {
// Static networks.
mainnet: networkDefinition(314, 'https://api.node.glif.io/rpc/v1'),
calibrationnet: networkDefinition(314159, 'https://api.calibration.node.glif.io/rpc/v1'),
localnet: networkDefinition(31415926, 'http://localhost:8545'),
// Auto uses RPC_URL provided by the user, and an optional CHAIN_ID.
// If provided, Hardhat will assert that the chain ID matches the one returned by the RPC.
auto: networkDefinition(parseInt(process.env.CHAIN_ID, 10), process.env.RPC_URL!),
},
let config: HardhatUserConfig = {
solidity: {
compilers: [
{
Expand Down Expand Up @@ -67,4 +58,21 @@ const config: HardhatUserConfig = {
},
}

// Only add the network configurations if we have a private key.
// Some targets don't require networks, e.g. gen-selector-library.
if (process.env.PRIVATE_KEY) {
config = Object.assign(config, {
defaultNetwork: 'calibrationnet',
networks: {
// Static networks.
mainnet: networkDefinition(314, 'https://api.node.glif.io/rpc/v1'),
calibrationnet: networkDefinition(314159, 'https://api.calibration.node.glif.io/rpc/v1'),
localnet: networkDefinition(31415926, 'http://localhost:8545'),
// Auto uses RPC_URL provided by the user, and an optional CHAIN_ID.
// If provided, Hardhat will assert that the chain ID matches the one returned by the RPC.
auto: networkDefinition(parseInt(process.env.CHAIN_ID, 10), process.env.RPC_URL!),
},
})
}

export default config
Loading

0 comments on commit 00e14bd

Please sign in to comment.