Contracts
Overview
The contracts module facilitates the creation of a contract between two parties and how it will be maintained over its life cycle. These contracts are designed to be settled at a future date and involve the exchange of both fungible and non-fungible tokens, subject to customized terms and conditions.
Concepts
Contract
A forward agreement, also known as a forward contract, is a financial derivative contract that allows two parties to agree on the terms of a transaction to be settled in the future. It involves an agreement to buy or sell some fungible/non-fungible tokens at a predetermined price on a specified time period in the future. The price, quantity, and settlement period are typically fixed at the time the contract is entered into.
A forward contract can be visualized as shown in the following image:
To outline the specified items within a contract:
- Counter-Party Terms:
- NFT: Each counterparty will have an NFT representing their position on the contract.
- Provided Tokens: Tokens that are meant to be delivered by the provider side of contract.
- Requester Tokens: Tokens that are meant to be delivered by the requester side of contract.
- Deposit Address: Each party may designate a secondary address to serve as the receiving address for settlements.
- Buy-in Enabled: Each party can select if they want to enable the buy-in option or not. Where if one party defaults another can step in and fulfill the order and keep the escrow.
- Settlement Limit: As settlement will happen automatically on the specified settlement period, each party can continuously adjust what they’re willing to be fulfilled on settlement.
- Contract Terms:
- Provider Escrow: The escrow that provider has put in escrow account. (In an order flow, this would be either of a fixed amount of stable coin or a portion of underlying asset. But generally in other flows, this can be any basket of tokens).
- Requester Escrow: ****The escrow that requester has put in escrow account. (In an order flow, this would be either of a fixed amount of stable coin or a portion of underlying asset. But generally in other flows, this can be any basket of tokens).
- Settlement Period: The agreed upon time range during which both sides are happy to have their trade settled that is independent of when the contract was created or signed.
- Partial Settlement Allowed: The option to settle just a portion of the contract and pay the penalty portion for the rest. This option must be approved by both parties to take place.
- Escrow Account: A dedicated account established for each contract to securely hold escrows and ensure party obligations are fulfilled. This account is exclusive to the specific contract and will not be shared with other accounts or contracts.
- Contract’s metadata:
- Id: Any contract would have an ID for identification of it. This ID is also used as a seed to mint NFTs.
- Creator: The address of the contract's creator. Contracts can be created by any third party, including counterparties, other accounts, modules, or even dApps.
- Creation Time: The timestamp when the contract was created.
- Settlement Time: The precise timestamp indicating when the settlement occurred. This field remains empty until the settlement is completed.
- Status: Indicates the status of the contract based on it’s life-cycle discussed here.
- Initial Provider/Requester: As counterparties can transfer their NFTs to others, the roles of provider and requester may change throughout the contract’s life-time. However, the addresses of the original provider and requester will be retained.
Tokens
To allow contracts to handle both fungible and non-fungible assets, the Token structure is designed. This structure is capable of representing either a fungible or a non-fungible asset at any given time.
An array of Tokens is then combined into a new structure called Tokens, which provides enhanced functionality and flexibility for contract management.
This allows you to sell a basket of fungible and non fungible tokens in one go.
Contract Terms
After specifying both party details and provided/requested assets, there are some terms that parties must specify for a contract. Here we review them:
- Escrow Amount: Upon signing the contract, both parties will deposit the agreed-upon escrow tokens into the escrow account. This ensures risk mitigation for future settlements by both parties.
- Settlement Period: The settlement period is the time frame within which the settlement must take place. Parties have the option to allow for early settlement by defining a period of time for the settlement instead of a fixed time. If both parties have set their
settlement limit
to their entire obligation, early settlement can occur. Otherwise, the settlement would either fail or occur partially (based on the partial settlement flag) at the end of the settlement period. Alternatively, both parties can specify a fixed time by setting the start and end of the period to the same value. - Partial Settlement Allowed: The option to settle just a portion of the contract and pay the penalty portion for the rest. This option must be approved by both parties to take place.
Risk Mitigation
By incorporating flexibility into the management of assets leading up to settlement, there is a potential for an increase in counter-party risks. This risk emerges when a counter-party fails to meet their obligations despite initiating their own orders, with no assurance of loyalty to their commitments.
To address this risk, it is advisable for both sellers and buyers to allocate a portion of their total obligation into escrow within a secure account that is bound to the contract between them. The escrow amount could be a fraction of the obligation or an agreed-upon amount of stable coins.
The escrow will be held in the account until the time of settlement. If the user fully fulfills their obligation, the escrow will be released and returned to the user, or considered as part of the fulfillment of the obligation if they want.
However, if the user fails to fulfill the obligation or only fulfills a portion of it, they will forfeit a proportional amount of their escrow as a penalty. This amount will be transferred to the other counter-party as compensation.
In addition to this, it is important to establish clear terms and conditions regarding the escrow arrangement in the order creation time to ensure transparency and fairness for both parties involved. This will help in minimizing the potential for disputes and misunderstandings related to the escrow process.
Contract Life-Cycle
A contract will go through different phases over it’s lifetime. The following state machine shows how a contract will change it’s phase during it’s life-cycle.
As shown in above image, there are 3 main phases during a contract’s life cycle:
1. Initialization Phase
Initializing a contract can be requested by anyone, including one of the parties, third-party DApps, or any other users. During contract initialization, only the predetermined terms of the contract are filled out, and no execution occurs at that point. This feature enables the development of a diverse range of applications utilizing the forwards chain.
As a result of this phase, a contract with all terms and an initialized
status will be created on-chain.
2. Signing Phase
For a contract to be binding, it is necessary for both parties involved or their authorized representatives to sign the contract. If only one party has signed the contract, it will be in a pending_second_sign
state until the second party or their authorized representative also signs the contract. Once both parties have signed, the contract will be considered started
and is going to be executed.
When a party signs the contract, the agreed-upon escrow amount will be transferred from their wallet to the escrow account associated with the contract. As stated in the contract terms, the escrow can be a percentage of the obligation or a specific amount in any coins.
When a party signs the contract, they automatically set the settlement limit to their entire remaining obligation. This limit determines the allowed tokens to be settled during the settlement period. It is important to note that the party can change this settlement limit at any time before the settlement happens.
Changing the settlement limit allows a party to restrict a contract's access to only a portion of its tokens. For instance, if a party decides that they do not wish to settle the entire obligation, they can choose to only pay half of it, even if they have the whole assets in their wallet. This change is applicable only to contracts that agreed on the partial settlement. Otherwise, the party can set the settlement limit to zero, effectively denying settlement and opting to pay the penalty instead.
A unique forward position NFT will be created and transferred to each party when the contract starts.
The NFT owner is always the counter-party of the contract. Therefore, if one party transfers their NFT to another user, the contract's counterparty will change from that point onward.
3. Settlement Phase
As parties have specified the contract to transfer their obligation from their wallets, this will be done automatically by the contract. The parties would control the settlement only by modifying the settlement limit of the contract.
Automatic settlement can happen in one of these two times:
-
Early Settlement:** Every EndBlock checks if both sides have the required obligation in their wallets and if they have set the settlement limit of the contract with the same remained obligation tokens. If the conditions are met, early settlement occurs and the contract transfers the remained obligations from both sides into the escrow account, finalizes the contract, releases the escrow amounts, burns position nfts, and settles the amounts to each party.
-
End Of Settlement Period: the chain will verify the wallets of both parties to determine the amount of obligations they have and how much they have specified for the settlement limit. At this stage, the following scenarios may occur:
- In case which partial settlement agreed upon, the contract would transfer the maximum specified settlement limit and available assets to an escrow account, calculate the penalty for each party, finalize the contract, and settle the assets to the respective parties. If one party has enabled a buy-in while the other party has only fulfilled part of their obligation, a buy-in auction would be initiated to find the best bid.
- In case which partial settlement didn’t agreed, the contract will verify if both parties possess the necessary assets in their wallets and have specified the required settlement limit. If both conditions are met, the tokens will be transferred to the escrow account, the contract will be finalized, NFTs will be burned, and tokens will be settled with the new owners. However, if one party fails to meet the requirements, and the other party has enabled the buy-in option, the failed party will be penalized, the contract will be finalized, and both NFTs will be burned. Then an automatic auction would be initiated to find the best bid.
Buy-in Auction
If someone doesn't settle their obligation on a contract and the Escrow amount is sat in there, then an auction could be held to deliver the original asset to the other side. There might be two types of traders who would want to join a contract:
- The person who is happy to receive the escrow amount as penalty if it fails
- The person who actually just wants to get the tokens and can't really be bothered with having to make a new order if the other side fails to deliver - they'd happily let someone else take all/portion of the escrow amount if someone else delivered the tokens when the other party failed
Here is the definition of buy-in in traditional finance:
“A buy-in is an occurrence in which an investor is forced to repurchase shares of a security because the seller of the original shares did not deliver the securities in a timely fashion or did not deliver them at all.”
Buy-in can be offered as an option for each party involved in the contract. This option can be enabled at the time of contract creation and update. While there may be an additional fee for the party enabling this option, it can significantly enhance the chance of successful delivery.
The buy-in option is permitted for contracts involving the trade of a single fungible token on both sides, which is an important consideration.
Life-Cycle Scenario Example
To clarify everything let’s make an example. We have a DApp called Green, a user called Blue, and another user called Red. The following picture shows a timeline that we’re just going through.
As shown in the picture, DApp Green initiates a contract with the following terms:
- Red will deliver 1 ETH.
- Blue will deliver 2000 USDC.
- Both parties will escrow 10% of their obligations.
- Partial settlement is allowed with mutual approval.
- Blue requests a buy-in option in case Red fails to deliver.
- The settlement period is from the 5th to the 10th of March 2025.
The contract is initiated on March 1, 2025, and Blue and Red are given until March 2nd to sign it. Fortunately, both parties signed the contract before it expired, and the contract becomes active.
After a few hours, Red decides to sell their NFT and exit the contract. They set their settlement limit for this contract to zero and list their NFT for a trade-up. However, after a day, no interesting suggestions are received. To avoid penalties, Red sets their settlement limit for the contract to half of the remained obligation.
On March 6th, Red decides to use their tokens elsewhere. They modify the settlement limit to fulfill only 0.1 ETH. Then, on March 9th, Red decides to increase their fulfillment in order to reduce the penalty. They modify their settlement limit to automatically settle 0.45 ETH.
On the 10th of March, which marks the end of the settlement period, the contract checks the status of wallet balances and settlement limits:
- Red has 500 ETH in their wallet but has limited the settlement to 0.45 ETH.
- Blue has 1800 USDC in their wallet and has limited the settlement of only 1800 USDC.
Based on this information, the contract proceeds as follows:
- Red had put 0.1 ETH as escrow and fulfilled half of their remaining obligation, which is 0.45 ETH. Therefore, they will pay (0.45 + 0.05) ETH as the obligation and 0.05 ETH as a penalty. They will also receive 1000 USDC.
- escrow: 0.1 ETH
- remained obligation: 0.9 ETH
- settled_portion: 0.45 ETH / 0.9 ETH = 0.5
- penalty_portion:= 1 - settled_portion = 0.5
- penalty: penalty_portion * escrow = 0.5 * 0.1 ETH = 0.05 ETH
- Blue had put 200 USDC as escrow and fulfilled all of their obligations. They will not be penalized and will receive 0.5 ETH and an additional 0.05 ETH as compensation.
Since Blue had enabled the buy-in option, an automatic auction will be initiated with a price of 0.5 ETH against the remained 1000 USDC which is in the escrow account now.
Messages
MsgCreateContract
This message initiates a forward contract with initialized
state. The response includes the ID of created contract.
message MsgCreateContract {
option (cosmos.msg.v1.signer) = "creator";
string creator = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string provider = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
CounterPartySettings provider_settings = 3;
repeated Token provided = 4 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
string requester = 5 [(cosmos_proto.scalar) = "cosmos.AddressString"];
CounterPartySettings requester_settings = 6;
repeated Token requested = 7 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
ContractTerms terms = 8;
string preferred_fee_denom = 9;
}
message CounterPartySettings {
string deposit_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
bool buy_in_enabled = 2;
repeated Token settlement_limit = 3 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
}
message ContractTerms {
Period settlement_period = 1;
bool partial_settlement_allowed = 2;
repeated Token provider_initial_escrow = 3 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
repeated Token requester_initial_escrow = 4 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
}
message MsgCreateContractResponse {
uint64 contract_id = 1;
}
MsgSignContract
This message is used by any of parties or their representatives to sign a contract. As mentioned earlier, the first sign would result in pending_second_sign
status and the second sign would result in started
status. The response would contain the resulted status of contract.
message MsgSignContract {
option (cosmos.msg.v1.signer) = "creator";
string creator = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
uint64 contract_id = 2;
repeated Token obligation = 3 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
string preferred_fee_denom = 4;
}
message MsgSignContractResponse {
ContractStatus status = 1;
}
MsgUpdateCounterParty
This message is used by any of the parties to update their settings including their deposit address, enabling buy-in or changing the settlement limit.
message MsgUpdateCounterParty {
option (cosmos.msg.v1.signer) = "creator";
string creator = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
uint64 contract_id = 2;
string address = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string deposit_address = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"];
bool buy_in_enabled = 5;
repeated Token settlement_limit = 6 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
string preferred_fee_denom = 7;
}
message MsgUpdateCounterPartyResponse {}
MsgBidOnBuyIn
This message is used by third party users to bid on buy-in auctions.
message MsgBidOnBuyIn {
option (cosmos.msg.v1.signer) = "bidder";
string bidder = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
uint64 auction_id = 2;
cosmos.base.v1beta1.Coin proposal = 3 [(gogoproto.nullable) = false];
string preferred_fee_denom = 4;
}
message MsgBidOnBuyInResponse {}
MsgUpdateModuleParams
This message updates the module parameters. Only the governance can execute this message.
message MsgUpdateModuleParams {
option (cosmos.msg.v1.signer) = "authority";
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
Params params = 2 [(gogoproto.nullable) = false];
}
message Params {
google.protobuf.Duration buy_in_auction_duration = 1 [
(gogoproto.stdduration) = true,
(gogoproto.nullable) = false
];
google.protobuf.Duration sign_time_limit = 2 [
(gogoproto.stdduration) = true,
(gogoproto.nullable) = false
];
}
message MsgUpdateModuleParamsResponse {}
MsgUpdateFeeConfig
This message updates the module’s fee configurations. Only the governance can execute this message.
message MsgUpdateFeeConfig {
option (cosmos.msg.v1.signer) = "authority";
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
FeeConfigs fee_config = 2 [(gogoproto.nullable) = false];
}
message FeeConfigs {
FeeConfig contract_creation = 1 [(gogoproto.nullable) = false];
FeeConfig contract_signing = 2 [(gogoproto.nullable) = false];
FeeConfig contract_update = 3 [(gogoproto.nullable) = false];
FeeConfig bid_on_buy_in = 4 [(gogoproto.nullable) = false];
FeeConfig buy_in_creation = 5 [(gogoproto.nullable) = false];
}
message FeeConfig {
bool enabled = 1;
repeated cosmos.base.v1beta1.Coin fixed = 2;
string percentage_of_underlying_asset = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = true
];
}
message MsgUpdateFeeConfigResponse {}
Queries
Params
This query fetches the parameters of the module.
message QueryParamsRequest {}
message QueryParamsResponse {
// params holds all the parameters of this module.
Params params = 1 [(gogoproto.nullable) = false];
}
FeeConfigs
This query fetches the fee configurations of the module.
message QueryFeeConfigsRequest {}
message QueryFeeConfigsResponse {
// fee config holds all the fee configurations of this module.
FeeConfigs fee_configs = 1 [(gogoproto.nullable) = false];
}
Contract
This query retrieves a contract using its id.
message QueryGetContractRequest {
uint64 id = 1;
}
message QueryGetContractResponse {
Contract contract = 1 [(gogoproto.nullable) = false];
}
ContractAll
This query fetches all contracts.
message QueryAllContractRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}
message QueryAllContractResponse {
repeated Contract contracts = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
BuyInAuction
This query retrieves a buy-in auction using its id.
message QueryGetBuyInAuctionRequest {
uint64 id = 1;
}
message QueryGetBuyInAuctionResponse {
BuyInAuction buy_in_auction = 1 [(gogoproto.nullable) = false];
}
BuyInAuctionAll
This query fetches all buy-in auctions.
message QueryAllBuyInAuctionRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}
message QueryAllBuyInAuctionResponse {
repeated BuyInAuction buy_in_auctions = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
Events
ContractInitiatedEvent
Emitted when a contract is initiated. It includes the contract information.
message ContractInitiatedEvent {
uint64 id = 1;
string creator = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string provider = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];
CounterPartySettings provider_settings = 4;
repeated Token provided = 5 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
string requester = 6 [(cosmos_proto.scalar) = "cosmos.AddressString"];
CounterPartySettings requester_settings = 7;
repeated Token requested = 8 [
(gogoproto.castrepeated) = "Tokens",
(gogoproto.nullable) = false
];
string escrow_account = 9 [(cosmos_proto.scalar) = "cosmos.AddressString"];
ContractTerms terms = 10;
}
ContractSignedEvent
Emitted when a contract is signed. It includes the signer’s settings.
message ContractSignedEvent {
uint64 contract_id = 1;
string counterparty = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string counterparty_deposit_address = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];
bool buy_in_enabled = 4;
}
ContractStartedEvent
Emitted when a contract is signed by both sides and started. It includes the minted NFT details.
message ContractStartedEvent {
uint64 id = 1;
string nft_class_id = 2;
string provider_nft_id = 3;
string requester_nft_id = 4;
}
ContractCounterPartyUpdatedEvent
Emitted when a party updates their settings. It includes the updated party settings.
message ContractCounterPartyUpdatedEvent {
uint64 contract_id = 1;
string address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
string deposit_address = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];
bool buy_in_enabled = 4;
}
ContractSettledSuccessfullyEvent
Emitted when a contract settled successfully. It includes the contract’s ID.
message ContractSettledSuccessfullyEvent {
uint64 id = 1;
}
BuyInAuctionCreatedEvent
Emitted when a buy-in auction is created automatically. It includes the buy-in auction’s information.
message BuyInAuctionCreatedEvent {
uint64 auction_id = 1;
string auction_owner = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
cosmos.base.v1beta1.Coin amount = 3;
cosmos.base.v1beta1.Coin base_price = 4;
google.protobuf.Timestamp creation_time = 5 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = true
];
google.protobuf.Timestamp expiration_time = 6 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = true
];
}
NewBidOnBuyInAuctionEvent
Emitted when a bid is placed on a buy-in auction. It includes the bid’s information.
message NewBidOnBuyInAuctionEvent {
uint64 auction_id = 1;
string bidder = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
cosmos.base.v1beta1.Coin proposal = 3 [(gogoproto.nullable) = false];
}
BuyInAuctionFinishedEvent
Emitted when a buy-in auction is finalized. It includes the winner bid’s information.
message BuyInAuctionFinishedEvent {
uint64 auction_id = 1;
string winner_bidder = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
cosmos.base.v1beta1.Coin winner_proposal = 3 [(gogoproto.nullable) = true];
}
State
Contract
Contracts are stored using their IDs as store keys:
(
[]byte("/Contracts/value/") |
[]byte(id)
) -> ProtoBuf(Contract)
Contracts are also indexed with their owner, state, end of settlement period:
type ContractIndexes struct {
// Owner is a multi index that indexes contracts by their owner address.
Owner *indexes.Multi[string, uint64, types.Contract]
// State is a multi index that indexes orders by their state.
State *indexes.Multi[string, uint64, types.Contract]
// SettlementPeriodEnd is a multi index that indexes orders by their settlement period end time.
SettlementPeriodEnd *indexes.Multi[time.Time, uint64, types.Contract]
}
Buy-in Auction
Buy-in auctions are stored using their IDs as store keys:
(
[]byte("/BuyInAuctions/value/") |
[]byte(id)
) -> ProtoBuf(BuyInAuction)
Buy-in auctions are also indexed with their owner, expiration time:
type BuyInAuctionIndexes struct {
// Owner is a multi index that indexes buy-in auctions by their owner address.
Owner *indexes.Multi[string, uint64, types.BuyInAuction]
// ExpirationTime is a multi index that indexes buy-in auctions by their expiration time.
ExpirationTime *indexes.Multi[time.Time, uint64, types.BuyInAuction]
}
Params
Module parameters are stored under the following store key:
([]byte("Params/")) -> ProtoBuf(Params)
FeeConfigs
Module fee configs are stored under the following store key:
([]byte("FeeConfigs/")) -> ProtoBuf(FeeConfigs)
Parameters
Contract Params
sign_time_limit
: This parameter defines how much do parties have time to sign a contract.- Type:
google.protobuf.Duration
- Range:
[0, inf]
- Default Value:
3600000000000 (1 Hour)
- Type:
Buy-in Auction Params
buy_in_auction_duration
: This parameter defines how much do parties have time to sign a contract.- Type:
google.protobuf.Duration
- Range:
[0, inf]
- Default Value:
600000000000 (10 minutes)
- Type:
Genesis
The genesis state of this module includes:
params
: Parameters for the contracts module.fee_configs
: Fee configurations of the contracts module.contracts_list
: List of all contracts.contracts_starting_id
: Next contract id that can be generated.contracts_count
: Number of contracts.buy_in_auctions_list
: List of all buy-in auctions.buy_in_auctions_starting_id
: Next buy-in auction id that can be generated.buy_in_auctions_count
: Number of buy-in auctions.