Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1d4a5f1
create new contract OutputValidator + tests + docs
0xDEnYO Aug 20, 2025
98a74d4
deploy to OPT and ARB staging
0xDEnYO Aug 20, 2025
2a7abcc
some smaller adjustments
0xDEnYO Aug 20, 2025
8a7c1a1
add constructor parameter validation
0xDEnYO Aug 20, 2025
2a9abaf
Update test/solidity/Periphery/OutputValidator.t.sol
0xDEnYO Aug 20, 2025
74ddaf5
Merge branch 'main' of github.com:lifinance/contracts into output-val…
0xDEnYO Aug 26, 2025
0c67927
Logic in OutputValidator updated
0xDEnYO Aug 26, 2025
dbd39aa
update conventions
0xDEnYO Aug 26, 2025
8717c9b
update deploy requirements
0xDEnYO Aug 26, 2025
48d9599
update deploy scripts
0xDEnYO Aug 26, 2025
f1c3e04
unit tests updated (100%)
0xDEnYO Aug 26, 2025
7b2b1a0
add withdrawablePeriphery as parent contract
0xDEnYO Aug 26, 2025
a5dd76a
update deploy requirements
0xDEnYO Aug 26, 2025
62e13ed
update deploy scripts
0xDEnYO Aug 26, 2025
5223a5e
update docs
0xDEnYO Aug 26, 2025
5e2770d
increase unit test coverage to 100%
0xDEnYO Aug 26, 2025
30f2c31
some smaller changes
0xDEnYO Aug 26, 2025
3a1d8fd
re-added complex test cases
0xDEnYO Aug 26, 2025
ca79be2
remove unnecessary content from docs file
0xDEnYO Aug 26, 2025
34f69f9
Update docs/OutputValidator.md
0xDEnYO Aug 26, 2025
9b41cef
Update src/Periphery/OutputValidator.sol
0xDEnYO Aug 26, 2025
27c23fb
Update docs/OutputValidator.md
0xDEnYO Aug 26, 2025
e15e372
Merge branch 'main' of github.com:lifinance/contracts into output-val…
0xDEnYO Aug 26, 2025
60ecd6a
Merge branch 'output-validator-lf-15125' of github.com:lifinance/cont…
0xDEnYO Aug 26, 2025
a558756
update comments
0xDEnYO Aug 26, 2025
97b1893
harmonize tests
0xDEnYO Aug 26, 2025
c78d983
update logic in OutputValidator
0xDEnYO Aug 26, 2025
4b50606
update docs
0xDEnYO Aug 26, 2025
514f66c
deployed to OPT/ARB staging
0xDEnYO Aug 26, 2025
9427846
updated unit test
0xDEnYO Aug 26, 2025
ab084c4
verify staging deployments
0xDEnYO Aug 26, 2025
38e0ade
update unit tests
0xDEnYO Aug 26, 2025
9cb5553
add outputValidator signatures to sigs.json
0xDEnYO Sep 24, 2025
17231bf
Merge branch 'main' of github.com:lifinance/contracts into output-val…
0xDEnYO Oct 1, 2025
3a1012a
remove duplicate entries from diamond logs
0xDEnYO Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,18 +524,48 @@ Bash scripts provide the robust deployment framework with automated retry mechan
- **Deployment Scripts:**

- Inherit `DeployScriptBase`
- Use JSON config with `stdJson`
- Define `getConstructorArgs()` if needed
- Encode constructor arguments
- Call `deploy()` with `type({ContractName}).creationCode`
- Example JSON handling:

- **For contracts WITHOUT constructor arguments:**
```solidity
string memory path = string.concat(root, "/config/{facetName}.json");
address configValue = _getConfigContractAddress(
path,
string.concat(".{key}.", network, ".{subkey}")
);
contract DeployScript is DeployScriptBase {
constructor() DeployScriptBase("ContractName") {}

function run() public returns (ContractName deployed) {
deployed = ContractName(deploy(type(ContractName).creationCode));
}
}
```
- DO NOT implement `getConstructorArgs()` function
- DO NOT import `stdJson`
- Return only the deployed contract

- **For contracts WITH constructor arguments:**
```solidity
contract DeployScript is DeployScriptBase {
using stdJson for string;

constructor() DeployScriptBase("ContractName") {}

function run() public returns (ContractName deployed, bytes memory constructorArgs) {
constructorArgs = getConstructorArgs();
deployed = ContractName(deploy(type(ContractName).creationCode));
}

function getConstructorArgs() internal override returns (bytes memory) {
// JSON config handling
string memory path = string.concat(root, "/config/{facetName}.json");
address configValue = _getConfigContractAddress(
path,
string.concat(".{key}.", network, ".{subkey}")
);
return abi.encode(configValue);
}
}
```
- Import `stdJson` for configuration
- Implement `getConstructorArgs()` function
- Return both deployed contract AND constructor args

- **Update Scripts:**
- Inherit `UpdateScriptBase`
Expand Down
32 changes: 32 additions & 0 deletions deployments/_deployments_log_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -39428,5 +39428,37 @@
]
}
}
},
"OutputValidator": {
"arbitrum": {
"staging": {
"1.0.0": [
{
"ADDRESS": "0x266f7a44969d0003AFD94cdf96bdfdEDd8754133",
"OPTIMIZER_RUNS": "1000000",
"TIMESTAMP": "2025-08-26 15:55:33",
"CONSTRUCTOR_ARGS": "0x000000000000000000000000156cebba59deb2cb23742f70dcb0a11cc775591f",
"SALT": "",
"VERIFIED": "true",
"ZK_SOLC_VERSION": ""
}
]
}
},
"optimism": {
"staging": {
"1.0.0": [
{
"ADDRESS": "0x266f7a44969d0003AFD94cdf96bdfdEDd8754133",
"OPTIMIZER_RUNS": "1000000",
"TIMESTAMP": "2025-08-26 15:55:52",
"CONSTRUCTOR_ARGS": "0x000000000000000000000000156cebba59deb2cb23742f70dcb0a11cc775591f",
"SALT": "",
"VERIFIED": "true",
"ZK_SOLC_VERSION": ""
}
]
}
}
}
}
5 changes: 3 additions & 2 deletions deployments/arbitrum.diamond.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@
"ReceiverStargateV2": "",
"RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70",
"Patcher": "0x3971A968c03cd9640239C937F8d30D024840E691",
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70"
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70",
"OutputValidator": "0x266f7a44969d0003AFD94cdf96bdfdEDd8754133"
}
}
}
}
5 changes: 3 additions & 2 deletions deployments/arbitrum.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@
"ChainflipFacet": "0xa884c21873A671bD010567cf97c937b153F842Cc",
"LiFiDEXAggregator": "0x14aB08312a1EA45F76fd83AaE89A3118537FC06D",
"Patcher": "0x18069208cA7c2D55aa0073E047dD45587B26F6D4",
"WhitelistManagerFacet": "0x603f0c31B37E5ca3eA75D5730CCfaBCFF6D17aa3"
}
"WhitelistManagerFacet": "0x603f0c31B37E5ca3eA75D5730CCfaBCFF6D17aa3",
"OutputValidator": "0x266f7a44969d0003AFD94cdf96bdfdEDd8754133"
}
5 changes: 4 additions & 1 deletion deployments/optimism.diamond.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@
"ReceiverAcrossV3": "0x3877f47B560819E96BBD7e7700a02dfACe36D696",
"ReceiverChainflip": "",
"ReceiverStargateV2": "",
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70"
"RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70",
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70",
"OutputValidator": "0x266f7a44969d0003AFD94cdf96bdfdEDd8754133",
"Patcher": ""
}
}
}
1 change: 1 addition & 0 deletions deployments/optimism.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@
"LidoWrapper": "0x462A9B6879770050021823D63aE62470E65Af8D4",
"Permit2Proxy": "0x808eb38763f3F51F9C47bc93Ef8d5aB7E6241F46",
"GasZipFacet": "0xfEeCe7B3e68B9cBeADB60598973704a776ac3ca1",
"OutputValidator": "0x266f7a44969d0003AFD94cdf96bdfdEDd8754133",
"GlacisFacet": "0x36e1375B0755162d720276dFF6893DF02bd49225"
}
194 changes: 194 additions & 0 deletions docs/OutputValidator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# OutputValidator

## Overview

The `OutputValidator` contract is a periphery contract that validates swap output amounts and transfers excess tokens to a designated validation wallet. It is designed to be called by the Diamond contract after a swap operation to ensure that any excess output tokens are properly distributed. The contract inherits from `WithdrawablePeriphery`, providing token recovery functionality for the contract owner.

**Key Design Philosophy**: This contract is designed to not hold any funds, which is why it's safe to work with full balances. Accidentally stuck funds can easily be recovered using the provided public functions.

## Key Features

- **Excess Distribution Management**: Intelligently distributes excess tokens to validation wallet
- **Dual Token Support**: Handles both native (ETH) and ERC20 tokens with separate functions
- **Owner-based Access Control**: Inherits from WithdrawablePeriphery for secure token management
- **Gas Optimized**: Minimal validation for maximum efficiency (does not validate expected amounts to save gas)
- **Token Recovery**: Owner can withdraw accidentally stuck tokens
- **No Fund Retention**: Contract never retains funds permanently, minimizing risk

## Contract Logic

### Native Token Flow

1. The calling contract (Diamond) forwards some or all native output as `msg.value` for excess handling.
2. **Compute pre-call balance and excess**:
- `preCallBalance = msg.sender.balance + msg.value` (caller’s balance before invoking this function)
- `excessAmount = preCallBalance - expectedAmount`
3. **Distribution**:
- Sends `min(excessAmount, msg.value)` to the validation wallet.
- Refunds the remainder `msg.value - min(excessAmount, msg.value)` back to the caller.

Integration note: If you intend to forward all excess, ensure `msg.value >= excessAmount`; otherwise any residual excess stays with the caller by design.

**Note**: This function requires `msg.value` to work as expected, otherwise it cannot determine how much excess exists.

### ERC20 Token Flow

1. The calling contract (Diamond) must have sufficient ERC20 token balance and approve this contract
2. **Calculates excess**: `excessAmount = ERC20(tokenAddress).balanceOf(msg.sender) - expectedAmount`
3. **Transfer excess**: If `excessAmount > 0`, transfers excess tokens to validation wallet via `transferFrom`
4. **Safety checks**: Validates wallet address and handles zero excess gracefully

> **Design Philosophy**: The contract handles excess distribution only. Users receive their `expectedAmount` through the primary swap mechanism, while this contract ensures proper excess management without holding funds permanently. The contract does not validate expected amounts to save gas, and tokens are never lost - even if amount == 0, all tokens will be forwarded to the validation wallet.

## Functions

### `validateNativeOutput`

```solidity
function validateNativeOutput(
uint256 expectedAmount,
address validationWalletAddress
) external payable
```

**Parameters:**

- `expectedAmount`: The expected amount of native tokens (minAmountOut)
- `validationWalletAddress`: The address to send excess tokens to

**Behavior:**

- Calculates pre-call balance as `preCallBalance = msg.sender.balance + msg.value` and excess as `excess = preCallBalance - expectedAmount`
- Distributes `min(excess, msg.value)` to the validation wallet and refunds the remainder to the caller
- Designed for scenarios where `msg.value` represents a portion sent for excess handling

### `validateERC20Output`

```solidity
function validateERC20Output(
address tokenAddress,
uint256 expectedAmount,
address validationWalletAddress
) external
```

**Parameters:**

- `tokenAddress`: The address of the ERC20 token to validate
- `expectedAmount`: The expected amount of tokens (minAmountOut)
- `validationWalletAddress`: The address to send excess tokens to

**Behavior:**

- Checks caller's token balance and calculates excess
- Transfers excess to validation wallet if `excessAmount > 0`
- Validates wallet address and requires sufficient allowance

### `withdrawToken` (inherited from WithdrawablePeriphery)

```solidity
function withdrawToken(
address assetId,
address payable receiver,
uint256 amount
) external onlyOwner
```

**Parameters:**

- `assetId`: The address of the token to withdraw (address(0) for native tokens)
- `receiver`: The address to receive the withdrawn tokens
- `amount`: The amount of tokens to withdraw

**Behavior:**

- Allows the contract owner to withdraw accidentally stuck tokens
- Supports both native and ERC20 token withdrawals
- Emits `TokensWithdrawn` event on successful withdrawal

## Errors

The contract inherits errors from WithdrawablePeriphery and defines custom errors:

- **UnAuthorized**: Thrown when non-owner tries to withdraw tokens
- **InvalidCallData**: Thrown when validation wallet address is zero
- **Native token errors**: Handled by `LibAsset.transferAsset()`
- **ERC20 token errors**: Handled by `SafeTransferLib.safeTransferFrom()`

## Events

### `TokensWithdrawn` (inherited from WithdrawablePeriphery)

```solidity
event TokensWithdrawn(
address assetId,
address payable receiver,
uint256 amount
);
```

Emitted when the owner successfully withdraws tokens from the contract.

## Integration

### Example Usage

```solidity
// For native tokens - send portion of output for excess handling
outputValidator.validateNativeOutput{value: portionForExcess}(
expectedAmount,
validationWallet
);

// For ERC20 tokens
// First approve the OutputValidator to spend excess tokens
token.approve(address(outputValidator), excessAmount);
outputValidator.validateERC20Output(
address(token),
expectedAmount,
validationWallet
);

// Owner can withdraw stuck tokens
outputValidator.withdrawToken(
address(token),
payable(owner),
stuckAmount
);
```

## Security Considerations

- **Owner-based Access Control**: Only the contract owner can withdraw stuck tokens
- **Safe Transfers**: Uses `SafeTransferLib` for safe ERC20 operations and `LibAsset` for native transfers
- **Input Validation**: ERC20 function validates wallet addresses; native transfers rely on `LibAsset` validation
- **Fail-Safe Behavior**: Reverts on arithmetic underflow when actual < expected amounts
- **No Fund Retention**: Contract never retains funds permanently, minimizing risk
- **Inheritance Security**: Inherits proven security patterns from WithdrawablePeriphery

## Deployment

The contract is deployed using the standard deployment script pattern with an owner parameter. The contract is automatically included in periphery contract deployments and is configured in `script/deploy/resources/deployRequirements.json`.

### **Deployment Scripts**

- **Standard**: `script/deploy/facets/DeployOutputValidator.s.sol`
- **zkSync**: `script/deploy/zksync/DeployOutputValidator.zksync.s.sol`

Both scripts follow the established deployment patterns and integrate with the CREATE3Factory for predictable contract addresses. The owner parameter is read from the global configuration file (`config/global.json`) using the `refundWallet` address.

### **Constructor Parameters**

- `_owner`: The address that will have access to withdraw stuck tokens (typically the same as refund wallet owner)

### **Configuration**

The contract owner is configured via the global configuration file:

```json
{
"refundWallet": "0x..."
}
```

This ensures consistent ownership with the refund wallet for token recovery operations.
9 changes: 5 additions & 4 deletions script/deploy/deploySingleContract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ deploySingleContract() {
if ! isZkEvmNetwork "$NETWORK"; then
# prepare bytecode
BYTECODE=$(getBytecodeFromArtifact "$CONTRACT")

# get CREATE3_FACTORY_ADDRESS
CREATE3_FACTORY_ADDRESS=$(getCreate3FactoryAddress "$NETWORK")
checkFailure $? "retrieve create3Factory address from networks.json"
Expand Down Expand Up @@ -163,7 +163,7 @@ deploySingleContract() {
if [[ $CONTRACT == "LiFiDiamond" && $DEPLOY_TO_DEFAULT_DIAMOND_ADDRESS == "true" ]]; then
CONTRACT_ADDRESS="0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE"
else
CONTRACT_ADDRESS=$(getContractAddressFromSalt "$DEPLOYSALT" "$NETWORK" "$CONTRACT" "$ENVIRONMENT")
CONTRACT_ADDRESS=$(getContractAddressFromSalt "$DEPLOYSALT" "$NETWORK" "$CONTRACT" "$ENVIRONMENT" "$CREATE3_FACTORY_ADDRESS")
fi

# check if address already contains code (=> are we deploying or re-running the script again?)
Expand Down Expand Up @@ -229,10 +229,11 @@ deploySingleContract() {
fi

# check the return code the last call
elif [ $RETURN_CODE -eq 0 ]; then
else
# extract deployed-to address from return data
ADDRESS=$(extractDeployedAddressFromRawReturnData "$RAW_RETURN_DATA" "$NETWORK")
if [[ $? -ne 0 ]]; then

if [[ $? -ne 0 || -z $ADDRESS ]]; then
error "❌ Could not extract deployed address from raw return data"
return 1
elif [[ -n "$ADDRESS" ]]; then
Expand Down
Loading
Loading