> For the complete documentation index, see [llms.txt](https://funarchy.gitbook.io/funarchy/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://funarchy.gitbook.io/funarchy/introduction/analyzed-target/azuro.md).

# Azuro

## Configuration

### Create Market

Azuro에서 시장 생성은 LiveCore의 `createCondition()`을 통해 수행되며, `conditionId`를 키로 Condition 상태(Active), 게임 ID, 승리 결과 수 등을 저장한다. 이 과정은 permissionless가 아니라 LP→Access의 `checkAccess()` 검증을 통과한 운영 주체만 수행할 수 있다.

### Market Creation Permission

```solidity
function checkAccess(
    address account,
    address target,
    bytes4 selector
) public {
    access.checkAccess(account, target, selector);
}
```

LP.checkAccess는 권한 판정 로직을 직접 갖지 않고 Access에 위임하여 권한 정책을 중앙 관리한다.

```solidity
function checkAccess(address account, address target, bytes4 selector) external view {
    if ((functionRoles[getFunctionId(target, selector)] & userRoles[account]) == 0)
        revert AccessNotGranted();
}
```

Access.checkAccess는 비트마스크 기반 role 매칭 로직이다. 접근을 요청하는 함수에 대해 해당 계정이 권한을 갖고 있는지 역할 기반 접근제어를 수행한다.

### RuleBook

룰북은 시장마다 온체인으로 따로 구분하지 않고 통합된 공식 룰북을 사용한다.

{% hint style="info" %}
[Azuro: rules-and-settlements-policy](https://gem.azuro.org/knowledge-hub/additional/rules-and-settlements-policy)
{% endhint %}

***

## Trading

### Liquidity Model

CLOB 거래소가 사용자 간 포지션 교환(P2P)을 전제로 하는 것과 달리, Azuro에서는 모든 베팅이 유동성 제공자를 상대방으로 하는 단방향 계약이며, 트레이딩의 본질은 사용자 간 매수·매도가 아니라 LP와 베팅자 간의 손익 분배(Peer-to-Pool)에 있다. \
사용자는 다른 사용자의 포지션을 매수하거나 매도하지 않으며, 각 베팅은 유동성 풀에 대해 체결되고, 그 결과는 LP의 손익으로 귀결된다.

#### (1) Bet Execution

**(1-A) LP가 금액을 수령하고 Core에 전달**

```solidity
function betOrder(
    address core,
    OrderData calldata order,
    address betOwner,
    bytes calldata data
) external isActive(core) returns (uint256[] memory) {
    if (msg.sender != relayer) revert IncorrectRelayer();

    uint128 minBet = _getCore(core).minBet;

    // get bets count and amounts from the `order`
    (uint128 totalAmount, ) = IBetting(core).getOrderBetsAmounts(order);

    _deposit(totalAmount);

    return IBetting(core).putOrder(order, betOwner, minBet, data);
}
```

`totalAmount`를 LP가 직접 수령하고, Core의 `putOrder()`로 넘겨 베팅을 발행/기록한다.

**(1-B) 실제 자금은 LP 컨트랙트로 들어옴**

```solidity
function _deposit(uint128 amount) internal {
    TransferHelper.safeTransferFrom(
        token,
        msg.sender,
        address(this),
        amount
    );
}
```

거래 상대방(다른 사용자)에게 토큰이 이동하는 게 아니라, LP 컨트랙트로 자금이 모이기에 거래 상대방은 항상 유동성 풀이다.

#### (2) Settlement Flow

**(2-A) 정산 결과는 LP 유동성에 반영**

```solidity
function addReserve(
    uint128 lockedReserve,
    uint128 finalReserve,
    uint48 depositId,
    bool isCombo
) external override isCore(msg.sender) {
    if (finalReserve > lockedReserve) {
        uint128 profit = finalReserve - lockedReserve;
        vault.addLiquidity(profit, depositId);
    } else {
        if (lockedReserve - finalReserve > 0) {
            uint128 loss = lockedReserve - finalReserve;
            vault.withdrawLiquidityFor(address(this), loss, depositId);
        }
    }
    if (lockedReserve > 0)
        _reduceLockedLiquidity(msg.sender, lockedReserve, isCombo);
}
```

* `lockedReserve`: 활성 시장 동안 LP가 잠가 둔 최대 지급 의무
* `finalReserve`: 결과 확정 후 실제 정산 값

`LP.addReserve()`는 차이(`finalReserve - lockedReserve`)를 profit/loss로 보고,

* profit이면 `vault.addLiquidity(profit, depositId)`
* loss이면 `vault.withdrawLiquidityFor(..., loss, depositId)`\
  로 LiquidityTree 분배/차감을 트리거한다.

정산의 결과가 특정 사용자에게서 다른 사용자에게 이동하는 게 아니라, LP의 유동성(=Vault/LiquidityTree)에 이익/손실로 누적된다.

#### (3) LiquidityTree

{% hint style="info" %}
LiquidityTree는 세그먼트 트리를 기반으로, 제공된 유동성을 추적하는 데 사용되는 데이터 구조입니다.

[Azuro: liquidity-tree](https://gem.azuro.org/knowledge-hub/how-azuro-works/liquidity-tree)\
[Github: LiquidityTree](https://github.com/Azuro-protocol/LiquidityTree)
{% endhint %}

Vault는 `LiquidityTree`를 상속하고 있고, 예치·이익 분배·출금 제한을 수행한다.

**(3-A) 예치**

```solidity
function addDeposit(
    address depositor,
    uint128 amount
) external onlyAdmin returns (uint48 depositId) {
    _deposit(msg.sender, amount);
    depositId = _nodeAddLiquidity(amount);
    _mint(depositor, depositId);
}
```

예치 시 새로운 leaf(`depositId`)가 생성되고, LP 수익 분배는 이 leaf 단위로 추적된다.

**(3-B) 이익 분배**

```solidity
function addLiquidity(uint128 amount, uint48 depositId) external onlyAdmin {
    _deposit(msg.sender, amount);
    _addLimit(amount, depositId);
}
```

특정 `depositId`까지의 범위에 이익이 공유되고, 세그먼트 트리 기반 range accounting을 사용한다.

**(3-C) 출금 제한**

```solidity
if (withdrawnAmount > (topNodeAmount - lockedLiquidity))
    revert LiquidityIsLocked();
```

활성 베팅으로 인해 잠긴 유동성(`lockedLiquidity`)은 출금 불가하고, LP의 지급 의무가 우선 보호된다.

### Combo Betting

Azuro는 단일 베팅(Ordinary) 외에 콤보(Combo) 베팅을 지원하는데, 콤보 베팅은 여러 Condition의 outcome을 하나의 베팅으로 묶어 단일 금액(stake) 을 걸고, 모든 선택이 적중해야 승리한다. 이때 최종 배당은 각 선택의 `odds`가 결합(곱셈)되며, 그 결과 `payout`이 기하급수적으로 증가할 수 있다.&#x20;

이러한 payout 증폭은 LP 입장에서 잠재 지급 의무(liability)를 급격히 키우므로, Azuro는 콤보 베팅에 대해 `lockedLiquidityCombo`를 별도로 집계하고 전용 한도(`maxLiquidityCombo`)를 두어 리스크를 제한한다.

#### Odds & Payout

```solidity
newBet.payout =
    uint128(uint256(newBet.stake) * uint256(condData.odds[0]) / 1e12);
```

Azuro에서 `odds`는 1e12 스케일의 고정소수점 배당 계수이고, `payout`은 베팅이 적중했을 때 지급되는 원금 포함 총 지급액이다. 따라서 단일 베팅의 잠재 지급액은 코드처럼 `payout = stake × odds / 1e12`로 계산된다.

* `odds`: 1e12 스케일 고정소수점 배당 계수
* `payout`: 승리 시 지급되는 원금 포함 총 지급액
* `payout = stake × odds / 1e12`

#### (1) Structure

여러 Condition/outcome을 한 번에 묶고 단일 금액(amount) 베팅

**(1-A) ComboPart 정의**

```solidity
string private constant _COMBOPART_TYPE =
    "ComboPart(uint256 conditionId,uint128 outcomeId)";
bytes32 public constant _COMBOPART_TYPE_TYPEHASH =
    keccak256(abi.encodePacked(_COMBOPART_TYPE));
```

각 레그는 `(conditionId, outcomeId)`만 가진다.

**(1-B) 단일 금액 구조**

```solidity
string private constant _CLIENTCOMBOBETDATA_TYPE =
    "ClientComboBetData(ClientData clientData,uint64 minOdds,uint128 amount,uint256 nonce,ComboPart[] bets)";
bytes32 private constant _CLIENTCOMBOBETDATA_TYPE_HASH =
    keccak256(
        abi.encodePacked(
            _CLIENTCOMBOBETDATA_TYPE,
            _CLIENTDATA_TYPE,
            _COMBOPART_TYPE
        )
    );
```

* `amount`: 콤보 전체에 적용되는 베팅 금액
* `comboParts[]`: 각 콤보 leg를 구성하는 `(conditionId, outcomeId)` 쌍의 배열

콤보는 여러 선택을 하나의 베팅 단위로 묶는다.

**(1-C) Relayer는 comboParts 배열을 해시로 묶어 서명 구조를 만듦**

```solidity
} else if (order.betType == BetType.COMBO) {
    ComboPart memory subBet;
    ClientComboBetData memory data = abi.decode(
        order.clientBetData,
        (ClientComboBetData)
    );
    length = data.comboParts.length;
    subBetHashes = new bytes32[](length);

    for (uint256 j; j < length; ++j) {
        subBet = data.comboParts[j];
        subBetHashes[j] = keccak256(
            abi.encode(
                _COMBOPART_TYPE_TYPEHASH,
                subBet.conditionId,
                subBet.outcomeId
            )
        );
    }

    structHash = keccak256(
        abi.encode(
            _CLIENTCOMBOBETDATA_TYPE_HASH,
            /* clientData hash */,
            data.minOdds,
            data.amount,
            data.nonce,
            keccak256(abi.encodePacked(subBetHashes))
        )
    );
}
```

여러 레그를 1개의 콤보로 묶은 그 전체가 하나의 서명 단위이다.

Relayer는 `comboParts[]` 배열을 그대로 서명하지 않고, 각 요소를 개별 해시로 만든 뒤 이를 다시 하나의 해시로 결합한다.\
이를 통해 콤보의 구성 순서·내용이 단 하나라도 바뀌면 서명이 무효화되고, Relayer가 임의로 콤보 구성을 수정해 제출하는 것을 방지한다. 즉, 콤보 구성의 원자성은 EIP-712 서명 레벨에서 보장된다.

#### (2) Risk Control

payout 급증 리스크는 lockedLiquidityCombo 별도 집계 및 전용 한도로 관리

**(2-A) 콤보 전용 집계**

```solidity
coreData.lockedLiquidity += _deltaReserve;
if (isCombo) coreData.lockedLiquidityCombo += _deltaReserve;
```

콤보 베팅은 별도로 `lockedLiquidityCombo`에 누적한다.

**(2-B) 콤보 전용 한도**

```solidity
(uint128 maxLiquidity, uint128 maxLiquidityCombo) =
    getLockedLiquidityLimit(msg.sender);

if (coreData.lockedLiquidity > maxLiquidity)
    revert LockedLiquidityLimitReached();
if (isCombo && coreData.lockedLiquidityCombo > maxLiquidityCombo)
    revert LockedLiquidityComboLimitReached();
```

콤보는 별도 상한선을 초과하면 즉시 revert한다.

**(2-C) 보수적 기본 파라미터**

```solidity
coreData.reinforcementAbility = uint64(FixedMath.ONE);
coreData.reinforcementAbilityCombo = uint64(FixedMath.ONE / 2);

emit CoreSettingsUpdated(
    core,
    CoreState.ACTIVE,
    uint64(FixedMath.ONE),
    uint64(FixedMath.ONE / 2), // 50% as default part for combo
    1
);
```

콤보는 odds가 곱으로 결합되므로 payout이 기하급수적으로 증가할 수 있다. \
따라서 LP 지급 의무를 통제하기 위해 코어 기본 설정부터 콤보에 더 보수적인 강화 계수 적용하는를 별도 리스크 모델이 적용된다.

### AzuroBet Token (ERC-721)

Azuro에서 베팅은 단순히 내부 매핑에 기록되는 데이터가 아니라, ERC-721 NFT로 발행된다.\
이 NFT는 베팅의 식별자이자, 정산 청구권의 소유권을 표현하는 수단이다.

#### (1) Mint

```solidity
modifier onlyCore() {
    if (msg.sender != core) revert OnlyCore();
    _;
}

function mint(
    address account
) external override onlyCore returns (uint256 tokenId) {
    tokenId = ++lastTokenId;
    super._mint(account, tokenId);
}
```

AzuroBet은 ERC-721이며, `mint()`는 `onlyCore`로 제한된다. 즉, 사용자가 직접 발행할 수 없고 LiveCore가 베팅 생성 시점에만 발행한다.

`tokenId`는 단순 NFT ID가 아니라, 베팅의 고유 식별자(betId) 역할을 한다.

이 구조는 베팅이 온체인 자산으로 토큰화된다는 점에서 CLOB 기반 예측시장과 구조적으로 다르다.

#### (2) Bet = tokenId

```solidity
Bet storage bet = betsMapping[tokenId];
```

LiveCore는 베팅 상태를 `betsMapping[tokenId]`로 관리한다.

```solidity
function resolvePayout(uint256 tokenId) external override onlyLp
    returns (address account, uint128 payout)
{
    Bet storage bet = betsMapping[tokenId];

    ...

    account = _betOwner(tokenId);
    return (account, payout);
}
```

정산 함수 `resolvePayout(tokenId)`가 입력값을 tokenId로 받는다.

```solidity
function _betOwner(uint256 tokenId) internal view returns (address) {
    (bool success, bytes memory result) = azuroBet.staticcall(
        abi.encodeWithSignature("ownerOf(uint256)", tokenId)
    );
    require(success && result.length == 32, "ownerOf call failed");
    return abi.decode(result, (address));
}
```

베팅의 소유자는 내부 매핑이 아니라 NFT 소유자(ownerOf)로 결정되고, 정산 금액은 NFT 보유자에게 귀속된다.

즉, 베팅 청구권은 NFT 소유권에 종속된다.

<details>

<summary></summary>

#### ClientData: 콤보 베팅의 실행 환경 제약

```solidity
string private constant _CLIENTDATA_TYPE =
    "ClientData(string attention,address affiliate,address core,uint256 expiresAt,uint256 chainId,uint256 relayerFeeAmount,bool isFeeSponsored,bool isBetSponsored,bool isSponsoredBetReturnable)";
```

콤보 베팅 서명에는 `ClientData`가 함께 포함된다.

* `core`\
  → 특정 LiveCore 인스턴스에만 유효한 베팅임을 강제 (cross-core replay 방지)
* `expiresAt`\
  → 일정 시점 이후 베팅 실행 불가 (stale order 방지)
* `chainId`\
  → 체인 간 서명 재사용 방지

따라서 콤보 베팅은 “어느 체인에서, 어느 코어에, 언제까지 실행 가능한지”가 서명 수준에서 고정된다.

```solidity
function changeLockedLiquidity(int128 deltaReserve, bool isCombo)
    external override isActive(msg.sender)
{
    if (deltaReserve > 0) {
        uint128 _deltaReserve = uint128(deltaReserve);

        CoreData storage coreData = _getCore(msg.sender);
        coreData.lockedLiquidity += _deltaReserve;
        if (isCombo) coreData.lockedLiquidityCombo += _deltaReserve;

        vault.lockLiquidity(_deltaReserve);

        (uint128 maxLiquidity, uint128 maxLiquidityCombo) =
            getLockedLiquidityLimit(msg.sender);

        if (coreData.lockedLiquidity > maxLiquidity)
            revert LockedLiquidityLimitReached();
        if (isCombo && coreData.lockedLiquidityCombo > maxLiquidityCombo)
            revert LockedLiquidityComboLimitReached();
    } else {
        _reduceLockedLiquidity(msg.sender, uint128(-deltaReserve), isCombo);
    }
}

```

콤보는 payout이 커지기 쉬워서(odds 곱), LP 레벨에서 `lockedLiquidityCombo`를 별도로 집계하고 별도 한도를 둔다. 또한 콤보 전용 한도를 초과하면 즉시 revert 된다.\
이러한 설계로 보아 콤보 베팅은 시스템 전체 리스크에 미치는 영향이 크기 때문에 일반 베팅과 분리된 한도, 더 보수적인 reinforcement 파라미터를 적용받는 것을 알 수 있다.

```solidity
function addCore(address core) external override onlyFactory {
        CoreData storage coreData = _getCore(core);
        coreData.minBet = 1;
        coreData.reinforcementAbility = uint64(FixedMath.ONE);
        coreData.reinforcementAbilityCombo = uint64(FixedMath.ONE / 2);
        coreData.state = CoreState.ACTIVE;

        emit CoreSettingsUpdated(
            core,
            CoreState.ACTIVE,
            uint64(FixedMath.ONE),
            uint64(FixedMath.ONE / 2), // 50% as default part for combo
            1
        );
    }
```

또한 코어 초기 설정에서도 콤보 관련 파라미터가 따로 잡혀 있다.

</details>

***

## Oracle & Data

***

## Governance

* Azuro는 거버넌스가 없슴다.. -> 디코에 문의 넣는 건 거버넌스가 아니라 운영 쪽으로 가야,,,

***

## Operation

* Access.sol 참고 -> NFT로 권한 관리\
  각 기능(Function)에 접근할 수 있는 역할(Role)을 비트 벡터(Bit Vector) 방식으로 관리한다.
* UpgradeableBeacon.sol 참고 -> 비콘프록시 구조

### Multi-Chain Deployment

Azuro는 동일한 컨트랙트 아키텍처를 여러 체인에 배포하도록 설계되어 있다. 이는 단순 배포 전략이 아니라 코드 레벨에서 명확히 드러난다.

#### (1) EIP-712 Domain에 chainId 포함

`block.chainid`와 `address(this)`가 domainSeparator에 포함되기 때문에, 같은 코드가 여러 체인에 배포되어도 체인이 다르면 domainSeparator가 달라진다.

결과적으로 A체인에서 만든 EIP-712 서명은 B체인에서 그대로 재사용(replay)될 수 없다. 이는 멀티체인에 동일 구조를 배포하는 운영을 하기 위해 반드시 필요한 안전장치이다.

#### (2) 오더 구조에 chainId 명시

```solidity
string private constant _CLIENTDATA_TYPE =
    "ClientData(string attention,address affiliate,address core,uint256 expiresAt,uint256 chainId,uint256 relayerFeeAmount,bool isFeeSponsored,bool isBetSponsored,bool isSponsoredBetReturnable)";
```

EIP-712 domainSeparator에 chainId가 들어가는 것과 별개로, 서명 payload 자체에도 chainId를 한 번 더 명시하는 구조다.

즉, 서명이 검증될 때 domainSeparator 수준에서 1차로 체인이 고정되고, ClientData 필드 수준에서 2차로 체인이 고정된다. 그래서 멀티체인 환경에서 cross-chain replay를 이중으로 방어한다.

#### (3) Beacon Proxy 구조

```solidity
contract UpgradeableBeacon is IBeacon {
    address private _implementation;
    address private _owner;

    event Upgraded(address indexed implementation);

    modifier onlyOwner() {
        require(msg.sender == _owner, "Ownable: caller is not the owner");
        _;
    }

    constructor(address implementation_) {
        _owner = msg.sender;
        _setImplementation(implementation_);
    }

    function implementation() external view override returns (address) {
        return _implementation;
    }

    function upgradeTo(address newImplementation) external onlyOwner {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }

    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "Beacon: implementation is not a contract");
        _implementation = newImplementation;
    }
}
```

Beacon Proxy 패턴은 프록시가 바라보는 구현 주소를 중앙(Beacon)에 모아 관리하는 업그레이드 방식이다.

멀티체인 배포 관점에서 중요한 점은 체인마다 Beacon 컨트랙트가 따로 존재하고, 체인마다 `upgradeTo()`가 독립적으로 실행된다는 것이다. 그래서 체인별로 구현체를 독립적으로 업그레이드할 수 있다.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://funarchy.gitbook.io/funarchy/introduction/analyzed-target/azuro.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
