Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
29464dd
feat: new validator that blocks V1 calls on selected addresses
arnigudj Aug 22, 2025
31f0320
feat: validator reverts with message for user
arnigudj Aug 22, 2025
9035f75
feat: emit Decision when validate false
arnigudj Aug 25, 2025
5027b04
feat: swap contract with tests
arnigudj Aug 25, 2025
8929f7b
feat: skip transfer for same recipient and added tests
arnigudj Aug 26, 2025
af266e7
feat: swap contract upgradable
arnigudj Aug 26, 2025
8ffab07
docs: swap docs
arnigudj Aug 26, 2025
07788b7
feat: LitePSM-compatible functions
arnigudj Sep 18, 2025
b9c799c
fix: Ackee Wake - AI Audit Report for src/SwapV1V2.sol PR
arnigudj Dec 22, 2025
b6dacbe
fix(Validator): V1_BLOCKED validation bypass vulnerability
arnigudj Feb 5, 2026
369c924
fix(SwapV1V2): wrap permit in try/catch to prevent front-run griefing…
arnigudj Apr 20, 2026
7d5ca14
docs(SwapV1V2): update permit behaviour and test count after L2 fix
arnigudj Apr 20, 2026
2b15f09
fix: address audit findings I1, I2, I3, I4, I5 in Validator and SwapV1V2
arnigudj Apr 20, 2026
ce29e08
test: replace deprecated testFail* pattern with vm.expectRevert
arnigudj Apr 20, 2026
f1bf88f
fix(Validator): block renounceRole bypass and direct V1_BLOCKED_ROLE …
arnigudj Apr 20, 2026
da2dc00
fix: add test case ro reproduce the v2 block failure
arnigudj Apr 20, 2026
4e83ac7
fix(Validator): move V1Block enforcement to ControllerToken _withCall…
arnigudj Apr 20, 2026
91425a5
feat: add upgrade scripts, chain controller tests, and upgrade playbook
arnigudj Apr 21, 2026
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
33 changes: 33 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@ export RPC_URL=
export PRIVATE_KEY=
export ETHERSCAN_API_KEY=

# Sepolia RPC and chain config
export SEPOLIA_RPC=
export ETHEREUM_RPC=

# Gnosis and Polygon block explorers (required by foundry.toml for verification)
export GNOSISSCAN_API=
export POLYGONSCAN_API=

# Upgrade script: proxy addresses per chain (V2 proxies, not V1 frontends)
# Ethereum mainnet
export PROXY_EUR=0x39b8B6385416f4cA36a20319F70D28621895279D
export PROXY_GBP=0x78a20B7AF85156B4389a349Aa4c96efC2E509768
export PROXY_USD=0x05968f40939fdc016AD58F82Cd08dA884825aD55
export PROXY_ISK=0x38D22BD604c4549e2cC15e94B8e22E6FE4aE82B4

# Address to receive ADMIN_ROLE on the new Validator
export VALIDATOR_ADMIN=

# Test accounts for TestUpgradeScenario.s.sol
# These are standard anvil accounts 1-4 — same keys work locally and on Sepolia.
# For Sepolia: fund each address with a small amount of Sepolia ETH from the faucet.
# alice address: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
# bob address: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
# charlie address: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
# dave address: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
export ALICE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
export BOB_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
export CHARLIE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
export DAVE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926b

export ARBITRUM_SEPOLIA_RPC=
export ARBITRUM_SEPOLIA_CHAIN_ID=421614
export ARBISCAN_API_KEY=
Expand All @@ -16,3 +46,6 @@ export OPT_SEPOLIA_RPC=
export OPT_SEPOLIA_CHAIN_ID=11155420
export OPTSCAN_API_KEY=
export OPTSCAN_URL=https://api-sepolia-optimistic.etherscan.io/api

# Required by foundry.toml for Linea — set to dummy if not deploying to Linea
export LINEASCAN_API_KEY=dummy
183 changes: 183 additions & 0 deletions docs/SWAP_V1V2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
## Scope
We have a problem with aggregators like CoW swap being unable to connect the V1 and V2 liquidity together.

## Solution

The `SwapV1V2` contract that enables 1:1 token swapping between V1 and V2 tokens that share the same balance storage. The contract supports five swap methods:

- `swapExactIn`: Basic 1:1 swap with slippage protection
- `swapWithPermitStrict`: Swap with ERC-2612 permit for gasless approvals; permit is attempted but a failed permit (e.g. front-run nonce consumption) does not block the swap if the allowance is already set
- `swapWithPermitBestEffort`: Swap with optional permit that continues even if permit fails
- `sellGem`: LitePSM-compatible function to sell V1 for V2
- `buyGem`: LitePSM-compatible function to buy V1 with V2

### Key features:
- **OpenZeppelin Upgradeable Pattern**: Uses UUPS (Universal Upgradeable Proxy Standard) for future contract upgrades
- 1:1 exchange rate (no reserves needed)
- Reentrancy protection via `ReentrancyGuardUpgradeable`
- Support for ERC-2612 permits
- Slippage protection via minOut parameter
- Owner-controlled upgrades via `OwnableUpgradeable`
- **LitePSM Compatibility**: Provides `sellGem` and `buyGem` functions for aggregator integration
- **Shared State Optimization**: Skips transfers when sender equals recipient (V1/V2 share same storage)

### Contract Implementation

- `SwapV1V2.sol`: Main swap contract that facilitates 1:1 swaps between two tokens
- **Upgradeable Architecture**: Inherits from `Initializable`, `OwnableUpgradeable`, `UUPSUpgradeable`, and `ReentrancyGuardUpgradeable`
- **Proxy Pattern**: Deployed using ERC1967Proxy with proper initialization instead of constructor
- **Storage Compatibility**: Maintains storage layout compatibility with V1/V2 shared storage principle
- **LitePSM Integration**: Added `sellGem` and `buyGem` functions with same 1:1 swap logic
- **Shared State Optimization**: All functions skip transfers when `msg.sender == recipient` since V1/V2 share storage
- Implemented permit functionality with proper calldata decoding (97 bytes packed format)
- Added comprehensive error handling (BadPair, ZeroAmount, slippage)

### Upgradeable Pattern Implementation

The contract follows OpenZeppelin's recommended upgradeable pattern:

```solidity
contract SwapV1V2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(address v1, address v2, address initialOwner) public initializer {
V1 = v1;
V2 = v2;
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
__ReentrancyGuard_init();
}

function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
```

**Key Benefits:**
- **Future-proof**: Contract can be upgraded to add new features or fix issues
- **Owner-controlled**: Only the contract owner can authorize upgrades
- **State preservation**: All contract state (V1, V2 addresses, ownership) is preserved across upgrades
- **Storage safety**: Uses OpenZeppelin's proven upgradeable patterns to avoid storage collisions

### Key Technical Details

- Uses abi.encodePacked for 97-byte permit calldata format: deadline(32) + v(1) + r(32) + s(32)
- Manual decoding of packed permit data in swapWithPermitBestEffort
- Both `swapWithPermitStrict` and `swapWithPermitBestEffort` wrap permit calls in try/catch; a failed permit (e.g. nonce already consumed by a front-runner) does not revert the swap as long as the allowance is already set
- Proper event emissions for all swap operations
- **Proxy deployment**: Uses ERC1967Proxy for transparent upgrades

### 1:1 Price Guarantee

The contract ensures a guaranteed 1:1 exchange rate through:
- Deterministic Quote Function: quote(tokenIn, tokenOut, amountIn) returns amountIn for valid pairs, 0 for invalid pairs
- No Price Discovery: Unlike AMMs, there are no reserves or price calculations - always amountOut = amountIn
- Pair Validation: Only allows swaps between the configured V1 and V2 token addresses
- Direct Transfer Logic: Contract receives amountIn of tokenIn and sends exactly amountIn of tokenOut

```solidity
function quote(address tokenIn, address tokenOut, uint256 amountIn)
external view returns (uint256) {
return _isPair(tokenIn, tokenOut) ? amountIn : 0; // Always 1:1 or 0
}
```

This makes the contract suitable for pathfinders and aggregators that need predictable pricing.

### LitePSM Compatibility Functions

The contract includes two additional functions to provide compatibility with LitePSM-style interfaces expected by some aggregators and solvers:

#### `sellGem(address usr, uint256 gemAmt)`
- **Purpose**: Sell V1 token and receive V2 token (V1 → V2)
- **Parameters**:
- `usr`: Address of the V2 token recipient
- `gemAmt`: Amount of V1 token being sold
- **Returns**: Amount of V2 token received (always equals `gemAmt` since 1:1)
- **Gas Optimization**: Skips transfers when `msg.sender == usr` (shared storage optimization)

#### `buyGem(address usr, uint256 gemAmt)`
- **Purpose**: Buy V1 token with V2 token (V2 → V1)
- **Parameters**:
- `usr`: Address of the V1 token recipient
- `gemAmt`: Amount of V1 token being bought
- **Returns**: Amount of V2 token required (always equals `gemAmt` since 1:1)
- **Gas Optimization**: Skips transfers when `msg.sender == usr` (shared storage optimization)

```solidity
// Example usage
uint256 amountOut = swap.sellGem(recipient, 1e18); // Sell 1 V1 for 1 V2
uint256 amountIn = swap.buyGem(recipient, 1e18); // Buy 1 V1 with 1 V2
```

**Key Benefits:**
- **Aggregator Integration**: Compatible with solvers expecting LitePSM interface patterns
- **Simplified Implementation**: No decimal conversions needed since V1/V2 share same storage and decimals
- **Gas Efficient**: Same shared-state optimization as other swap functions
- **Consistent Events**: Emits same `Swapped` events for unified tracking

### Test Coverage

- 100% test coverage for SwapV1V2 contract
- Tests for all five swap functions with both V1→V2 and V2→V1 directions:
- `swapExactIn`, `swapWithPermitStrict`, `swapWithPermitBestEffort`
- **New**: `sellGem` and `buyGem` LitePSM-compatible functions
- **Shared State Optimization Tests**: Verifies transfers are skipped when `msg.sender == recipient`
- **Upgrade functionality tests**: Verifies state preservation and owner-only upgrade authorization
- Error condition testing (bad pairs, zero amounts, slippage protection)
- Permit functionality testing (valid signatures, invalid calldata)
- Initialization validation tests (replaces constructor tests for upgradeable pattern)
- Event emission verification for all functions
- Edge cases with different recipient addresses
- Reentrancy attack protection verification

## How to test

Run the full test suite:

`forge test --match-contract SwapV1V2Test`

Run with coverage:

`forge coverage --match-contract SwapV1V2Test`

Key Test Cases:

- ✅ Basic V1 ↔ V2 swaps in both directions (`swapExactIn`)
- ✅ Permit-based swaps (strict and best-effort modes)
- ✅ **LitePSM-compatible functions**: `sellGem` and `buyGem`
- ✅ **Shared State Optimization**: Same-address transfers skipped for all functions
- ✅ **Upgrade functionality and owner-only authorization**
- ✅ **Proxy deployment and initialization**
- ✅ Error handling (bad pairs, zero amounts, slippage)
- ✅ Initialization validation (zero addresses, same addresses)
- ✅ Event emissions for all swap functions
- ✅ Different recipient addresses
- ✅ Permit signature validation and allowance setting
- ✅ **Front-run griefing protection**: `swapWithPermitStrict` succeeds when permit nonce is consumed by a third party before the swap lands
- ✅ Reentrancy protection

All tests pass (42/42) and achieve 100% line and branch coverage for the SwapV1V2 contract.

### Deployment Instructions

For upgradeable contracts, deployment follows the proxy pattern:

```solidity
// 1. Deploy implementation
SwapV1V2 implementation = new SwapV1V2();

// 2. Deploy proxy with initialization
bytes memory initData = abi.encodeWithSelector(
SwapV1V2.initialize.selector,
v1Address,
v2Address,
ownerAddress
);
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);

// 3. Use proxy address for interactions
SwapV1V2 swapContract = SwapV1V2(address(proxy));
```
Loading