Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
32 changes: 32 additions & 0 deletions deployments/_deployments_log_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -39088,5 +39088,37 @@
]
}
}
},
"OutputValidator": {
"arbitrum": {
"staging": {
"1.0.0": [
{
"ADDRESS": "0xd54C00CA32eC8Db51B7dBbC73124De83096C850A",
"OPTIMIZER_RUNS": "1000000",
"TIMESTAMP": "2025-08-20 15:59:24",
"CONSTRUCTOR_ARGS": "0x000000000000000000000000156cebba59deb2cb23742f70dcb0a11cc775591f",
"SALT": "",
"VERIFIED": "true",
"ZK_SOLC_VERSION": ""
}
]
}
},
"optimism": {
"staging": {
"1.0.0": [
{
"ADDRESS": "0xd54C00CA32eC8Db51B7dBbC73124De83096C850A",
"OPTIMIZER_RUNS": "1000000",
"TIMESTAMP": "2025-08-20 16:25:58",
"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 @@ -179,7 +179,8 @@
"ReceiverStargateV2": "",
"RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70",
"Patcher": "0x3971A968c03cd9640239C937F8d30D024840E691",
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70"
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70",
"OutputValidator": "0xd54C00CA32eC8Db51B7dBbC73124De83096C850A"
}
}
}
}
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": "0xd54C00CA32eC8Db51B7dBbC73124De83096C850A"
}
6 changes: 4 additions & 2 deletions deployments/optimism.diamond.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@
"ReceiverChainflip": "",
"ReceiverStargateV2": "",
"RelayerCelerIM": "0xa1Ed8783AC96385482092b82eb952153998e9b70",
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70"
"TokenWrapper": "0xF63b27AE2Dc887b88f82E2Cc597d07fBB2E78E70",
"OutputValidator": "0xd54C00CA32eC8Db51B7dBbC73124De83096C850A",
"Patcher": ""
}
}
}
}
3 changes: 2 additions & 1 deletion deployments/optimism.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@
"PioneerFacet": "0x371E61d9DC497C506837DFA47B8dccEF1da30459",
"LidoWrapper": "0x462A9B6879770050021823D63aE62470E65Af8D4",
"Permit2Proxy": "0x808eb38763f3F51F9C47bc93Ef8d5aB7E6241F46",
"GasZipFacet": "0xfEeCe7B3e68B9cBeADB60598973704a776ac3ca1"
"GasZipFacet": "0xfEeCe7B3e68B9cBeADB60598973704a776ac3ca1",
"OutputValidator": "0xd54C00CA32eC8Db51B7dBbC73124De83096C850A"
}
135 changes: 135 additions & 0 deletions docs/OutputValidator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# OutputValidator

## Overview

The `OutputValidator` contract is a periphery contract that validates swap output amounts and handles positive slippage by transferring 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 managed.

## Key Features

- **Output Validation**: Assumes actual output amount exceeds expected amount for gas efficiency
- **Excess Token Management**: Automatically transfers excess tokens to a validation wallet
- **Dual Token Support**: Handles both native (ETH) and ERC20 tokens
- **Gas Optimized**: Eliminates conditional checks and assumes positive slippage scenarios (~200-300 gas saved per call)
- **Comprehensive Test Coverage**: 100% line coverage with 19 test cases

## Contract Logic

### Native Token Flow

1. The calling contract (Diamond) sends native tokens as `msg.value` to the OutputValidator
2. The contract always returns the expected amount to the calling contract using `LibAsset.transferAsset`
3. **Assumes positive slippage**: Always transfers excess to validation wallet (handles zero excess by transferring 0 tokens)

### ERC20 Token Flow

1. The calling contract (Diamond) must have sufficient ERC20 token balance
2. The OutputValidator checks the Diamond's ERC20 balance using `ERC20(tokenAddress).balanceOf(msg.sender)`
3. **Assumes positive slippage**: Always transfers excess to validation wallet (handles zero excess by transferring 0 tokens)
4. The Diamond retains the expected amount

> **Gas Optimization**: The contract assumes `actualAmount > expectedAmount` and eliminates conditional checks. If this assumption is violated, the transaction reverts immediately on arithmetic underflow. Zero excess cases are handled gracefully by transferring 0 tokens.

**Note**: The contract successfully handles edge cases where `actualAmount == expectedAmount` by transferring 0 excess tokens, rather than reverting.

## Functions

### `validateOutput`

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

**Parameters:**

- `tokenAddress`: The address of the token to validate (use `LibAsset.NULL_ADDRESS` for native tokens)
- `expectedAmount`: The expected amount of tokens
- `validationWalletAddress`: The address to send excess tokens to

**Behavior:**

- For native tokens: Receives tokens as `msg.value`, returns expected amount to caller, transfers excess to validation wallet
- For ERC20 tokens: Checks caller's balance, transfers excess to validation wallet using `transferFrom`

## Errors

The contract does not define custom errors. Error handling is delegated to the underlying libraries:

- **Native token errors**: Handled by `LibAsset.transferAsset()`
- **ERC20 token errors**: Handled by `SafeTransferLib.safeTransferFrom()`
- **Input validation**: Handled by `LibAsset` library for null address checks

## Integration

### Example Usage

```solidity
// For native tokens
outputValidator.validateOutput{value: actualAmount}(
LibAsset.NULL_ADDRESS,
expectedAmount,
validationWallet
);

// For ERC20 tokens
// First approve the OutputValidator to spend tokens
token.approve(address(outputValidator), actualAmount);
outputValidator.validateOutput(
address(token),
expectedAmount,
validationWallet
);
```

## Security Considerations

- The contract inherits from `TransferrableOwnership` for secure ownership management
- Uses `SafeTransferLib` for safe ERC20 operations
- Custom errors provide gas-efficient error handling
- Input validation leverages `LibAsset.transferAsset` for null address checks

## Test Coverage

The contract includes comprehensive test coverage with **100% line coverage** including:

### **Core Functionality Tests**

- Native token validation with excess (positive slippage scenarios)
- ERC20 token validation with excess (positive slippage scenarios)
- Edge cases (zero expected amount, insufficient allowance)

### **Integration Tests**

- Complete DEX swap + OutputValidator + Bridge flows
- ERC20 → ERC20 swap with positive slippage
- ERC20 → Native swap with positive slippage
- Native → ERC20 swap with positive slippage

### **Negative Test Cases**

- Insufficient allowance scenarios
- Native transfer failures to invalid addresses
- Zero value with non-zero expected amount
- **No excess scenarios** (contract handles gracefully by transferring 0 tokens)

### **Test Statistics**

- **19 test cases** covering all code paths
- **All branches covered** including edge cases
- **Realistic scenarios** using MockDEX and Diamond integration

> **Note**: Coverage tools may mark comment lines as uncovered, but all executable code lines achieve 100% coverage.

## Deployment

The contract is deployed using the standard deployment script pattern and extracts the owner address from the global configuration file. The contract is automatically included in periphery contract deployments and is configured in `script/deploy/resources/deployRequirements.json`.

### **Deployment Scripts**

- **Standard**: `script/deploy/Periphery/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.
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
39 changes: 39 additions & 0 deletions script/deploy/facets/DeployOutputValidator.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { DeployScriptBase } from "./utils/DeployScriptBase.sol";
import { OutputValidator } from "lifi/Periphery/OutputValidator.sol";
import { stdJson } from "forge-std/Script.sol";

contract DeployScript is DeployScriptBase {
using stdJson for string;

constructor() DeployScriptBase("OutputValidator") {}

function run()
public
returns (OutputValidator deployed, bytes memory constructorArgs)
{
constructorArgs = getConstructorArgs();

deployed = OutputValidator(deploy(type(OutputValidator).creationCode));
}

function getConstructorArgs() internal override returns (bytes memory) {
// get path of global config file
string memory globalConfigPath = string.concat(
root,
"/config/global.json"
);

// read file into json variable
string memory globalConfigJson = vm.readFile(globalConfigPath);

// extract outputValidatorOwner address
address refundWalletAddress = globalConfigJson.readAddress(
".refundWallet"
);

return abi.encode(refundWalletAddress);
}
}
9 changes: 9 additions & 0 deletions script/deploy/resources/deployRequirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,15 @@
}
}
},
"OutputValidator": {
"configData": {
"_owner": {
"configFileName": "global.json",
"keyInConfigFile": ".refundWallet",
"allowToDeployWithZeroAddress": "false"
}
}
},
"ChainflipFacet": {
"configData": {
"_chainflipVault": {
Expand Down
39 changes: 39 additions & 0 deletions script/deploy/zksync/DeployOutputValidator.zksync.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { DeployScriptBase } from "./utils/DeployScriptBase.sol";
import { OutputValidator } from "lifi/Periphery/OutputValidator.sol";
import { stdJson } from "forge-std/Script.sol";

contract DeployScript is DeployScriptBase {
using stdJson for string;

constructor() DeployScriptBase("OutputValidator") {}

function run()
public
returns (OutputValidator deployed, bytes memory constructorArgs)
{
constructorArgs = getConstructorArgs();

deployed = OutputValidator(deploy(type(OutputValidator).creationCode));
}

function getConstructorArgs() internal override returns (bytes memory) {
// get path of global config file
string memory globalConfigPath = string.concat(
root,
"/config/global.json"
);

// read file into json variable
string memory globalConfigJson = vm.readFile(globalConfigPath);

// extract outputValidatorOwner address
address refundWalletAddress = globalConfigJson.readAddress(
".refundWallet"
);

return abi.encode(refundWalletAddress);
}
}
Loading
Loading