Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Contract syntax #2058

spapinistarkware started this conversation in Ideas
Feb 8, 2023 · 12 comments · 50 replies
Discussion options

Suggestion of contract and ABI syntax based on traits.

ABI

Define the ABI separately as a trait. This is good for a few reasons:

  1. Other libraries can depend only on ABI to call the contract, and not on the implementation.
  2. Easier to make sure we are conforming to the same ABI when upgrading a contract.
  3. Implement the same ABI multiple times.
#[starknet::abi(event=ERC20Event)]
trait ERC20<T> {
 fn get_balance(self: T, user: address) -> u128;
}

Events

User code:

#[derive(Event)]
enum ERC20Event {
 Deposit: DepositEvent,
 Withdraw: WithdrawEvent,
}
#[derive(Event)]
struct DepositEvent {
 from: address,
 amt: u128
}

Under the hood, the derive will auto implement this trait:

trait Event<T> {
 fn keys(self: T) -> Array::<felt>;
 fn values(self: T) -> Array::<felt>;
 fn from_keys_and_values(keys: Array::<felt>, values: Array::<felt>) -> T;
}

The from_keys_and_values is more for outside tooling and inspection, as it doesn't really have a use
in a contract.

Contract storage

Rust is data oriented, as opposed to object oriented. Basically, this means we strive to separate
data and functionality.
For example, structs and impls are separated.
Inspired by this, it makes sense in Cairo1 to separate contract storage from functionality (impls).
Benefits:

  1. Easier to keep state backward compatilibity on upgrade.
  2. Contents of state is clear.
  3. Easy composition of contract states (by including the struct of another as a member of your contract).
  4. Possible to add multiple ABI implementations on the same struct.

Storage variables

#[starknet::storage]
struct MyStorage {
 balances: Mapping<address, u128>
}

Constructor

Starknet core will have the trait:

trait Contract<T> {
 type Input;
 type Output;
 fn construct(self: T, input: Input) -> Output;
}

User code:

#[starknet::storage]
struct MyStorage {
 ...
}
struct CtorInput {
 user: address,
 balance: felt,
}
impl MyContractImpl of Contract::<MyStorage> {
 type Input = CtorInput;
 type Output = String;
 fn construct(self: MyContract, inputs: CtorInput) -> String {
 self.balances.set(...);
 "Success"
 }
}

Implementations

Multiple ABI implementations can be added ontop of a given storage.

impl USDCImpl of ERC20::<MyStorage> {
 fn get_balance(self: MyStorage, user: address) -> u128 {
 self.emit(ERC20Event::BalanceRequested { user });
 self.balances.get(user)
 }
}
impl NISImpl of OwnedContract::<MyStorage> {
 ...
}
impl NISImpl of ERC20::<MyStorage> {
 fn get_balance(self: T, user: address) -> u128 {
 ...
 }
}

Component

A component is a storage struct along with a generic impl that every contract can use. This is one kind on extensibility (the other being composition).

struct ComponentStorage { ... }
#[abi]
trait ComponentABI { ... }
impl Component<Storage, impl HasComponent<Storage, ComponentStorage>> {
 ...
}

Then, a contract wishing to "extend" this component can just declare its impl using

impl MyComponentImpl = Component<MyStorage, _>;

Thi will impl the component abi for this contract.

Dispatcher

The #[starknet::abi] plugin would auto-generate a Dispatcher impl of the trait, that will look something like this:

struct ERC20Dispatcher {
 contract_address: Address,
 impl_selector: Selector,
}
impl ERC20DispatcherImpl of ERC20::<ERC20Dispatcher> {
 fn get_balance(self: T, user: address) -> u128 {
 let calldata = Array::new();
 user.serialize_into(calldata);
 let res = starknet::call_contract(
 address: self.contract_address,
 selector: self.impl_selector.compose("get_balance"),
 :call_data,
 );
 deserialize::<u128>(res)
 }
}

Note: The impl_selector is designed to avoid selector collisions between multiple impls for the same contract.
For example, a contract might want to implement ERC20 and ERC721, and they have the same function name.
A concrete suggestion is to use the name of the impl as the impl_selector.

Emitting events

The emitted events are a part of the interface of the contract - a part of the ABI.
They are indeed specified on the #[abi(event=EventType)] annotation.
Since a contract might have multiple impls of abis, to separate different sets of events, the first
event key will be the impl_selector of the impl.

Ideally, self.emit(event) will force the type of the event to be the type specified on the ABI.
This is not trivial, and left as an open problem for now.

Contract composition

#[starknet::storage]
struct MyOtherStorage {
 #[compose]
 inner: MyStorage,
}
impl OtherERC20Impl of ERC20::<MyOtherStorage> {
 fn get_balance(self: T, user: address) -> u128 {
 ERC20DispatcherImpl::get_balance(self.inner, user)
 }
}

Perhaps some plugin support for auto-generating a forwarding impl is required.

Contract class as an ensemble

A contract class is composed of a storage struct, and a set of ABI impls.
An ergonomic way to specify this set is by searching for all of the struct ABI impls in the current module.
An explicit way would be either externally (in the starknet cli, or some starknet config), managed by an outside tool.
It is also possible to specify the impls in some kind of annotation - not sure if it's desired.

Upgrade

In the latest starknet version, a new transaction was introduced to change the contract_class of a contract.
This is visioned to be the preferred way to upgrade a contract.
How to safely upgrade a contract, in a backwards compatible way?
There are two factors at play: ABI and state.

ABI

Since there might be other existing contracts calling our contract, we want to still support the exact same ABI as before.
A good way to do this is to keep the old impls of our contract, with the same impl name (as it will server as the
impl_selector). Their functionality might be changed. We can also introduce new impls for the same or different ABIs,
to add new functionalities.

State

TODO:

Deployment and tooling.

A starknet deploy tool will need to:

  • Discover all the relevant ABI impls of the contract (e.g. impls in the same module, or from a config).
  • Output all ther ABIs the contract supports.
  • On upgrade, check that the state and ABIs are backward compatible.
You must be logged in to vote

Replies: 12 comments 50 replies

Comment options

trait Event<T> {
 fn get_keys(self: T) -> Array::<felt>;
 fn get_values(self: T) -> Array::<felt>;
 fn from_keys_and_values(keys: Array::<felt>, values: Array::<felt>) -> T;
}

get_* is non-idiomatic in Rust. Not sure how code style rules are going to look like in Cairo, but I would rather see:

trait Event<T> {
 fn keys(self: T) -> @Array::<felt>;
 fn values(self: T) -> @Array::<felt>;
 fn from_keys_and_values(keys: Array::<felt>, values: Array::<felt>) -> T;
}

I have also used hypothetical snapshot syntax, because I feel like keys/values should be read-only?

You must be logged in to vote
1 reply
Comment options

Done

Comment options

I know this is not there yet, but I welcome treating procmacros as regular items in resolution scope, and would love them to be provided by a module/package:

#[starknet::storage]
struct MyContract {
 balances: Mapping<address, u128>
}

or

use starknet::storage;
#[storage]
struct MyContract {
 balances: Mapping<address, u128>
}
You must be logged in to vote
1 reply
Comment options

Done

Comment options

#[storage]
struct MyOtherContract {
 inner: MyContract,
}

Hmm, this seems to be conflicting with storage variables. I don't know how plugins API look like, but without access to type system (which forms a plugin<>types cycle!!, because type system needs plugins output in order to work), you won't be able to reliably differentiate storage vars from inner contracts...

maybe put it like this?

#[starknet::contract]
struct MyOtherContract {
 #[storage]
 myvar: felt,
 #[storage]
 myvar2: felt,
 #[extends]
 inner: MyContract,
}
You must be logged in to vote
13 replies
Comment options

Hm it is all semantics, so a tough problem to solve :p I don't have a strong preference

Comment options

I suggest renaming struct MyContract to struct MyStorage, the examples above are quite confusing. This could also help with this:

Embed for me sounds like you're embedding one contract in another, including its behaviours...?

Comment options

why is #[extends] or similar needed if there's no flattening?

Comment options

I suggest renaming struct MyContract to struct MyStorage, the examples above are quite confusing. This could also help with this:

Embed for me sounds like you're embedding one contract in another, including its behaviours...?

Done

Comment options

why is #[extends] or similar needed if there's no flattening?

Regular members are different than the compose(renamed from extends) ones.
A member is turned into a storage variable accessor. self.member won't be of the typoe specified, it will be something like StorageVar.
A composed contract on the other hand will have the type as is, or perhaps something like Substorage<Type> to allow passing it as the self to other functions.

Comment options

I was wondering what are your thoughts on this suggestion vs the syntax we have today (annotated module)
Note that I'm also not sure how to add composability and safety in the annotated module approach, so that might require some vision on your part:)

You must be logged in to vote
9 replies
Comment options

  1. Requirement of the "annotated module" syntax. These functions need access to storage variables which is only possible there in the module, but they shouldn't be part of the abi.
Comment options

  1. Actually, I even used the word "face" in a previous version to describe just that. These impls are exactly faces. Sound like you have some concrete syntax suggestion, but I'm not sure what it is. Can you describe it? Also include what is the "deployed contract" and its abi please:)
Comment options

  1. Added some proposal for constructors:)
Comment options

Views: I thought of something like this:

#[starknet::storage]
struct MyContract { ... }
#[starknet::view(MyContract)]
struct USDCMyContract {
 contract: MyContract,
}
impl ERC20 for USDCMyContract {
 ...
}
#[starknet::view(MyContract)]
struct PLNMyContract {
 contract: MyContract,
}
impl ERC20 for PLNMyContract {
 ...
}

Note: Implementing trait directly on contract struct will mean creating a Default view.

The deployed contract's ABI will include identifiers of all possible views, and I imagine having a function MyContract::view::<PLNMyContract>()

Comment options

That sounds like the rust way of overcoming the inability to have multiple impls. Wrapper types are good in that case.

But in Cairo, you have multiple impls, and impls will be first class citizen. So no need to have wrapper types.

Comment options

I find proposed Contract trait limiting. How are default values computed for each type? Mind that not every type may have 0_felt as a valid underlying value. It also seems not to be possible to construct the contract outside StarkNet, like in tests.

My proposal:

trait Contract<T> {
 type ConstructorArgs;
 fn construct(args: ConstructorArgs) -> Result<T>; // perhaps panic?
}
You must be logged in to vote
2 replies
Comment options

Interesting.
I like it, but I'm missing some details about how that migth work under the hood.
The storage struct that the user provides, at least in how I thought about this proposal is not "real" - it would get translated to something else.
Example:

#[starknet::storage]
struct MyContract {
 balances: Mapping<address, u128>,
 owner: address,
 #[compose]
 other: OtherContract
}

This actually might get translated (in the current suggestion) to something like

struct MyContract<const selector> {
}
impl MyContract<const selector> {
 balances: Mapping<mix(selector, 'balances'), address, u128>,
 owner: StorageVar<mix(selector, 'owner', address>
 other: OtherContract<mix(selector, 'other')>
}

So, what does construct actually return?
Maybe we need another generated struct for initialization?
And it will still need to support the set interfance, and maybe even call functions that are supposed to get MyContract.

Comment options

Hmm, with associated types (present in Rust, not in Cairo), you could actually attach a separate struct to use by the constructor:

struct MyContract<const selector> { ... }
struct MyContractPure {
 balances: Mapping<address, u128>,
 owner: address,
 // composed fields are not obvious to me, whether include them here or not?
}
impl MyContract<...> {
 type Initializer = MyContractInitializer;
}

This struct could be only used in constructor, but I would also happily see a function that builds *Pure on demand in contract code. Could be useful?

Comment options

How intra contract calls will look like? Knowing storage type of the contract being called should not be necessary.

You must be logged in to vote
10 replies
Comment options

Ad 1. Does it mean that in order to refer to some contract ABI I will need to provide type of its storage(ERC20<Storage> vs just ERC20)?
Ad 2. Why would I want to use ABIs internal functions? They should not be accessible to the external users, unless contract encapsulation model is very different from what we know from ethereum.

Comment options

is there any interest/possibility of having function overloading in cairo1? if the dispatcher is already distinguishing between ERC721::transfer_from and ERC20::transfer_from, could it also distinguish between ERC721::safe_transfer_from(from, to, token_id) and ERC721::safe_transfer_from(from, to, token_id, data)?

Comment options

Why would I want to use ABIs internal functions? They should not be accessible to the external users, unless contract encapsulation model is very different from what we know from ethereum.

i think there's extensibility use cases where you'd need access to internal functions (e.g. _approve in ERC20) but that's not part of the ABI. should all extensibility be ABI-based?

Comment options

Perhaps we can have trait-based extensibility and use function decorators to specify if the function should be present in the ABI.

Something like

#[starknet::abi(event=ERC721Event)]
trait IERC721<T> {
 #[view]
 fn balance_of(self: T, user: address) -> u128;
 #[internal] // or no decorator at all
 fn _mint(self:T, to: address, token_id: u128);
}
#[starknet::abi(event=ERC721MintableEvent)]
trait IERC721Mintable<T> {
 #[external]
 fn mint(self:T, to: address, token_id: u128);
}
#[derive(Event)]
enum ERC721Event {
 TransferEvent,
}
#[derive(Event)]
enum ERC721MintableEvent{
 MintEvent,
}
impl ERC721BaseImpl<MyContract> of IERC721::<MyContract> {
 fn balance_of(self: MyContract, user: address) -> u128 {
 self.balances.get(user)
 }
 fn _mint(self: MyContract, to: address, token_id: u128) {
 self.balances.set(to, token_id);
 }
}
impl ERC721Mintable<MyContract> of IERC721Mintable::<MyContract>{
 fn mint(self:MyContract, to: address, token_id: u128){
 self._mint(123, 1_u128);
 }
}

and then the contract ABI is the union of all ABIs of traits implemented

Comment options

You don't need the internal function uin the abi: it's not a part of the interface, and use can just have a free function (not inside the trait), ior just have a non-abi trait for your type. the abi trait is just for the interface. I don't see a benefit to putting it in the trait.

Comment options

For events more compact syntax should be available. I mean instead of:

#[derive(Event)]
enum ERC20Event {
 Deposit: DepositEvent,
 ...
}
#[derive(Event)]
struct DepositEvent {
 from: address,
 amt: u128
}

it should be possible to:

#[derive(Event)]
enum ERC20Event {
 Deposit: { from: address, amt: u128},
 ...
}
You must be logged in to vote
3 replies
Comment options

That is not in line with the rest of the language.
Why would it be possible in event but not in enum?

Comment options

In Rust it is possible to have an enum variant with named fields. Listing 6-2 in: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html

Comment options

Right. Not so in Cairo1. So maybe your suggestion is towards the enum syntax rather than contract syntax?

Comment options

Can you clarify the exact meaning of #[compose]? What happens if it is removed from the following example?

#[starknet::storage]
struct MyOtherContract {
 #[compose]
 inner: MyContract,
}
You must be logged in to vote
1 reply
Comment options

I answered at #2058 (comment)

Comment options

Is there any proposal/example on how extensibility should work when there's multiple modules being composed down the line? Say you want to have an account contract that extends an Account module, which in turns needs to leverage an ERC165 module.

  1. does the final contract need to be mindful of ERC165's ABI and storage?
  2. would there be any access to non-ABI functions (a.k.a. private methods)?
  3. should we expect any storage+logic encapsulation mechanism?
  4. how would that work when there's multiple modules using ERC165 behind. e.g. if my contract uses the Account and ERC721 modules (which require/implement ERC165 under the hood), would there be two ERC165Storage.supported_interfaces storage variables?
You must be logged in to vote
6 replies
Comment options

Funny how a comment with loosely defined terms became the most upvoted one in the discussion 😅

By extend and leverage I mean to reuse/inherit the functionality of a module or contract in another contract. This was discussed in the forum and the extensibility pattern became the predominant one in the language. Even the #[extends] macro was proposed above.

For example, the ERC165 module provides introspection functionality for contracts to reply through a supportsInterface getter whether they support a given interface or not. Accounts leverage this module (methods and associated storage) for many reasons, e.g. to let ERC721 contracts know they can handle tokens.

The problem though (and hence my questions above) is how to deal with a chain of dependencies since the proposed solutions above assumed the Account contract should be aware of the ERC165 storage structure which does not scale when there's multiple modules/contracts involved.

Given there's zero documentation on Cairo 1, this was completely unknown to me when I asked this a month ago; but now having played more with the language I get a better idea of what you mean by "call code of an implementation of another contract", although I'm not sure what you mean by "pass the required storage" since this is not needed today and IMO the best for encapsulation.

Comment options

Wasn't attacking. I literally was rereading a dozen times before giving up. That's also why I didn't originally reply.
We are in a "documentation" week, so more and more docs should become available the next few days:)

Note that Cairo is not going the object oriented way, and inheritance is not supported.
Instead, it encourages composition and polymorphism through traits and impls.

I think I gave the usage example above for everything this suggestion includes. If you jave a concrete question about how to implement something, I'd be happy to provide a sample implementation.

Comment options

An example of how would a contract A extend/compose a contract B that in turn uses another C contract would be very useful, if there's one above please link it since i've missed it. This is how we're extending modules today -- although you warned us that would stop working eventually (not sure why). I would assume that calling modules directly would be enough since it encapsulates storage while exposing reusable methods, but then I was puzzled when you said "you can just call it as code, and pass the required storage". Maybe I'm not understanding what you meant, but even if there's composition instead of inheritance wouldn't that require a contract to be aware of the storage used by a subcontract?

Another question would be how impls are expected to work. Today you cannot use the #[contract] directive on an impl, nor impls can have storage. How do you envision an IERC165 trait being implemented by an ERC165 impl (which has its own storage), that is used by an Account impl, which should also be composable/used by another impl/contract? Or how would you model such modules?

Comment options

An alternative would be to implement everything as contracts as we're doing today, but then they can't impl X of to make sure you're following an interface (trait).

Since contracts can't use traits and traits can't use storage, today we're embedding impls into contracts which is very ugly.

Comment options

Here is one possibility:

// b.cairo
#[storage]
struct B {
 #[compose]
 b: C
}
#[abi]
trait BTrait<Storage> {
 fn bar(self: B);
}
impl BImpl of BTrait<B> {..}
// a.cairo
#[storage]
struct A {
 #[compose]
 b: B
}
#[abi]
trait ATrait<Storage> {
 fn foo(self: A);
}
impl AImpl of ATrait<A> {
 fn foo(self: A) {
 self.b.bar(); //self.b is of type B, which is B's storage. We are passing it to bar.
 }
}

This is the composition pattern.
Another useful pattern (the equivalent for the diamond pattern in inheritence), when C is actually some interface that is needed by B0 and B1, both used by A. Here, B0 doesn't want to own C, just make sure it exists on the implementor - so it is not a standalone contract, but a component.

// starknet.cairo
trait HasStorage<Storage, SubStorage> {
 fn get(self:Storage) -> SubStorage;
}
// b0.cairo
#[storage]
struct B0 {..}
#[abi]
trait B0Trait<Storage>;
// Generic implementation for anything that has B0 and C.
impl B0Impl<Storage, impl HasB0: HasStorage<Storage,B0>, impl HasC: HasStorage<Storage, C>> of B0Trait<Storage>{
 fn foo(self: Storage) {
 HasB0::get(self).foo()
 HasC0::get(self).foo()
 }
}
#[storage]
struct A {
 #[compose] // will auto-implement HasStorage<A, C>
 c: C,
 #[compose]
 b0: B0,
 #[compose]
 b1: B1,
}
impl AImpl of CTrait<A> { ... }
// impl alias
impl AB0Impl = B0Impl<A, _, _>;
Comment options

  • On upgrade, check that the state and ABIs are backward compatible.

I'd be careful with this, since ABI compatibility does not guarantee equal behavior. As an example edge case, a contract may maintain the ABI but then revert for every call to a given function.

You must be logged in to vote
1 reply
Comment options

The intent here is not to guarantee behavior. It is to guarantee that the developer did not do a mistake with adhereing to the interface. The actualy implementation can be whatever he wants

Comment options

Here is what I think your Component proposal means / an example of what It could look like. I like this, with some slight concerns for backwards-compatibility.

////////////////////
// LinkToContract.cairo
#[storage]
struct ToContractStorage {
 address: ContractAddress;
};
#[abi]
trait LinkToContract<Storage> {
 #[view]
 get(self: Storage) -> ContractAddress;
 #[external]
 set(self: Storage, address: ContractAddress);
}
// Generic implementation for a given struct T.
// The second argument is a 'derivation path' to obtain a ToContractStorage-compatible struct from T. My imaginary syntax to call this is `self.get_subcomponent<A>()`
impl LinkToContract<T, impl A: DeriveComponent<T, ToContractStorage>> of LinkToContract<T> {
 fn get(self: T, ) -> ContractAddress { self.get_subcomponent<A>().address::read() }
 fn set(self: T, address: ContractAddress) { self.get_subcomponent<A>().address::write(address) }
}
////////////////////
// ERC20.cairo
#[storage]
struct Erc20Storage<ValueType> {
 balance: Mapping<ContractAddress, ValueType>
}
#[abi]
trait ERC20<Contract, ExternalValueType> {
 #[view]
 fn balanceOf(self: Contract, owner: ContractAddress) -> ExternalValueType;
 #[external]
 fn transferFrom(self: Contract, from: ContractAddress, to: ContractAddress, value: ExternalValueType);
}
// Again this 'derivation path' this time parametrised.
impl ERC20<Contract, ExternalValueType, InternalValueType, impl A: DeriveComponent<Contract, Erc20Storage<InternalValueType>> of ERC20<Contract, ExternalValueType>
{
 fn balanceOf(self: Contract, owner: ContractAddress) -> ExternalValueType {
 // Because we get an InternalValueType we need to convert it appropriately via `into`
 self.get_subcomponent<A>().balance::read(owner).into()
 }
 ...
}
////////////////////
// MyContractClass.cairo
#[contract_class]
mod MyContract {
 #[storage]
 struct MyStorage {
 #[component] // TBH not sure this would actually be needed? I'll leave it for clarity
 toContractA: ToContractStorage;
 // The storage addresses actually used by the storage below will be different (handled silently), though it's the same 'variable name'.
 // For backwards compatibility, it might be worth to be able to specify which is what I do in the parentheses.
 #[component(address: b_address)]
 toContractB: ToContractStorage;
 #[component]
 erc20: Erc20Storage<u64>;
 }
 impl ToA = LinkToContract<MyStorage, MyStorage::ToContractA>;
 impl ToB = LinkToContract<MyStorage, MyStorage::ToContractB>;
 // Declare our interfaces alongside their implementation. One outputs felts, one outputs u64, one outputs u256. They all use the same _generic_ implementation and the same storage.
 impl FeltBasedInterface = ERC20<MyStorage, felt252, u64, MyStorage::erc20>; // Imaginary syntax to specofiy the 'derivation path' at the end.
 impl u256BasedInterface = ERC20<MyStorage, u256, u64, MyStorage::erc20>;
 impl u64BasedInterface = ERC20<MyStorage, u64, u64, MyStorage::erc20>;
}
// Can now call the above kinda like that:
MyContract { address, FeltBasedInterface }.balanceOf(owner: ContractAddress) -> felt262
MyContract { address, u256BasedInterface }.balanceOf(owner: ContractAddress) -> u256
MyContract { address, u64BasedInterface }.balanceOf(owner: ContractAddress) -> 64
You must be logged in to vote
1 reply
Comment options

Yes, I agree with this:)
We still need to work out the details of the syntax.
I think I like the contract_class name vs the old contact

Comment options

It seems confusing that the same syntax is used to make function calls and to emit events. The only differentiation is the capitalization, but that is by convention rather than enforced by the compiler.

Here is an example using ERC-20 to illustrate my point, but I think it would be even harder to differentiate if both the function and event have the same input arguments.

#[event]
fn Transfer(from: ContractAddress, to: ContractAddress, value: u256) {}
#[external]
fn transfer(to: ContractAddress, value: u256) -> bool {...}
// To make a function call
transfer(to, val);
// To emit an event
Transfer(to, from, val);

It would be nice if function calls and emitting of events can be better differentiated instantly without double checking their definitions. For example, the Cairo 0.x syntax of Event.emit(...) would achieve this.

You must be logged in to vote
2 replies
Comment options

Is this related to the suggestion above? Or to the current Cairo 1 syntax?
Anyway, event.emit() makes sense to me.

Comment options

Is this related to the suggestion above? Or to the current Cairo 1 syntax?

For the current Cairo 1 syntax, wasn't sure where was the best place to start a discussion on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /